diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.js b/test/components/views/dialogs/AccessSecretStorageDialog-test.js deleted file mode 100644 index d90b23baac..0000000000 --- a/test/components/views/dialogs/AccessSecretStorageDialog-test.js +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; - -import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import { stubClient } from '../../../test-utils'; -import { findById, flushPromises } from '../../../test-utils'; -import AccessSecretStorageDialog from "../../../../src/components/views/dialogs/security/AccessSecretStorageDialog"; - -describe("AccessSecretStorageDialog", function() { - it("Closes the dialog if _onRecoveryKeyNext is called with a valid key", async () => { - const onFinished = jest.fn(); - const checkPrivateKey = jest.fn().mockResolvedValue(true); - const wrapper = mount( - , - ); - wrapper.setState({ - recoveryKeyValid: true, - recoveryKey: "a", - }); - const e = { preventDefault: () => {} }; - - wrapper.find('form').simulate('submit', e); - - await flushPromises(); - - expect(checkPrivateKey).toHaveBeenCalledWith({ recoveryKey: "a" }); - expect(onFinished).toHaveBeenCalledWith({ recoveryKey: "a" }); - }); - - it("Considers a valid key to be valid", async function() { - const wrapper = mount( - true} - />, - ); - stubClient(); - MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => 'a raw key'; - MatrixClientPeg.get().checkSecretStorageKey = () => true; - - const v = "asdf"; - const e = { target: { value: v } }; - act(() => { - findById(wrapper, 'mx_securityKey').find('input').simulate('change', e); - }); - // force a validation now because it debounces - await wrapper.instance().validateRecoveryKey(); - const { recoveryKeyValid } = wrapper.instance().state; - expect(recoveryKeyValid).toBe(true); - }); - - it("Notifies the user if they input an invalid Security Key", async function() { - const wrapper = mount( - false} - />, - ); - const e = { target: { value: "a" } }; - stubClient(); - MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => { - throw new Error("that's no key"); - }; - - act(() => { - findById(wrapper, 'mx_securityKey').find('input').simulate('change', e); - }); - // force a validation now because it debounces - await wrapper.instance().validateRecoveryKey(); - - const { recoveryKeyValid, recoveryKeyCorrect } = wrapper.instance().state; - expect(recoveryKeyValid).toBe(false); - expect(recoveryKeyCorrect).toBe(false); - - wrapper.setProps({}); - const notification = wrapper.find(".mx_AccessSecretStorageDialog_recoveryKeyFeedback"); - expect(notification.props().children).toEqual("Invalid Security Key"); - }); - - it("Notifies the user if they input an invalid passphrase", async function() { - const wrapper = mount( - false} - onFinished={() => {}} - keyInfo={{ - passphrase: { - salt: 'nonempty', - iterations: 2, - }, - }} - />, - ); - const e = { target: { value: "a" } }; - stubClient(); - MatrixClientPeg.get().isValidRecoveryKey = () => false; - wrapper.instance().onPassPhraseChange(e); - await wrapper.instance().onPassPhraseNext({ preventDefault: () => { } }); - - wrapper.setProps({}); - const notification = wrapper.find(".mx_AccessSecretStorageDialog_keyStatus"); - expect(notification.props().children).toEqual( - ["\uD83D\uDC4E ", "Unable to access secret storage. Please verify that you " + - "entered the correct Security Phrase."]); - }); -}); diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx b/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx new file mode 100644 index 0000000000..1376c18c3b --- /dev/null +++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx @@ -0,0 +1,166 @@ +/* +Copyright 2020, 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { IPassphraseInfo } from 'matrix-js-sdk/src/crypto/api'; + +import { findByTestId, getMockClientWithEventEmitter, unmockClientPeg } from '../../../test-utils'; +import { findById, flushPromises } from '../../../test-utils'; +import AccessSecretStorageDialog from "../../../../src/components/views/dialogs/security/AccessSecretStorageDialog"; + +describe("AccessSecretStorageDialog", () => { + const mockClient = getMockClientWithEventEmitter({ + keyBackupKeyFromRecoveryKey: jest.fn(), + checkSecretStorageKey: jest.fn(), + isValidRecoveryKey: jest.fn(), + }); + const defaultProps = { + onFinished: jest.fn(), + checkPrivateKey: jest.fn(), + keyInfo: undefined, + }; + const getComponent = (props ={}): ReactWrapper => + mount(); + + beforeEach(() => { + jest.clearAllMocks(); + mockClient.keyBackupKeyFromRecoveryKey.mockReturnValue('a raw key' as unknown as Uint8Array); + mockClient.isValidRecoveryKey.mockReturnValue(false); + }); + + afterAll(() => { + unmockClientPeg(); + }); + + it("Closes the dialog when the form is submitted with a valid key", async () => { + const onFinished = jest.fn(); + const checkPrivateKey = jest.fn().mockResolvedValue(true); + const wrapper = getComponent({ onFinished, checkPrivateKey }); + + // force into valid state + act(() => { + wrapper.setState({ + recoveryKeyValid: true, + recoveryKey: "a", + }); + }); + const e = { preventDefault: () => {} }; + + act(() => { + wrapper.find('form').simulate('submit', e); + }); + + await flushPromises(); + + expect(checkPrivateKey).toHaveBeenCalledWith({ recoveryKey: "a" }); + expect(onFinished).toHaveBeenCalledWith({ recoveryKey: "a" }); + }); + + it("Considers a valid key to be valid", async () => { + const checkPrivateKey = jest.fn().mockResolvedValue(true); + const wrapper = getComponent({ checkPrivateKey }); + mockClient.keyBackupKeyFromRecoveryKey.mockReturnValue('a raw key' as unknown as Uint8Array); + mockClient.checkSecretStorageKey.mockResolvedValue(true); + + const v = "asdf"; + const e = { target: { value: v } }; + act(() => { + findById(wrapper, 'mx_securityKey').find('input').simulate('change', e); + wrapper.setProps({}); + }); + await act(async () => { + // force a validation now because it debounces + // @ts-ignore + await wrapper.instance().validateRecoveryKey(); + wrapper.setProps({}); + }); + + const submitButton = findByTestId(wrapper, 'dialog-primary-button').at(0); + // submit button is enabled when key is valid + expect(submitButton.props().disabled).toBeFalsy(); + expect(wrapper.find('.mx_AccessSecretStorageDialog_recoveryKeyFeedback').text()).toEqual('Looks good!'); + }); + + it("Notifies the user if they input an invalid Security Key", async () => { + const checkPrivateKey = jest.fn().mockResolvedValue(false); + const wrapper = getComponent({ checkPrivateKey }); + const e = { target: { value: "a" } }; + mockClient.keyBackupKeyFromRecoveryKey.mockImplementation(() => { + throw new Error("that's no key"); + }); + + act(() => { + findById(wrapper, 'mx_securityKey').find('input').simulate('change', e); + }); + // force a validation now because it debounces + // @ts-ignore private + await wrapper.instance().validateRecoveryKey(); + + const submitButton = findByTestId(wrapper, 'dialog-primary-button').at(0); + // submit button is disabled when recovery key is invalid + expect(submitButton.props().disabled).toBeTruthy(); + expect( + wrapper.find('.mx_AccessSecretStorageDialog_recoveryKeyFeedback').text(), + ).toEqual('Invalid Security Key'); + + wrapper.setProps({}); + const notification = wrapper.find(".mx_AccessSecretStorageDialog_recoveryKeyFeedback"); + expect(notification.props().children).toEqual("Invalid Security Key"); + }); + + it("Notifies the user if they input an invalid passphrase", async function() { + const keyInfo = { + name: 'test', + algorithm: 'test', + iv: 'test', + mac: '1:2:3:4', + passphrase: { + // this type is weird in js-sdk + // cast 'm.pbkdf2' to itself + algorithm: 'm.pbkdf2' as IPassphraseInfo['algorithm'], + iterations: 2, + salt: 'nonempty', + }, + }; + const checkPrivateKey = jest.fn().mockResolvedValue(false); + const wrapper = getComponent({ checkPrivateKey, keyInfo }); + mockClient.isValidRecoveryKey.mockReturnValue(false); + + // update passphrase + act(() => { + const e = { target: { value: "a" } }; + findById(wrapper, 'mx_passPhraseInput').at(1).simulate('change', e); + }); + wrapper.setProps({}); + + // input updated + expect(findById(wrapper, 'mx_passPhraseInput').at(0).props().value).toEqual('a'); + + // submit the form + act(() => { + wrapper.find('form').at(0).simulate('submit'); + }); + await flushPromises(); + + wrapper.setProps({}); + const notification = wrapper.find(".mx_AccessSecretStorageDialog_keyStatus"); + expect(notification.props().children).toEqual( + ["\uD83D\uDC4E ", "Unable to access secret storage. Please verify that you " + + "entered the correct Security Phrase."]); + }); +}); diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js deleted file mode 100644 index 798a30886c..0000000000 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; -import MatrixReactTestUtils from 'matrix-react-test-utils'; -import { sleep } from "matrix-js-sdk/src/utils"; - -import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import * as TestUtilsMatrix from '../../../test-utils'; -import InteractiveAuthDialog from "../../../../src/components/views/dialogs/InteractiveAuthDialog"; - -describe('InteractiveAuthDialog', function() { - let parentDiv; - - beforeEach(function() { - TestUtilsMatrix.stubClient(); - parentDiv = document.createElement('div'); - document.body.appendChild(parentDiv); - }); - - afterEach(function() { - ReactDOM.unmountComponentAtNode(parentDiv); - parentDiv.remove(); - }); - - it('Should successfully complete a password flow', function() { - const onFinished = jest.fn(); - const doRequest = jest.fn().mockResolvedValue({ a: 1 }); - - // tell the stub matrixclient to return a real userid - const client = MatrixClientPeg.get(); - client.credentials = { userId: "@user:id" }; - - const dlg = ReactDOM.render( - , parentDiv); - - // wait for a password box and a submit button - return MatrixReactTestUtils.waitForRenderedDOMComponentWithTag(dlg, "form", 2).then((formNode) => { - const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( - dlg, "input", - ); - let passwordNode; - let submitNode; - for (const node of inputNodes) { - if (node.type == 'password') { - passwordNode = node; - } else if (node.type == 'submit') { - submitNode = node; - } - } - expect(passwordNode).toBeTruthy(); - expect(submitNode).toBeTruthy(); - - // submit should be disabled - expect(submitNode.disabled).toBe(true); - - // put something in the password box, and hit enter; that should - // trigger a request - passwordNode.value = "s3kr3t"; - ReactTestUtils.Simulate.change(passwordNode); - expect(submitNode.disabled).toBe(false); - ReactTestUtils.Simulate.submit(formNode, {}); - - expect(doRequest).toHaveBeenCalledTimes(1); - expect(doRequest).toBeCalledWith(expect.objectContaining({ - session: "sess", - type: "m.login.password", - password: "s3kr3t", - identifier: { - type: "m.id.user", - user: "@user:id", - }, - })); - // let the request complete - return sleep(1); - }).then(sleep(1)).then(() => { - expect(onFinished).toBeCalledTimes(1); - expect(onFinished).toBeCalledWith(true, { a: 1 }); - }); - }); -}); diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.tsx b/test/components/views/dialogs/InteractiveAuthDialog-test.tsx new file mode 100644 index 0000000000..787dbd2cd5 --- /dev/null +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.tsx @@ -0,0 +1,106 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mount, ReactWrapper } from 'enzyme'; + +import InteractiveAuthDialog from "../../../../src/components/views/dialogs/InteractiveAuthDialog"; +import { flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from '../../../test-utils'; + +describe('InteractiveAuthDialog', function() { + const mockClient = getMockClientWithEventEmitter({ + generateClientSecret: jest.fn().mockReturnValue('t35tcl1Ent5ECr3T'), + }); + + const defaultProps = { + matrixClient: mockClient, + makeRequest: jest.fn().mockResolvedValue(undefined), + onFinished: jest.fn(), + }; + const getComponent = (props = {}) => mount(); + + beforeEach(function() { + jest.clearAllMocks(); + mockClient.credentials = null; + }); + + afterAll(() => { + unmockClientPeg(); + }); + + const getSubmitButton = (wrapper: ReactWrapper) => wrapper.find('[type="submit"]').at(0); + + it('Should successfully complete a password flow', async () => { + const onFinished = jest.fn(); + const makeRequest = jest.fn().mockResolvedValue({ a: 1 }); + + mockClient.credentials = { userId: "@user:id" }; + const authData = { + session: "sess", + flows: [ + { "stages": ["m.login.password"] }, + ], + }; + + const wrapper = getComponent({ makeRequest, onFinished, authData }); + + const passwordNode = wrapper.find('input[type="password"]').at(0); + const submitNode = getSubmitButton(wrapper); + + const formNode = wrapper.find('form').at(0); + expect(passwordNode).toBeTruthy(); + expect(submitNode).toBeTruthy(); + + // submit should be disabled + expect(submitNode.props().disabled).toBe(true); + + // put something in the password box + act(() => { + passwordNode.simulate('change', { target: { value: "s3kr3t" } }); + wrapper.setProps({}); + }); + + expect(wrapper.find('input[type="password"]').at(0).props().value).toEqual("s3kr3t"); + expect(getSubmitButton(wrapper).props().disabled).toBe(false); + + // hit enter; that should trigger a request + act(() => { + formNode.simulate('submit'); + }); + + // wait for auth request to resolve + await flushPromises(); + + expect(makeRequest).toHaveBeenCalledTimes(1); + expect(makeRequest).toBeCalledWith(expect.objectContaining({ + session: "sess", + type: "m.login.password", + password: "s3kr3t", + identifier: { + type: "m.id.user", + user: "@user:id", + }, + })); + + expect(onFinished).toBeCalledTimes(1); + expect(onFinished).toBeCalledWith(true, { a: 1 }); + }); +});