diff options
-rw-r--r-- | Makefile | 6 | ||||
-rw-r--r-- | service/diagrams/backup_account.png | bin | 0 -> 32685 bytes | |||
-rw-r--r-- | service/diagrams/backup_account.txt | 37 | ||||
-rw-r--r-- | service/diagrams/forgot_password.png | bin | 0 -> 36634 bytes | |||
-rw-r--r-- | service/diagrams/forgot_password.txt | 35 | ||||
-rw-r--r-- | service/test/functional/features/environment.py | 2 | ||||
-rw-r--r-- | service/test/functional/features/smoke.feature | 9 | ||||
-rw-r--r-- | service/test/functional/features/steps/backup_account.py | 22 | ||||
-rw-r--r-- | service/test/functional/features/steps/login.py | 5 | ||||
-rw-r--r-- | web-ui/src/common/flat_button/flat_button.js | 58 | ||||
-rw-r--r-- | web-ui/src/common/flat_button/flat_button.spec.js | 32 | ||||
-rw-r--r-- | web-ui/src/common/header/header.js | 15 | ||||
-rw-r--r-- | web-ui/src/common/header/header.scss | 5 | ||||
-rw-r--r-- | web-ui/src/common/header/header.spec.js | 12 | ||||
-rw-r--r-- | web-ui/src/common/logout/logout.js | 48 | ||||
-rw-r--r-- | web-ui/src/common/logout/logout.spec.js | 49 | ||||
-rw-r--r-- | web-ui/test/integration/backup_account.spec.js | 6 |
17 files changed, 321 insertions, 20 deletions
@@ -114,3 +114,9 @@ remove_virtualenv: remove_javascript_packages: rm -rf web-ui/node_modules rm -rf web-ui/app/bower_components + +diagrams: + @. $(VIRTUALENV)/bin/activate;\ + pip install plantuml;\ + cd service/diagrams/png;\ + python -m plantuml *.txt diff --git a/service/diagrams/backup_account.png b/service/diagrams/backup_account.png Binary files differnew file mode 100644 index 00000000..e3e6871b --- /dev/null +++ b/service/diagrams/backup_account.png diff --git a/service/diagrams/backup_account.txt b/service/diagrams/backup_account.txt new file mode 100644 index 00000000..661126f2 --- /dev/null +++ b/service/diagrams/backup_account.txt @@ -0,0 +1,37 @@ +title Backup Account Flow + +actor User + +User -> PixUA : backup email +note left + existing user adds backup email +end note + +PixUA -> SoledadClient +note right + generate recovery code + encrypt secret + save to secrets doc +end note + +SoledadClient -> PixUA: recovery code + +PixUA -> BonafideClient : recovery code +note right + generate salt and verifier +end note + +BonafideClient -> LeapWebapp : recovery code + +LeapWebapp -> BonafideClient +BonafideClient -> PixUA + +alt successful case + PixUA -> SoledadClient + note right: delete old recovery code + PixUA -> User : email with recovery code +else bonafide failure + PixUA -> SoledadClient + note right: delete new recovery code + PixUA -> User : error message +end diff --git a/service/diagrams/forgot_password.png b/service/diagrams/forgot_password.png Binary files differnew file mode 100644 index 00000000..37dea8fa --- /dev/null +++ b/service/diagrams/forgot_password.png diff --git a/service/diagrams/forgot_password.txt b/service/diagrams/forgot_password.txt new file mode 100644 index 00000000..2a303ddc --- /dev/null +++ b/service/diagrams/forgot_password.txt @@ -0,0 +1,35 @@ +title Forgot Password Flow + +actor User + +User -> PixUA : recovery code and new password + +PixUA -> BonafideClient : //handshake// +PixUA -> BonafideClient : recovery code and new password +note right + generate salt and verifier +end note + +BonafideClient -> LeapWebapp : recovery code and new password +note right + authenticate + save new password +end note + +LeapWebapp -> BonafideClient +BonafideClient -> PixUA + +alt successful case + PixUA -> SoledadClient : new password + note right : save secret with new password + SoledadClient -> PixUA + + alt successful case + PixUA -> User : confirmation page + note left: start backup account flow + else soledad failure + PixUA -> User : error message + end +else bonafide failure + PixUA -> User : error message +end diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py index 821a762b..9f8507b2 100644 --- a/service/test/functional/features/environment.py +++ b/service/test/functional/features/environment.py @@ -54,11 +54,13 @@ def before_all(context): hostname = urlparse(context.host).hostname context.signup_url = 'https://{}/signup'.format(hostname) context.login_url = 'https://mail.{}/login'.format(hostname) + context.backup_account_url = 'https://mail.{}/backup-account'.format(hostname) context.username = 'testuser_{}'.format(uuid.uuid4()) if 'localhost' in context.host: _mock_user_agent(context) context.login_url = context.multi_user_url + '/login' + context.backup_account_url = context.single_user_url + '/backup-account' context.username = 'username' diff --git a/service/test/functional/features/smoke.feature b/service/test/functional/features/smoke.feature index 724c680c..1467baf9 100644 --- a/service/test/functional/features/smoke.feature +++ b/service/test/functional/features/smoke.feature @@ -34,3 +34,12 @@ Feature: sign up, login and logout Then I have mails When I logout Then I should see the login page + + Scenario: Existing user logs in and logs out from the header + Given a user is accessing the login page + When I enter username and password as credentials + And I click on the login button + Then I should see the fancy interstitial + Given I am on the backup account page + When I logout from the header + Then I should see the login page diff --git a/service/test/functional/features/steps/backup_account.py b/service/test/functional/features/steps/backup_account.py new file mode 100644 index 00000000..914309f2 --- /dev/null +++ b/service/test/functional/features/steps/backup_account.py @@ -0,0 +1,22 @@ +# +# Copyright (c) 2017 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + +from behave import given + + +@given(u'I am on the backup account page') +def backup_account_page(context): + context.browser.get(context.backup_account_url) diff --git a/service/test/functional/features/steps/login.py b/service/test/functional/features/steps/login.py index 9ce37370..2d7be259 100644 --- a/service/test/functional/features/steps/login.py +++ b/service/test/functional/features/steps/login.py @@ -48,6 +48,11 @@ def click_logout(context): find_element_by_css_selector(context, '#logout-form div').click() +@when(u'I logout from the header') +def click_logout(context): + find_element_by_css_selector(context, 'button[name="logout"]').click() + + @then(u'I should see the login page') def see_login_page(context): find_element_by_css_selector(context, 'form#login_form') diff --git a/web-ui/src/common/flat_button/flat_button.js b/web-ui/src/common/flat_button/flat_button.js new file mode 100644 index 00000000..b8c6c2c7 --- /dev/null +++ b/web-ui/src/common/flat_button/flat_button.js @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +import React from 'react'; +import FlatButton from 'material-ui/FlatButton'; +import FontIcon from 'material-ui/FontIcon'; +import { grey500 } from 'material-ui/styles/colors'; + +const labelStyle = { + textTransform: 'none', + verticalAlign: 'middle' +}; + +const iconStyle = { + marginRight: 0 +}; + +const flatButtonStyle = { + minWidth: 0, + verticalAlign: 'top' +}; + +const SubmitFlatButton = ({ name, buttonText, fontIconClass }) => ( + <FlatButton + name={name} + type='submit' + hoverColor='transparent' + style={flatButtonStyle} + labelPosition='before' + label={buttonText} + labelStyle={labelStyle} + aria-label={buttonText} + title={buttonText} + icon={<FontIcon className={fontIconClass} color={grey500} style={iconStyle} />} + /> +); + +SubmitFlatButton.propTypes = { + name: React.PropTypes.string.isRequired, + buttonText: React.PropTypes.string.isRequired, + fontIconClass: React.PropTypes.string.isRequired +}; + +export default SubmitFlatButton; diff --git a/web-ui/src/common/flat_button/flat_button.spec.js b/web-ui/src/common/flat_button/flat_button.spec.js new file mode 100644 index 00000000..16f03acb --- /dev/null +++ b/web-ui/src/common/flat_button/flat_button.spec.js @@ -0,0 +1,32 @@ +import { shallow } from 'enzyme'; +import expect from 'expect'; +import React from 'react'; +import FlatButton from 'src/common/flat_button/flat_button'; + +describe('FlatButton', () => { + let flatButton; + + beforeEach(() => { + flatButton = shallow(<FlatButton name='logout' buttonText='Logout' fontIconClass='fa fa-sign-out' />); + }); + + it('renders a FlatButton of type submit with name logout', () => { + expect(flatButton.find('FlatButton').props().name).toEqual('logout'); + }); + + it('renders a FlatButton of type submit with text logout', () => { + expect(flatButton.find('FlatButton').props().label).toEqual('Logout'); + }); + + it('renders a FlatButton of type submit with title logout', () => { + expect(flatButton.find('FlatButton').props().title).toEqual('Logout'); + }); + + it('renders a FlatButton of type submit with aria-label logout', () => { + expect(flatButton.find('FlatButton').props()['aria-label']).toEqual('Logout'); + }); + + it('renders a FlatButton with given fontIcon class', () => { + expect(flatButton.find('FlatButton').props().icon.props.className).toEqual('fa fa-sign-out'); + }); +}); diff --git a/web-ui/src/common/header/header.js b/web-ui/src/common/header/header.js index 50c863b5..715d54c6 100644 --- a/web-ui/src/common/header/header.js +++ b/web-ui/src/common/header/header.js @@ -16,10 +16,10 @@ */ import React from 'react'; -import { translate } from 'react-i18next'; +import Logout from 'src/common/logout/logout'; import './header.scss'; -export const Header = ({ t }) => ( +export const Header = () => ( <header className='header-wrapper'> <div className='header-content'> <a href='/'> @@ -30,17 +30,10 @@ export const Header = ({ t }) => ( /> </a> <div className='header-icons'> - <a href='/'> - <span>{t('logout')}</span> - <i className='fa fa-sign-out' aria-hidden='true' /> - </a> + <Logout /> </div> </div> </header> ); -Header.propTypes = { - t: React.PropTypes.func.isRequired -}; - -export default translate('', { wait: true })(Header); +export default Header; diff --git a/web-ui/src/common/header/header.scss b/web-ui/src/common/header/header.scss index d56629bf..3e10ba39 100644 --- a/web-ui/src/common/header/header.scss +++ b/web-ui/src/common/header/header.scss @@ -28,7 +28,6 @@ .header-content { display: flex; - align-items: center; } .header-logo { @@ -41,7 +40,7 @@ position: absolute; right: 6%; - top: 13px; + line-height: 0; span { display: none; @@ -51,6 +50,7 @@ font-size: 1.3em; margin-left: 0.4em; color: $medium_light_grey; + overflow: visible; } } @@ -78,6 +78,7 @@ font-style: normal; font-size: 0.7em; padding-bottom: 0.1em; + line-height: 36px; } .fa { diff --git a/web-ui/src/common/header/header.spec.js b/web-ui/src/common/header/header.spec.js index 82e29e1c..81a952c7 100644 --- a/web-ui/src/common/header/header.spec.js +++ b/web-ui/src/common/header/header.spec.js @@ -2,16 +2,20 @@ import { shallow } from 'enzyme'; import expect from 'expect'; import React from 'react'; import { Header } from 'src/common/header/header'; +import Logout from 'src/common/logout/logout'; describe('Header', () => { let header; beforeEach(() => { - const mockTranslations = key => key; - header = shallow(<Header t={mockTranslations} />); + header = shallow(<Header />); }); - it('renders the header content', () => { - expect(header.find('header').text()).toContain('logout'); + it('renders the header containing the logout button', () => { + expect(header.find('header').find(Logout)).toExist(); + }); + + it('renders the header pixelated logo', () => { + expect(header.find('header').find('img').props().alt).toEqual('Pixelated'); }); }); diff --git a/web-ui/src/common/logout/logout.js b/web-ui/src/common/logout/logout.js new file mode 100644 index 00000000..259b448e --- /dev/null +++ b/web-ui/src/common/logout/logout.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +import React from 'react'; +import { translate } from 'react-i18next'; +import browser from 'helpers/browser'; + +import SubmitFlatButton from 'src/common/flat_button/flat_button'; + +export class Logout extends React.Component { + + constructor(props) { + super(props); + this.state = { csrf_token: browser.getCookie('XSRF-TOKEN') }; + } + + render() { + const t = this.props.t; + return ( + <div className='logout-container'> + <form id='logout-form' method='POST' action='logout'> + <input type='hidden' name='csrftoken' value={this.state.csrf_token} /> + <SubmitFlatButton name='logout' buttonText={t('logout')} fontIconClass='fa fa-sign-out' /> + </form> + </div> + ); + } +} + +Logout.propTypes = { + t: React.PropTypes.func.isRequired +}; + +export default translate('', { wait: true })(Logout); diff --git a/web-ui/src/common/logout/logout.spec.js b/web-ui/src/common/logout/logout.spec.js new file mode 100644 index 00000000..ba228e61 --- /dev/null +++ b/web-ui/src/common/logout/logout.spec.js @@ -0,0 +1,49 @@ +import { shallow } from 'enzyme'; +import expect from 'expect'; +import React from 'react'; +import { Logout } from 'src/common/logout/logout'; + +describe('Logout', () => { + let logout; + + beforeEach(() => { + const mockTranslations = key => key; + logout = shallow(<Logout t={mockTranslations} fontIconClass='fa fa-sign-out' />); + }); + + it('renders the logout container', () => { + expect(logout.find('div.logout-container')).toExist(); + }); + + describe('logout form', () => { + let logoutForm; + + beforeEach(() => { + logoutForm = logout.find('form#logout-form'); + }); + + it('renders logout form', () => { + expect(logoutForm).toExist(); + }); + + it('renders logout form with POST method', () => { + expect(logoutForm.props().method).toEqual('POST'); + }); + + it('renders logout form with action as logout', () => { + expect(logoutForm.props().action).toEqual('logout'); + }); + + it('renders csrf hidden input', () => { + expect(logoutForm.find('input[name="csrftoken"]')).toExist(); + }); + + it('renders SubmitFlatButton for logout', () => { + expect(logoutForm.find('SubmitFlatButton').props().buttonText).toEqual('logout'); + }); + + it('renders SubmitFlatButton for logout with fontIcon', () => { + expect(logoutForm.find('SubmitFlatButton').props().fontIconClass).toEqual('fa fa-sign-out'); + }); + }); +}); diff --git a/web-ui/test/integration/backup_account.spec.js b/web-ui/test/integration/backup_account.spec.js index b44c3b2c..b9667a41 100644 --- a/web-ui/test/integration/backup_account.spec.js +++ b/web-ui/test/integration/backup_account.spec.js @@ -16,7 +16,7 @@ describe('Backup account email validation', () => { context('with valid email', () => { beforeEach(() => { - backupAccountPage.find('input').simulate('change', {target: {value: 'test@test.com'}}); + backupAccountPage.find('input[name="email"]').simulate('change', {target: {value: 'test@test.com'}}); }); it('shows no validation error', () => { @@ -30,7 +30,7 @@ describe('Backup account email validation', () => { context('with invalid email', () => { beforeEach(() => { - backupAccountPage.find('input').simulate('change', {target: {value: 'test'}}); + backupAccountPage.find('input[name="email"]').simulate('change', {target: {value: 'test'}}); }); it('shows validation error', () => { @@ -44,7 +44,7 @@ describe('Backup account email validation', () => { context('with empty email', () => { beforeEach(() => { - backupAccountPage.find('input').simulate('change', {target: {value: ''}}); + backupAccountPage.find('input[name="email"]').simulate('change', {target: {value: ''}}); }); it('shows no validation error', () => { |