mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 23:18:25 +00:00
New BaseTest structure; new FlowStep sequence testing
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ class SettingsEntry:
|
||||
visibility: str = SettingsConstants.VISIBILITY__GENERAL
|
||||
type: str = SettingsConstants.TYPE__ENABLED_DISABLED
|
||||
help_text: str = None
|
||||
selection_options: List[str] = None
|
||||
selection_options: List[Tuple[Union[str,int],str]] = None
|
||||
default_value: Any = None
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
@@ -6,14 +6,14 @@ from binascii import hexlify
|
||||
from embit import bip39
|
||||
from embit.descriptor import Descriptor
|
||||
from embit.networks import NETWORKS
|
||||
from typing import List, Type
|
||||
from typing import List
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
||||
WarningScreen, DireWarningScreen, seed_screens)
|
||||
from seedsigner.gui.screens.screen import BaseScreen, LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
@@ -120,22 +120,16 @@ class SeedMnemonicEntryView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
ret = seed_screens.SeedMnemonicEntryScreen(
|
||||
ret = self.run_screen(
|
||||
seed_screens.SeedMnemonicEntryScreen,
|
||||
title=f"Seed Word #{self.cur_word_index + 1}", # Human-readable 1-indexing!
|
||||
initial_letters=list(self.cur_word) if self.cur_word else ["a"],
|
||||
wordlist=Seed.get_wordlist(wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)),
|
||||
).display()
|
||||
)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
if self.cur_word_index > 0:
|
||||
return Destination(BackStackView)
|
||||
# return Destination(
|
||||
# SeedMnemonicEntryView,
|
||||
# view_args={
|
||||
# "cur_word_index": self.cur_word_index - 1,
|
||||
# "is_calc_final_word": self.is_calc_final_word
|
||||
# }
|
||||
# )
|
||||
else:
|
||||
self.controller.storage.discard_pending_mnemonic()
|
||||
return Destination(MainMenuView)
|
||||
@@ -185,13 +179,14 @@ class SeedMnemonicInvalidView(View):
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
button_data = [EDIT, DISCARD]
|
||||
|
||||
selected_menu_num = WarningScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Invalid Mnemonic!",
|
||||
status_headline=None,
|
||||
text=f"Checksum failure; not a valid seed phrase.",
|
||||
show_back_button=False,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == EDIT:
|
||||
return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.gui.components import SeedSignerCustomIconConstants
|
||||
|
||||
from .view import View, Destination, BackStackView, MainMenuView
|
||||
from .view import View, Destination, MainMenuView
|
||||
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
|
||||
from seedsigner.models.settings import SettingsConstants, SettingsDefinition
|
||||
@@ -56,17 +55,17 @@ class SettingsMenuView(View):
|
||||
title = "Dev Options"
|
||||
next = None
|
||||
|
||||
screen = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title=title,
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data,
|
||||
selected_button=selected_button,
|
||||
scroll_y_initial_offset=self.initial_scroll,
|
||||
)
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
# Preserve our scroll position in this Screen so we can return
|
||||
initial_scroll = screen.buttons[0].scroll_y
|
||||
initial_scroll = self.screen.buttons[0].scroll_y
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
|
||||
@@ -100,6 +99,9 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
|
||||
self.selected_button = None
|
||||
self.parent_initial_scroll = parent_initial_scroll
|
||||
|
||||
# hook for test suite to override recursion in run()
|
||||
self.is_test = False
|
||||
|
||||
|
||||
def run(self):
|
||||
@@ -123,14 +125,18 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
if not self.selected_button:
|
||||
self.selected_button = 0
|
||||
|
||||
ret_value = settings_screens.SettingsEntryUpdateSelectionScreen(
|
||||
screen_args = dict(
|
||||
display_name=self.settings_entry.display_name,
|
||||
help_text=self.settings_entry.help_text,
|
||||
button_data=button_data,
|
||||
selected_button=self.selected_button,
|
||||
checked_buttons=checked_buttons,
|
||||
settings_entry_type=self.settings_entry.type,
|
||||
).display()
|
||||
settings_entry_type=self.settings_entry.type,
|
||||
)
|
||||
ret_value = self.run_screen(
|
||||
settings_screens.SettingsEntryUpdateSelectionScreen,
|
||||
**screen_args
|
||||
)
|
||||
|
||||
destination = None
|
||||
settings_menu_view_destination = Destination(
|
||||
@@ -178,7 +184,13 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
|
||||
# All selects stay in place; re-initialize where in the list we left off
|
||||
self.selected_button = ret_value
|
||||
return self.run()
|
||||
|
||||
if self.is_test:
|
||||
# Test suite can't handle the recursive call. Fake it by returning us here
|
||||
# via a Destination back to this View.
|
||||
return Destination(self.__class__, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll))
|
||||
else:
|
||||
return self.run()
|
||||
|
||||
|
||||
|
||||
@@ -187,7 +199,7 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
****************************************************************************"""
|
||||
class IOTestView(View):
|
||||
def run(self):
|
||||
settings_screens.IOTestScreen().display()
|
||||
self.run_screen(settings_screens.IOTestScreen)
|
||||
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
@@ -195,6 +207,6 @@ class IOTestView(View):
|
||||
|
||||
class DonateView(View):
|
||||
def run(self):
|
||||
settings_screens.DonateScreen().display()
|
||||
self.run_screen(settings_screens.DonateScreen)
|
||||
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
@@ -57,17 +57,19 @@ class View:
|
||||
self.canvas_height = self.renderer.canvas_height
|
||||
|
||||
self.buttons = self.controller.buttons
|
||||
self.screen = None
|
||||
|
||||
|
||||
def run_screen(self, Screen_cls: Type[BaseScreen], **kwargs) -> Union[int,str]:
|
||||
"""
|
||||
Instantiates the View class' Screen_cls and runs its interactive display.
|
||||
Instantiates the provided Screen_cls and runs its interactive display.
|
||||
Returns the user's input upon completion.
|
||||
"""
|
||||
return Screen_cls(**kwargs).display()
|
||||
self.screen = Screen_cls(**kwargs)
|
||||
return self.screen.display()
|
||||
|
||||
|
||||
def run(self, **kwargs):
|
||||
def run(self, **kwargs) -> 'Destination':
|
||||
raise Exception("Must implement in the child class")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from mock import MagicMock, patch
|
||||
from typing import Callable, List, Type, Union
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies.
|
||||
# These must precede any SeedSigner imports.
|
||||
sys.modules['seedsigner.gui.renderer'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.buttons'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models import Settings
|
||||
from seedsigner.views.view import Destination, View
|
||||
|
||||
|
||||
|
||||
class BaseTest:
|
||||
|
||||
def reset_controller(self):
|
||||
""" Wipe and re-initialize the Controller singleton for each test run """
|
||||
Controller._instance = None
|
||||
Controller.configure_instance()
|
||||
|
||||
|
||||
def setup_class(self):
|
||||
Settings.SETTINGS_FILENAME = "settings-test.json"
|
||||
|
||||
|
||||
def teardown_class(self):
|
||||
""" If settings were written to disk, delete """
|
||||
import os
|
||||
try:
|
||||
print(f"attempting to remove {Settings.SETTINGS_FILENAME}")
|
||||
os.remove(Settings.SETTINGS_FILENAME)
|
||||
print(f"Removed {Settings.SETTINGS_FILENAME}")
|
||||
except:
|
||||
print(f"{Settings.SETTINGS_FILENAME} not found to be removed")
|
||||
|
||||
|
||||
def setup_method(self):
|
||||
self.reset_controller()
|
||||
self.controller = Controller.get_instance()
|
||||
self.settings = Settings.get_instance()
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowStep:
|
||||
expected_view: Type[View] = None
|
||||
run_before: Callable = None
|
||||
screen_return_value: Union[int, str] = None
|
||||
|
||||
|
||||
|
||||
class FlowTest(BaseTest):
|
||||
def run_sequence(self, initial_destination: Destination, sequence: List[FlowStep]) -> Destination:
|
||||
"""
|
||||
Runs the given sequence from the initial_destination, mocking out the associated
|
||||
Screen and the scenario's return_value, receiving the resulting new Destination,
|
||||
and repeating until the sequence is complete.
|
||||
|
||||
Returns the final Destination.
|
||||
"""
|
||||
next_destination = initial_destination
|
||||
for flow_step in sequence:
|
||||
try:
|
||||
# Validate that the next Destination's View is what we expected
|
||||
if flow_step.expected_view:
|
||||
assert next_destination.View_cls == flow_step.expected_view
|
||||
|
||||
# Patch the `View.run_screen()` so we don't actually instantiate the Screen
|
||||
qualname = ".".join([next_destination.View_cls.__module__, next_destination.View_cls.__name__])
|
||||
with patch(qualname + ".run_screen") as mock_run_screen:
|
||||
mock_run_screen.return_value = flow_step.screen_return_value
|
||||
|
||||
# Now we can run the View and grab its resulting next Destination
|
||||
next_destination = next_destination.run(run_before=flow_step.run_before)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"{next_destination} | {flow_step}")
|
||||
raise e
|
||||
|
||||
return next_destination
|
||||
@@ -1,25 +1,21 @@
|
||||
import pytest
|
||||
|
||||
# Must import this before the Controller
|
||||
from utils import reset_controller
|
||||
from base import BaseTest
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
|
||||
|
||||
class TestController:
|
||||
|
||||
def setup_method(self):
|
||||
reset_controller()
|
||||
|
||||
class TestController(BaseTest):
|
||||
|
||||
def test_reset_controller(self):
|
||||
""" The reset_controller util should completely reset the Controller singleton """
|
||||
controller = Controller.get_instance()
|
||||
controller.address_explorer_data = "foo"
|
||||
|
||||
reset_controller()
|
||||
self.reset_controller()
|
||||
controller = Controller.get_instance()
|
||||
assert controller.address_explorer_data is None
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
from typing import List, Type, Union
|
||||
from mock import patch
|
||||
|
||||
# Must import this before the Controller
|
||||
from utils import reset_controller
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.views.view import Destination, MainMenuView, View
|
||||
from seedsigner.views import seed_views, scan_views
|
||||
|
||||
|
||||
|
||||
RUN_BEFORE = "run_before"
|
||||
RETURN_VALUE = "return_value"
|
||||
|
||||
|
||||
|
||||
def run_flow_sequence(initial_destination: Destination, return_values: List[Union[int, str, dict]]) -> Destination:
|
||||
"""
|
||||
Runs the given sequence from the initial_destination, mocking out the associated
|
||||
Screen and the scenario's return_value, receiving the resulting new Destination,
|
||||
and repeating until the sequence is complete.
|
||||
|
||||
Returns the final Destination.
|
||||
"""
|
||||
next_destination = initial_destination
|
||||
for return_value in return_values:
|
||||
# Optionally modify the View_cls instance before run_screen is called
|
||||
run_before = None
|
||||
if type(return_value) == dict:
|
||||
if RUN_BEFORE in return_value:
|
||||
run_before = return_value[RUN_BEFORE]
|
||||
|
||||
# Patch the run_screen so we don't actually instantiate and execute the Screen
|
||||
qualname = ".".join([next_destination.View_cls.__module__, next_destination.View_cls.__name__])
|
||||
with patch(qualname + ".run_screen") as mock_run_screen:
|
||||
mock_run_screen.return_value = return_value
|
||||
|
||||
# Now we can run the View and grab its resulting next Destination
|
||||
next_destination = next_destination.run(run_before=run_before)
|
||||
|
||||
return next_destination
|
||||
|
||||
|
||||
|
||||
class TestFlows:
|
||||
|
||||
def setup_method(self):
|
||||
reset_controller()
|
||||
|
||||
|
||||
def test_home_scan_seedoptions_flow(self):
|
||||
"""
|
||||
Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the
|
||||
Finalize Seed flow and end at the SeedOptionsView.
|
||||
"""
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
sequence = [
|
||||
0, # MainMenuView: ret SCAN
|
||||
{RUN_BEFORE: load_seed_into_decoder, RETURN_VALUE: None}, # ScanView: read SeedQR
|
||||
0, # SeedFinalizeView: ret DONE
|
||||
]
|
||||
destination = run_flow_sequence(Destination(MainMenuView), return_values=sequence)
|
||||
|
||||
# Should land on SeedOptionsView
|
||||
assert destination.View_cls == seed_views.SeedOptionsView
|
||||
|
||||
|
||||
def test_addressexplorer_seedfinalizescreen_flow(self):
|
||||
"""
|
||||
Finalizing a seed during the Address Explorer flow should return to the next
|
||||
Address Explorer step upon completion.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
seed = Seed(mnemonic=["abandon "* 11 + "about"])
|
||||
controller.storage.set_pending_seed(seed)
|
||||
|
||||
# Default behavior should land at the SeedOptionsView
|
||||
sequence = [
|
||||
0, # SeedFinalizeView: ret DONE
|
||||
]
|
||||
destination = run_flow_sequence(Destination(seed_views.SeedFinalizeView), return_values=sequence)
|
||||
assert destination.View_cls == seed_views.SeedOptionsView
|
||||
|
||||
# Reset
|
||||
controller.storage.seeds.clear()
|
||||
controller.storage.set_pending_seed(seed)
|
||||
|
||||
# Now set the flow and try again
|
||||
controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
|
||||
|
||||
# Finalize the new seed w/out passphrase
|
||||
sequence = [
|
||||
0, # SeedFinalizeView: ret DONE
|
||||
None, # SeedOptionsView should auto-route away
|
||||
]
|
||||
destination = run_flow_sequence(Destination(seed_views.SeedFinalizeView), return_values=sequence)
|
||||
|
||||
# Flow should resume at Script Type selection
|
||||
assert destination.View_cls == seed_views.SeedExportXpubScriptTypeView
|
||||
|
||||
# Reset
|
||||
controller.storage.seeds.clear()
|
||||
controller.storage.set_pending_seed(seed)
|
||||
|
||||
# Finalize the new seed w/passphrase
|
||||
sequence = [
|
||||
1, # SeedFinalizeView: ret PASSPHRASE
|
||||
"mypassphrase", # SeedPassphraseView: ret passphrase str
|
||||
1, # SeedReviewPassphraseView: ret DONE
|
||||
None, # SeedOptionsView should auto-route away
|
||||
]
|
||||
destination = run_flow_sequence(Destination(seed_views.SeedFinalizeView), return_values=sequence)
|
||||
|
||||
# Flow should resume at Script Type selection
|
||||
assert destination.View_cls == seed_views.SeedExportXpubScriptTypeView
|
||||
@@ -0,0 +1,143 @@
|
||||
|
||||
# Must import this before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.views.view import Destination, MainMenuView
|
||||
from seedsigner.views import seed_views, scan_views
|
||||
|
||||
|
||||
|
||||
class TestSeedFlows(FlowTest):
|
||||
|
||||
def test_scan_seedqr_flow(self):
|
||||
"""
|
||||
Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the
|
||||
Finalize Seed flow and end at the SeedOptionsView.
|
||||
"""
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
destination = self.run_sequence(
|
||||
Destination(MainMenuView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=0, # ret SCAN
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=scan_views.ScanView,
|
||||
run_before=load_seed_into_decoder, # simulate read SeedQR
|
||||
screen_return_value=None,
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedFinalizeView,
|
||||
screen_return_value=0, # ret DONE
|
||||
),
|
||||
]
|
||||
)
|
||||
assert destination.View_cls == seed_views.SeedOptionsView
|
||||
|
||||
|
||||
def test_mnemonic_entry_flow(self):
|
||||
"""
|
||||
Manually entering a mnemonic should land at the Finalize Seed flow and end at
|
||||
the SeedOptionsView.
|
||||
"""
|
||||
def test_with_mnemonic(mnemonic):
|
||||
sequence = [
|
||||
FlowStep(
|
||||
screen_return_value=1, # ret SEEDS
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedsMenuView,
|
||||
# Nothing to return; auto-forwards to LoadSeedView
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.LoadSeedView,
|
||||
screen_return_value=1 if len(mnemonic) == 12 else 2, # ret TYPE_12WORD or TYPE_24WORD
|
||||
),
|
||||
]
|
||||
|
||||
# Now add each manual word entry step
|
||||
for word in mnemonic:
|
||||
sequence.append(
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedMnemonicEntryView,
|
||||
screen_return_value=word
|
||||
)
|
||||
)
|
||||
|
||||
# With the mnemonic completely entered, we land on the SeedFinalizeView
|
||||
sequence.append(
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedFinalizeView,
|
||||
screen_return_value=0, # ret DONE
|
||||
)
|
||||
)
|
||||
|
||||
destination = self.run_sequence(
|
||||
Destination(MainMenuView),
|
||||
sequence=sequence
|
||||
)
|
||||
assert destination.View_cls == seed_views.SeedOptionsView
|
||||
|
||||
# Test data from iancoleman.io; 12- and 24-word mnemonic
|
||||
test_with_mnemonic("tone flat shed cool census soul paddle boy flight fantasy stem social".split())
|
||||
|
||||
self.reset_controller()
|
||||
|
||||
test_with_mnemonic("cotton artefact spy mind wing there echo steak child oak awful host despair online bicycle divorce middle firm diamond rare execute chimney almost hollow".split())
|
||||
|
||||
|
||||
def test_invalid_mnemonic(self):
|
||||
""" Should be able to go back and edit or discard an invalid mnemonic """
|
||||
# Test data from iancoleman.io
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
sequence = [
|
||||
FlowStep(
|
||||
screen_return_value=1 if len(mnemonic) == 12 else 2, # ret TYPE_12WORD or TYPE_24WORD
|
||||
),
|
||||
]
|
||||
for word in mnemonic[:-1]:
|
||||
sequence.append(
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedMnemonicEntryView,
|
||||
screen_return_value=word
|
||||
)
|
||||
)
|
||||
|
||||
sequence += [
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedMnemonicEntryView,
|
||||
screen_return_value="zoo" # But finish with an INVALID checksum word
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedMnemonicInvalidView,
|
||||
screen_return_value=0 # ret EDIT
|
||||
),
|
||||
]
|
||||
|
||||
# Restarts from first word
|
||||
for word in mnemonic[:-1]:
|
||||
sequence.append(
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedMnemonicEntryView,
|
||||
screen_return_value=word
|
||||
)
|
||||
)
|
||||
|
||||
sequence += [
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedMnemonicEntryView,
|
||||
screen_return_value="zebra" # provide yet another invalid checksum word
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedMnemonicInvalidView,
|
||||
screen_return_value=1 # ret DISCARD; this time we give up
|
||||
),
|
||||
]
|
||||
|
||||
destination = self.run_sequence(
|
||||
Destination(seed_views.LoadSeedView),
|
||||
sequence
|
||||
)
|
||||
assert destination.View_cls == MainMenuView
|
||||
@@ -0,0 +1,157 @@
|
||||
from mock import MagicMock
|
||||
from typing import Type
|
||||
|
||||
# Must import this before any seedsigner imports
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.models import SettingsDefinition
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.views.view import Destination, MainMenuView, View
|
||||
from seedsigner.views import settings_views
|
||||
|
||||
|
||||
|
||||
class TestSettingsFlows(FlowTest):
|
||||
|
||||
def test_persistent_settings(self):
|
||||
""" Basic flow from MainMenuView to enable/disable persistent settings """
|
||||
# Which option are we testing?
|
||||
settings_entries = SettingsDefinition.get_settings_entries()
|
||||
nth_entry = [s.attr_name for s in settings_entries].index(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
|
||||
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
|
||||
|
||||
def mock_settingsmenuview_screen(view: Type[View]):
|
||||
# SettingsMenuView reaches into its `self.screen` so we need to mock it out
|
||||
view.screen = MagicMock()
|
||||
|
||||
destination = self.run_sequence(
|
||||
Destination(MainMenuView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=3, # ret SETTINGS
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsMenuView,
|
||||
run_before=mock_settingsmenuview_screen,
|
||||
screen_return_value=nth_entry, # ret persistent settings
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsEntryUpdateSelectionView,
|
||||
screen_return_value=[s[0] for s in settings_entry.selection_options].index(SettingsConstants.OPTION__ENABLED),
|
||||
),
|
||||
]
|
||||
)
|
||||
assert destination.View_cls == settings_views.SettingsMenuView
|
||||
|
||||
|
||||
def test_multiselect(self):
|
||||
""" Multiselect Settings options should stay in-place; requires BACK to exit. """
|
||||
# Which option are we testing?
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS)
|
||||
nth_entry = SettingsDefinition.get_settings_entries().index(settings_entry)
|
||||
|
||||
def mock_settingsmenuview_screen(view: Type[View]):
|
||||
# SettingsMenuView reaches into its `self.screen` so we need to mock it out
|
||||
view.screen = MagicMock()
|
||||
|
||||
def disable_recursion(view: Type[View]):
|
||||
# The recursion in SettingsEntryUpdateSelectionView for multiselect settings
|
||||
# won't work with our FlowTest sequence structure.
|
||||
view.is_test = True
|
||||
|
||||
destination = self.run_sequence(
|
||||
Destination(MainMenuView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=3, # ret SETTINGS
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsMenuView,
|
||||
run_before=mock_settingsmenuview_screen,
|
||||
screen_return_value=nth_entry, # ret Coordinator software
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsEntryUpdateSelectionView,
|
||||
run_before=disable_recursion,
|
||||
screen_return_value=0, # select/deselect first option
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsEntryUpdateSelectionView,
|
||||
run_before=disable_recursion,
|
||||
screen_return_value=1, # select/deselect second option
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsEntryUpdateSelectionView,
|
||||
run_before=disable_recursion,
|
||||
screen_return_value=1, # select/deselect second option
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsEntryUpdateSelectionView,
|
||||
screen_return_value=RET_CODE__BACK_BUTTON, # BACK to exit
|
||||
),
|
||||
]
|
||||
)
|
||||
assert destination.View_cls == settings_views.SettingsMenuView
|
||||
|
||||
|
||||
def test_io_test(self):
|
||||
""" Basic flow from MainMenuView to I/O Test View """
|
||||
# I/O Test will be the nth_entry after we include the submenu to "Advanced"
|
||||
nth_entry = len(SettingsDefinition.get_settings_entries()) + 1
|
||||
|
||||
def mock_settingsmenuview_screen(view: Type[View]):
|
||||
# SettingsMenuView reaches into its `self.screen` so we need to mock it out
|
||||
view.screen = MagicMock()
|
||||
|
||||
destination = self.run_sequence(
|
||||
Destination(MainMenuView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=3, # ret SETTINGS
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsMenuView,
|
||||
run_before=mock_settingsmenuview_screen,
|
||||
screen_return_value=nth_entry, # ret I/O Test
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.IOTestView,
|
||||
screen_return_value=None, # ret value is ignored
|
||||
),
|
||||
]
|
||||
)
|
||||
# Exiting IOTestView should return us to the main SettingsMenuView
|
||||
assert destination.View_cls == settings_views.SettingsMenuView
|
||||
|
||||
|
||||
def test_donate(self):
|
||||
""" Basic flow from MainMenuView to Donate View """
|
||||
# DonateView will be the nth_entry after we include the submenu to "Advanced"
|
||||
# and the I/O Test
|
||||
nth_entry = len(SettingsDefinition.get_settings_entries()) + 2
|
||||
|
||||
def mock_settingsmenuview_screen(view: Type[View]):
|
||||
# SettingsMenuView reaches into its `self.screen` so we need to mock it out
|
||||
view.screen = MagicMock()
|
||||
|
||||
destination = self.run_sequence(
|
||||
Destination(MainMenuView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=3, # ret SETTINGS
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.SettingsMenuView,
|
||||
run_before=mock_settingsmenuview_screen,
|
||||
screen_return_value=nth_entry, # ret Donate
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=settings_views.DonateView,
|
||||
screen_return_value=None, # ret value is ignored
|
||||
),
|
||||
]
|
||||
)
|
||||
# Exiting IOTestView should return us to the main SettingsMenuView
|
||||
assert destination.View_cls == settings_views.SettingsMenuView
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
# Must import this before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.views.view import Destination, MainMenuView
|
||||
from seedsigner.views import seed_views, scan_views
|
||||
|
||||
|
||||
|
||||
class TestToolsFlows(FlowTest):
|
||||
|
||||
def test_addressexplorer_seedfinalizescreen_flow(self):
|
||||
"""
|
||||
Finalizing a seed during the Address Explorer flow should return to the next
|
||||
Address Explorer step upon completion.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
seed = Seed(mnemonic=["abandon "* 11 + "about"])
|
||||
controller.storage.set_pending_seed(seed)
|
||||
|
||||
# Default behavior should land at the SeedOptionsView
|
||||
destination = self.run_sequence(
|
||||
Destination(seed_views.SeedFinalizeView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=0, # ret DONE
|
||||
),
|
||||
]
|
||||
)
|
||||
assert destination.View_cls == seed_views.SeedOptionsView
|
||||
|
||||
# Reset
|
||||
controller.storage.seeds.clear()
|
||||
controller.storage.set_pending_seed(seed)
|
||||
|
||||
# Now set the flow and try again
|
||||
controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
|
||||
|
||||
# Finalize the new seed w/out passphrase
|
||||
destination = self.run_sequence(
|
||||
Destination(seed_views.SeedFinalizeView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=0, # ret DONE
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedOptionsView,
|
||||
screen_return_value=None, # should auto-route away w/out a selection
|
||||
),
|
||||
]
|
||||
)
|
||||
# Flow should resume at Script Type selection
|
||||
assert destination.View_cls == seed_views.SeedExportXpubScriptTypeView
|
||||
|
||||
# Reset
|
||||
controller.storage.seeds.clear()
|
||||
controller.storage.set_pending_seed(seed)
|
||||
|
||||
# Finalize the new seed w/passphrase
|
||||
destination = self.run_sequence(
|
||||
Destination(seed_views.SeedFinalizeView),
|
||||
sequence=[
|
||||
FlowStep(
|
||||
screen_return_value=1, # ret PASSPHRASE
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedAddPassphraseView,
|
||||
screen_return_value="mypassphrase",
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedReviewPassphraseView,
|
||||
screen_return_value=1, # ret DONE
|
||||
),
|
||||
FlowStep(
|
||||
expected_view=seed_views.SeedOptionsView,
|
||||
screen_return_value=None, # SeedOptionsView should auto-route away
|
||||
),
|
||||
]
|
||||
)
|
||||
# Flow should resume at Script Type selection
|
||||
assert destination.View_cls == seed_views.SeedExportXpubScriptTypeView
|
||||
@@ -1,17 +0,0 @@
|
||||
import sys
|
||||
from mock import MagicMock
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies
|
||||
sys.modules['seedsigner.gui.renderer'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.buttons'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
|
||||
|
||||
def reset_controller():
|
||||
""" Wipe and re-initialize the Controller singleton for each test run """
|
||||
Controller._instance = None
|
||||
Controller.configure_instance()
|
||||
Reference in New Issue
Block a user