From 3880137ac60f1558a91938c1f16a3742ae1e09cf Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 17 Nov 2017 14:27:48 +0100 Subject: [PATCH 1/6] Create Authenticate component --- src/components/authenticate/authenticate.js | 66 ++++++++++++++ .../authenticate/authenticate.test.js | 90 +++++++++++++++++++ src/components/authenticate/index.js | 21 +++++ src/components/authenticate/index.test.js | 49 ++++++++++ 4 files changed, 226 insertions(+) create mode 100644 src/components/authenticate/authenticate.js create mode 100644 src/components/authenticate/authenticate.test.js create mode 100644 src/components/authenticate/index.js create mode 100644 src/components/authenticate/index.test.js diff --git a/src/components/authenticate/authenticate.js b/src/components/authenticate/authenticate.js new file mode 100644 index 000000000..a25a837e3 --- /dev/null +++ b/src/components/authenticate/authenticate.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { handleChange, authStatePrefill, authStateIsValid } from '../../utils/form'; +import ActionBar from '../actionBar'; +import AuthInputs from '../authInputs'; +import InfoParagraph from '../infoParagraph'; + +class Authenticate extends React.Component { + constructor() { + super(); + this.state = { + ...authStatePrefill(), + }; + this.message = ''; + } + + componentDidMount() { + const newState = { + ...authStatePrefill(this.props.account), + }; + this.setState(newState); + } + + componentWillUpdate(props) { + const { nextAction, t } = props; + this.message = `${t('You are looking into a saved account. In order to')} ${t(nextAction)} ${t('you need to enter your passphrase.')}`; + } + + update(e) { + e.preventDefault(); + const data = { + activePeer: this.props.peers.data, + passphrase: this.state.passphrase.value, + }; + if (typeof this.props.account.secondPublicKey === 'string') { + data.secondPassphrase = this.state.secondPassphrase.value; + } + this.props.accountUpdated(data); + } + + render() { + return ( +
+ + {this.message} + + + + + + ); + } +} + +export default Authenticate; diff --git a/src/components/authenticate/authenticate.test.js b/src/components/authenticate/authenticate.test.js new file mode 100644 index 000000000..ab9dc3693 --- /dev/null +++ b/src/components/authenticate/authenticate.test.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import configureStore from 'redux-mock-store'; +import PropTypes from 'prop-types'; +import { spy } from 'sinon'; +import ActionBar from '../actionBar'; +import i18n from '../../i18n'; +import Authenticate from './authenticate'; + + +const fakeStore = configureStore(); + +describe('Authenticate', () => { + let wrapper; + let props; + + const peers = { + status: { + online: false, + }, + data: { + currentPeer: 'localhost', + port: 4000, + options: { + name: 'Custom Node', + }, + }, + }; + + const account = { + isDelegate: false, + publicKey: 'c094ebee7ec0c50ebee32918655e089f6e1a604b83bcaa760293c61e0f18ab6f', + address: '16313739661670634666L', + }; + + const passphrase = 'wagon stock borrow episode laundry kitten salute link globe zero feed marble'; + + beforeEach(() => { + props = { + account, + peers, + t: str => str, + nextAction: 'perform a sample action', + closeDialog: spy(), + accountUpdated: spy(), + }; + + const store = fakeStore({ + account: { + balance: 100e8, + }, + }); + wrapper = mount(, { + context: { store, i18n }, + childContextTypes: { + store: PropTypes.object.isRequired, + i18n: PropTypes.object.isRequired, + }, + }); + }); + + it('renders 3 compound React components', () => { + expect(wrapper.find('InfoParagraph')).to.have.length(1); + expect(wrapper.find(ActionBar)).to.have.length(1); + expect(wrapper.find('AuthInputs')).to.have.length(1); + }); + + it('should render InfoParagraph with appropriate message', () => { + expect(wrapper.find('InfoParagraph').text()).to.include( + `You are looking into a saved account. In order to ${props.nextAction} you need to enter your passphrase`); + }); + + it('should activate primary button if correct passphrase entered', () => { + expect(wrapper.find('button.authenticate-button').props().disabled).to.equal(true); + wrapper.find('.passphrase input').simulate('change', { target: { value: passphrase } }); + expect(wrapper.find('button.authenticate-button').props().disabled).to.equal(false); + }); + + it('should call accountUpdated if entered passphrase and clicked submit', () => { + wrapper.find('.passphrase input').simulate('change', { target: { value: passphrase } }); + wrapper.update(); + wrapper.find('Button.authenticate-button').simulate('click'); + wrapper.update(); + expect(props.accountUpdated).to.have.been.calledWith({ + activePeer: props.peers.data, + passphrase, + }); + }); +}); diff --git a/src/components/authenticate/index.js b/src/components/authenticate/index.js new file mode 100644 index 000000000..9bcc9e2fe --- /dev/null +++ b/src/components/authenticate/index.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { accountUpdated } from '../../actions/account'; +import Authenticate from './authenticate'; + +/** + * Passing state + */ +const mapStateToProps = state => ({ + peers: state.peers, + account: state.account, +}); + +const mapDispatchToProps = dispatch => ({ + accountUpdated: data => dispatch(accountUpdated(data)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(translate()(Authenticate)); diff --git a/src/components/authenticate/index.test.js b/src/components/authenticate/index.test.js new file mode 100644 index 000000000..57823c593 --- /dev/null +++ b/src/components/authenticate/index.test.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import i18n from '../../i18n'; +import AuthenticateHOC from './index'; + +describe('AuthenticateHOC', () => { + let wrapper; + const peers = { + status: { + online: false, + }, + data: { + currentPeer: 'localhost', + port: 4000, + options: { + name: 'Custom Node', + }, + }, + }; + + const account = { + isDelegate: false, + address: '16313739661670634666L', + username: 'lisk-nano', + }; + + const store = configureMockStore([])({ + peers, + account, + }); + + beforeEach(() => { + wrapper = mount(); + }); + + it('should render Authenticate', () => { + expect(wrapper.find('Authenticate')).to.have.lengthOf(1); + }); + + it('should mount Authenticate with appropriate properties', () => { + const props = wrapper.find('Authenticate').props(); + expect(props.peers).to.be.equal(peers); + expect(props.account).to.be.equal(account); + expect(typeof props.accountUpdated).to.be.equal('function'); + }); +}); From faf58d50c6391233f2e20bddae1fafa1453ecd1e Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 17 Nov 2017 14:28:21 +0100 Subject: [PATCH 2/6] Use Authenticate in the first step of setSecondPassphrase --- src/components/secondPassphrase/index.js | 1 + .../secondPassphrase/secondPassphrase.js | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/secondPassphrase/index.js b/src/components/secondPassphrase/index.js index e879e2fa1..2eebabf2a 100644 --- a/src/components/secondPassphrase/index.js +++ b/src/components/secondPassphrase/index.js @@ -9,6 +9,7 @@ import SecondPassphrase from './secondPassphrase'; */ const mapStateToProps = state => ({ account: state.account, + passphrase: state.account.passphrase, peers: state.peers, }); diff --git a/src/components/secondPassphrase/secondPassphrase.js b/src/components/secondPassphrase/secondPassphrase.js index c2a7e9b8a..86c5e5948 100644 --- a/src/components/secondPassphrase/secondPassphrase.js +++ b/src/components/secondPassphrase/secondPassphrase.js @@ -1,9 +1,10 @@ import React from 'react'; import Passphrase from '../passphrase'; import Fees from '../../constants/fees'; +import Authenticate from '../authenticate'; const SecondPassphrase = ({ - account, peers, registerSecondPassphrase, closeDialog, t, + passphrase, account, peers, registerSecondPassphrase, closeDialog, t, }) => { const onLoginSubmission = (secondPassphrase) => { registerSecondPassphrase({ @@ -14,15 +15,17 @@ const SecondPassphrase = ({ }; return ( - - ); + typeof passphrase === 'string' && passphrase.length > 0 ? + + : + ); }; export default SecondPassphrase; From d6c5d440895aa6e9dd10d47aec107f71d686de08 Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 17 Nov 2017 14:28:50 +0100 Subject: [PATCH 3/6] Add e2e test for Authenticate --- test/e2e/registerSecondPassphrase.feature | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/e2e/registerSecondPassphrase.feature b/test/e2e/registerSecondPassphrase.feature index 66826c240..07b4a7580 100644 --- a/test/e2e/registerSecondPassphrase.feature +++ b/test/e2e/registerSecondPassphrase.feature @@ -12,6 +12,19 @@ Feature: Register second passphrase Given I'm logged in as "second passphrase account" Then There is no "register second passphrase" in main menu + Scenario: should ask for passphrase for saved account + Given I'm logged in as "empty account" + When I click "saved accounts" in main menu + And I click "add active account button" + And I click "x button" + And I wait 1 seconds + And I refresh the page + When I click "register second passphrase" in main menu + And I fill in passphrase of "empty account" to "passphrase" field + And I click "authenticate button" + Then I should see "Insufficient funds for 5 LSK fee" error message + And "next button" should be disabled + @integration Scenario: should not allow to set 2nd passphrase if not enough funds for the fee Given I'm logged in as "empty account" From 221978fecaf4d2232d1a17bf3a68201ee460a4e3 Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 17 Nov 2017 14:29:27 +0100 Subject: [PATCH 4/6] Fixed a bug in relativeLink --- src/components/relativeLink/index.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/relativeLink/index.js b/src/components/relativeLink/index.js index ed06e619a..69c0e751b 100644 --- a/src/components/relativeLink/index.js +++ b/src/components/relativeLink/index.js @@ -1,8 +1,10 @@ import React from 'react'; +import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { withRouter } from 'react-router'; import buttonStyle from 'react-toolbox/lib/button/theme.css'; import offlineStyle from '../offlineWrapper/offlineWrapper.css'; +import dialogs from '../dialog/dialogs'; const RelativeLink = ({ location, to, children, className, raised, neutral, primary, flat, disableWhenOffline, @@ -15,10 +17,20 @@ const RelativeLink = ({ if (disableWhenOffline !== undefined) style += `${offlineStyle.disableWhenOffline} `; if (style !== '') style += ` ${buttonStyle.button}`; - const path = location.pathname.indexOf(`/${to}`) < 0 ? `${location.pathname}/${to}`.replace('//', '/') : location.pathname; + const dialogNames = Object.keys(dialogs()); + let pathname = location.pathname; + dialogNames.forEach((dialog) => { + pathname = pathname.replace(`/${dialog}`, ''); + }); + + const path = `${pathname}/${to}`.replace('//', '/'); return ( { children } ); }; -export default withRouter(RelativeLink); +const mapStateToProps = state => ({ + dialogTitle: state.dialog.title, +}); + +export default withRouter(connect(mapStateToProps)(RelativeLink)); From d37edf5b40283e2db8a545b13896ca755710418d Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 17 Nov 2017 15:05:10 +0100 Subject: [PATCH 5/6] Use dialog instead of dialog.title, since title might not be available --- src/components/relativeLink/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/relativeLink/index.js b/src/components/relativeLink/index.js index 69c0e751b..046eea8d6 100644 --- a/src/components/relativeLink/index.js +++ b/src/components/relativeLink/index.js @@ -30,7 +30,7 @@ const RelativeLink = ({ }; const mapStateToProps = state => ({ - dialogTitle: state.dialog.title, + dialog: state.dialog, }); export default withRouter(connect(mapStateToProps)(RelativeLink)); From 9abf0882ae9f5a75d8d66036e6a636c4af666448 Mon Sep 17 00:00:00 2001 From: reyraa Date: Fri, 17 Nov 2017 15:06:31 +0100 Subject: [PATCH 6/6] Fixed a wrong usage of arrow function --- src/components/authenticate/authenticate.js | 2 +- .../registerDelegate/registerDelegate.js | 4 +- .../secondPassphrase/secondPassphrase.test.js | 73 ++++++++++++------- src/utils/form.js | 4 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/components/authenticate/authenticate.js b/src/components/authenticate/authenticate.js index a25a837e3..9e596ef35 100644 --- a/src/components/authenticate/authenticate.js +++ b/src/components/authenticate/authenticate.js @@ -47,7 +47,7 @@ class Authenticate extends React.Component { + onChange={handleChange.bind(this)} /> + onChange={handleChange.bind(this)} />
{this.props.t('Becoming a delegate requires registration. You may choose your own delegate name, which can be used to promote your delegate. Only the top 101 delegates are eligible to forge. All fees are shared equally between the top 101 delegates.')} diff --git a/src/components/secondPassphrase/secondPassphrase.test.js b/src/components/secondPassphrase/secondPassphrase.test.js index ab3631f8e..62f17c180 100644 --- a/src/components/secondPassphrase/secondPassphrase.test.js +++ b/src/components/secondPassphrase/secondPassphrase.test.js @@ -24,38 +24,57 @@ describe('SecondPassphrase', () => { i18n: PropTypes.object.isRequired, }, }; - const prop = { - account, - peers, - registerSecondPassphrase: spy(), - t: key => key, - }; + const passphrase = 'wagon stock borrow episode laundry kitten salute link globe zero feed marble'; - beforeEach(() => { - wrapper = mount(, options); - }); + describe('Authenticated', () => { + const prop = { + account, + passphrase, + peers, + registerSecondPassphrase: spy(), + t: key => key, + }; - it('renders Passphrase component', () => { - expect(wrapper.find('Passphrase')).to.have.length(1); - }); + beforeEach(() => { + wrapper = mount(, options); + }); - it('should mount SecondPassphrase with appropriate properties', () => { - const props = wrapper.find('Passphrase').props(); - expect(props.securityNote).to.be.equal('Losing access to this passphrase will mean no funds can be sent from this account.'); - expect(props.useCaseNote).to.be.equal('your second passphrase will be required for all transactions sent from this account'); - expect(props.confirmButton).to.be.equal('Register'); - expect(props.fee).to.be.equal(Fees.setSecondPassphrase); - expect(props.keepModal).to.be.equal(true); - expect(typeof props.onPassGenerated).to.be.equal('function'); + it('renders Passphrase component', () => { + expect(wrapper.find('Passphrase')).to.have.length(1); + }); + + it('should mount SecondPassphrase with appropriate properties', () => { + const props = wrapper.find('Passphrase').props(); + expect(props.securityNote).to.be.equal('Losing access to this passphrase will mean no funds can be sent from this account.'); + expect(props.useCaseNote).to.be.equal('your second passphrase will be required for all transactions sent from this account'); + expect(props.confirmButton).to.be.equal('Register'); + expect(props.fee).to.be.equal(Fees.setSecondPassphrase); + expect(props.keepModal).to.be.equal(true); + expect(typeof props.onPassGenerated).to.be.equal('function'); + }); + + it('should call registerSecondPassphrase if props.onPassGenerated is called', () => { + const props = wrapper.find('Passphrase').props(); + props.onPassGenerated('sample passphrase'); + expect(prop.registerSecondPassphrase).to.have.been.calledWith({ + activePeer: peers.data, + secondPassphrase: 'sample passphrase', + account, + }); + }); }); - it('should call registerSecondPassphrase if props.onPassGenerated is called', () => { - const props = wrapper.find('Passphrase').props(); - props.onPassGenerated('sample passphrase'); - expect(prop.registerSecondPassphrase).to.have.been.calledWith({ - activePeer: peers.data, - secondPassphrase: 'sample passphrase', - account, + describe('Not authenticated', () => { + it('Should mount an Authenticate component is no passphrase provided', () => { + const prop = { + account, + peers, + registerSecondPassphrase: spy(), + t: key => key, + }; + + wrapper = mount(, options); + expect(wrapper.find('Authenticate')).to.have.length(1); }); }); }); diff --git a/src/utils/form.js b/src/utils/form.js index 5403939a6..4a10c9710 100644 --- a/src/utils/form.js +++ b/src/utils/form.js @@ -15,8 +15,8 @@ export const authStateIsValid = state => ( state.secondPassphrase.value !== '' ); -export const handleChange = (component, name, value, error) => { - component.setState({ +export const handleChange = function (name, value, error) { + this.setState({ [name]: { value, error: typeof error === 'string' ? error : undefined,