From 1ac5a7568fb8b2871ecbdad22cc524dec48569fe Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Fri, 13 Apr 2018 19:15:30 +0200 Subject: [PATCH] Account creation screen and tests, fixes #13 --- src/etheroll.kv | 34 ++++++++- src/etheroll.py | 126 +++++++++++++++++++++++++++++++--- src/tests/ui/__init__.py | 0 src/tests/ui/test_etheroll.py | 107 ++++++++++++++++++++++++++--- 4 files changed, 247 insertions(+), 20 deletions(-) create mode 100644 src/tests/ui/__init__.py diff --git a/src/etheroll.kv b/src/etheroll.kv index 10da833..dc666c7 100644 --- a/src/etheroll.kv +++ b/src/etheroll.kv @@ -44,6 +44,35 @@ PushUp: +: + orientation: 'vertical' + MDTextField: + id: new_password1_id + hint_text: "Password" + helper_text: "Enter the password for encrypting your new account" + helper_text_mode: "on_focus" + password: True + write_tab: False + text: root.new_password1 + on_text: root.new_password1 = args[1] + MDTextField: + id: new_password2_id + # TODO: do validation as we type + hint_text: "Password (again)" + helper_text: "Retype your password" + helper_text_mode: "on_focus" + password: True + write_tab: False + text: root.new_password2 + on_text: root.new_password2 = args[1] + AnchorLayout: + MDRaisedButton: + id: create_account_button_id + text: "Create account" + on_release: root.create_account() + PushUp: + + : name: 'switch_account' on_pre_enter: @@ -59,11 +88,12 @@ SwitchAccount: id: switch_account_id MDBottomNavigationItem: + id: create_new_account_nav_item_id name: "create" text: "Create" icon: "plus" - MDLabel: - text: "Not implemented" + CreateNewAccount: + id: create_new_account_id MDBottomNavigationItem: name: "import" text: "Import" diff --git a/src/etheroll.py b/src/etheroll.py index 23d8110..6549511 100755 --- a/src/etheroll.py +++ b/src/etheroll.py @@ -17,6 +17,7 @@ from raven.conf import setup_logging from raven.handlers.logging import SentryHandler +from ethereum_utils import AccountUtils from utils import Dialog, patch_find_library_android, patch_typing_python351 from version import __version__ @@ -70,7 +71,7 @@ def load_account_list(self): self.controller = App.get_running_app().root account_list_id = self.ids.account_list_id account_list_id.clear_widgets() - accounts = self.controller.pyethapp.services.accounts + accounts = self.controller.account_utils.get_account_list() if len(accounts) == 0: self.on_empty_account_list() for account in accounts: @@ -80,13 +81,123 @@ def load_account_list(self): @staticmethod def on_empty_account_list(): controller = App.get_running_app().root - keystore_dir = controller.pyethapp.services.accounts.keystore_dir + keystore_dir = controller.account_utils.keystore_dir title = "No account found" body = "No account found in:\n%s" % keystore_dir dialog = Dialog.create_dialog(title, body) dialog.open() +class CreateNewAccount(BoxLayout): + """ + Makes it possible to create json keyfiles. + """ + + new_password1 = StringProperty() + new_password2 = StringProperty() + + def __init__(self, **kwargs): + super(CreateNewAccount, self).__init__(**kwargs) + Clock.schedule_once(lambda dt: self.setup()) + + def setup(self): + """ + Sets security vs speed default values. + Plus hides the advanced widgets. + """ + self.controller = App.get_running_app().root + + def verify_password_field(self): + """ + Makes sure passwords are matching and are not void. + """ + passwords_matching = self.new_password1 == self.new_password2 + passwords_not_void = self.new_password1 != '' + return passwords_matching and passwords_not_void + + def verify_fields(self): + """ + Verifies password fields are valid. + """ + return self.verify_password_field() + + @staticmethod + def try_unlock(account, password): + """ + Just as a security measure, verifies we can unlock + the newly created account with provided password. + """ + # making sure it's locked first + account.lock() + try: + account.unlock(password) + except ValueError: + title = "Unlock error" + body = "" + body += "Couldn't unlock your account.\n" + body += "The issue should be reported." + dialog = Dialog.create_dialog(title, body) + dialog.open() + return + + # @mainthread + def on_account_created(self, account): + """ + Switches to the newly created account. + Clears the form. + """ + self.controller.switch_account_screen.current_account = account + self.new_password1 = '' + self.new_password2 = '' + + # @mainthread + def toggle_widgets(self, enabled): + """ + Enables/disables account creation widgets. + """ + self.disabled = not enabled + + def show_redirect_dialog(self): + title = "Account created, redirecting..." + body = "" + body += "Your account was created, " + body += "you will be redirected to the overview." + dialog = Dialog.create_dialog(title, body) + dialog.open() + + def load_landing_page(self): + """ + Returns to the landing page. + """ + screen_manager = self.controller.screen_manager + screen_manager.transition.direction = 'right' + screen_manager.current = 'roll_screen' + + # @run_in_thread + def create_account(self): + """ + Creates an account from provided form. + Verify we can unlock it. + Disables widgets during the process, so the user doesn't try + to create another account during the process. + """ + self.toggle_widgets(False) + if not self.verify_fields(): + Dialog.show_invalid_form_dialog() + self.toggle_widgets(True) + return + password = self.new_password1 + Dialog.snackbar_message("Creating account...") + account = self.controller.account_utils.new_account(password=password) + Dialog.snackbar_message("Created!") + self.toggle_widgets(True) + self.on_account_created(account) + CreateNewAccount.try_unlock(account, password) + self.show_redirect_dialog() + self.load_landing_page() + return account + + class CustomToolbar(Toolbar): """ Toolbar with helper method for loading default/back buttons. @@ -334,12 +445,7 @@ def _after_init(self, dt): def _init_pyethapp(self, keystore_dir=None): if keystore_dir is None: keystore_dir = self.get_keystore_path() - # must be imported after `patch_find_library_android()` - from devp2p.app import BaseApp - from pyethapp.accounts import AccountsService - self.pyethapp = BaseApp( - config=dict(accounts=dict(keystore_dir=keystore_dir))) - AccountsService.register_with_app(self.pyethapp) + self.account_utils = AccountUtils(keystore_dir=keystore_dir) @property def pyetheroll(self): @@ -440,6 +546,10 @@ def update_profit_property(self): def navigation(self): return self.ids.navigation_id + @property + def screen_manager(self): + return self.ids.screen_manager_id + @property def roll_screen(self): return self.ids.roll_screen_id diff --git a/src/tests/ui/__init__.py b/src/tests/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/ui/test_etheroll.py b/src/tests/ui/test_etheroll.py index 59e3c76..6059a59 100644 --- a/src/tests/ui/test_etheroll.py +++ b/src/tests/ui/test_etheroll.py @@ -1,5 +1,6 @@ import os import shutil +import threading import time import unittest from functools import partial @@ -38,6 +39,18 @@ def advance_frames(self, count): for i in range(count): EventLoop.idle() + def advance_frames_for_screen(self): + """ + Gives screen switch animation time to render. + """ + self.advance_frames(50) + + def advance_frames_for_drawer(self): + """ + Gives drawer switch animation time to render. + """ + self.advance_frames_for_screen() + def helper_setup(self, app): etheroll.SCREEN_SWITCH_DELAY = 0.001 @@ -81,8 +94,7 @@ def helper_test_toolbar(self, app): self.assertEqual(len(left_actions.children), 1) # clicking the menu action item should load the navigation drawer left_actions.children[0].dispatch('on_release') - # the drawer animation takes time - self.advance_frames(50) + self.advance_frames_for_drawer() # checking the drawer items navigation = controller.ids.navigation_id navigation_drawer = navigation.ids.navigation_drawer_id @@ -93,8 +105,7 @@ def helper_test_toolbar(self, app): # clicking the about one about_item = navigation_drawer_items[0] about_item.dispatch('on_release') - # the drawer animation takes time - self.advance_frames(50) + self.advance_frames_for_drawer() self.assertEqual( controller.ids.screen_manager_id.current, 'about_screen') # toolbar should now be loaded with the back button @@ -105,8 +116,7 @@ def helper_test_toolbar(self, app): # going back to main screen left_actions = toolbar.ids.left_actions left_actions.children[0].dispatch('on_release') - # loading screen takes time - self.advance_frames(50) + self.advance_frames_for_screen() self.assertEqual( controller.ids.screen_manager_id.current, 'roll_screen') @@ -121,8 +131,7 @@ def helper_test_about_screen(self, app): self.assertEqual(screen.name, 'roll_screen') # loads the about and verify screen_manager.current = 'about_screen' - # loading screen takes time - self.advance_frames(50) + self.advance_frames_for_screen() screen = screen_manager.children[0] self.assertEqual(screen.name, 'about_screen') # checks about screen content @@ -132,8 +141,85 @@ def helper_test_about_screen(self, app): 'https://github.com/AndreMiras/EtherollApp' in about_content) # loads back the default screen screen_manager.current = 'roll_screen' - # loading screen takes time - self.advance_frames(50) + self.advance_frames_for_screen() + + def helper_test_create_first_account(self, app): + """ + Creates the first account. + """ + controller = app.root + screen_manager = controller.ids.screen_manager_id + account_utils = controller.account_utils + # makes sure no account are loaded + self.assertEqual(len(account_utils.get_account_list()), 0) + # loads the switch account screen + switch_account_screen = controller.switch_account_screen + screen_manager.current = switch_account_screen.name + self.advance_frames_for_screen() + # it should open the warning dialog + dialogs = Dialog.dialogs + self.assertEqual(len(dialogs), 1) + dialog = dialogs[0] + self.assertEqual(dialog.title, 'No account found') + dialog.dismiss() + self.assertEqual(len(dialogs), 0) + # loads the create new account tab + create_new_account_nav_item = \ + switch_account_screen.ids.create_new_account_nav_item_id + create_new_account_nav_item.dispatch('on_tab_press') + # verifies no current account is setup + self.assertEqual(switch_account_screen.current_account, None) + # retrieves the create_new_account widget + create_new_account = switch_account_screen.ids.create_new_account_id + # retrieves widgets (password fields, sliders and buttons) + new_password1_id = create_new_account.ids.new_password1_id + new_password2_id = create_new_account.ids.new_password2_id + create_account_button_id = \ + create_new_account.ids.create_account_button_id + # fills them up with same password + new_password1_id.text = new_password2_id.text = "password" + # before clicking the create account button, + # only the main thread is running + self.assertEqual(len(threading.enumerate()), 1) + main_thread = threading.enumerate()[0] + self.assertEqual(type(main_thread), threading._MainThread) + # click the create account button + create_account_button_id.dispatch('on_release') + # currently account creation do not run in a thread, so only 1 thread + self.assertEqual(len(threading.enumerate()), 1) + self.assertEqual(type(main_thread), threading._MainThread) + """ + create_account_thread = threading.enumerate()[0] + self.assertEqual( + create_account_thread._Thread__target.func_name, + "create_account") + # waits for the end of the thread + create_account_thread.join() + # thread has ended and the main thread is running alone again + self.assertEqual(len(threading.enumerate()), 1) + main_thread = threading.enumerate()[0] + self.assertEqual(type(main_thread), threading._MainThread) + """ + # and verifies the account was created + self.assertEqual(len(account_utils.get_account_list()), 1) + self.assertEqual(new_password1_id.text, '') + self.assertEqual(new_password2_id.text, '') + # we should get redirected to the overview page + self.advance_frames_for_screen() + self.assertEqual(controller.screen_manager.current, 'roll_screen') + # the new account should be loaded in the controller + self.assertEqual( + controller.switch_account_screen.current_account, + account_utils.get_account_list()[0]) + # joins ongoing threads + [t.join() for t in threading.enumerate()[1:]] + # check the redirect dialog + dialogs = Dialog.dialogs + self.assertEqual(len(dialogs), 1) + dialog = dialogs[0] + self.assertEqual(dialog.title, 'Account created, redirecting...') + dialog.dismiss() + self.assertEqual(len(dialogs), 0) # main test function def run_test(self, app, *args): @@ -144,6 +230,7 @@ def run_test(self, app, *args): self.helper_test_empty_account(app) self.helper_test_toolbar(app) self.helper_test_about_screen(app) + self.helper_test_create_first_account(app) # Comment out if you are editing the test, it'll leave the # Window opened. app.stop()