Merge pull request #339 from kdmukai/test_flows

[Enhancement] Enable flow-based test scenarios
This commit is contained in:
Nick Klockenga
2023-07-20 14:46:38 -04:00
committed by GitHub
21 changed files with 1511 additions and 471 deletions
+1
View File
@@ -177,6 +177,7 @@ cd seedsigner
```bash
# Takes 1hr 45min on a Pi Zero 1.3
pip install -r requirements.txt
pip install -r requirements-raspi.txt
```
#### `pyzbar`
+3
View File
@@ -0,0 +1,3 @@
picamera==1.13
RPi.GPIO==0.7.0
spidev==3.5
-3
View File
@@ -1,10 +1,7 @@
embit==0.7.0
numpy==1.21.1
picamera==1.13
Pillow==8.2.0
-e git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar
qrcode==7.3.1
RPi.GPIO==0.7.0
six==1.16.0
spidev==3.5
urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
+42 -2
View File
@@ -32,6 +32,23 @@ class BackStack(List[Destination]):
class StopFlowBasedTest(Exception):
"""
This is a special exception that is only raised by the test suite to stop the
Controller's main loop. It should not be raised by any other code.
"""
pass
class FlowBasedTestException(Exception):
"""
This is a special exception that is only raised by the test suite.
It should not be raised by any other code.
"""
pass
class Controller(Singleton):
"""
The Controller is a globally available singleton that maintains SeedSigner state.
@@ -185,7 +202,13 @@ class Controller(Singleton):
self.back_stack = BackStack()
def start(self) -> None:
def start(self, initial_destination: Destination = None) -> None:
"""
The main loop of the application.
* initial_destination: The first View to run. If None, the MainMenuView is
used. Only used by the test suite.
"""
from .views import MainMenuView, BackStackView
from .views.screensaver import OpeningSplashScreen
@@ -218,7 +241,10 @@ class Controller(Singleton):
View_cls(**init_args).run()
"""
try:
next_destination = Destination(MainMenuView)
if initial_destination:
next_destination = initial_destination
else:
next_destination = Destination(MainMenuView)
while True:
# Destination(None) is a special case; render the Home screen
if next_destination.View_cls is None:
@@ -240,10 +266,24 @@ class Controller(Singleton):
print(f"back_stack: {self.back_stack}")
try:
# Instantiate the View class and run it
print(f"Executing {next_destination}")
next_destination = next_destination.run()
except StopFlowBasedTest:
# This is a special exception that is only raised by the test suite
# to stop the Controller loop and exit the test.
return
except FlowBasedTestException as e:
# This is a special exception that is only raised by the test suite.
# Re-raise so the test suite can handle it.
raise e
except Exception as e:
# Display user-friendly error screen w/debugging info
import traceback
traceback.print_exc()
next_destination = self.handle_exception(e)
if not next_destination:
+1 -1
View File
@@ -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[str | int], str] = None
default_value: Any = None
def __post_init__(self):
+73 -60
View File
@@ -11,9 +11,8 @@ from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.qr_type import QRType
from seedsigner.models.settings import SettingsConstants
from seedsigner.gui.screens import psbt_screens
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, DireWarningScreen,
LoadingScreenThread, QRDisplayScreen, WarningScreen)
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, DireWarningScreen, QRDisplayScreen)
from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
@@ -22,6 +21,11 @@ logger = logging.getLogger(__name__)
class PSBTSelectSeedView(View):
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
button_data = []
def run(self):
# Note: we can't just autoroute to the PSBT Overview because we might have a
# multisig where we want to sign with more than one key on this device.
@@ -31,32 +35,29 @@ class PSBTSelectSeedView(View):
seeds = self.controller.storage.seeds
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
button_data = []
for seed in seeds:
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
# Doesn't look like this seed can sign the current PSBT
button_str += " (?)"
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
self.button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
button_data.append(SCAN_SEED)
button_data.append(TYPE_12WORD)
button_data.append(TYPE_24WORD)
self.button_data.append(self.SCAN_SEED)
self.button_data.append(self.TYPE_12WORD)
self.button_data.append(self.TYPE_24WORD)
if self.controller.psbt_seed:
if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.controller.psbt_seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
# skip the seed prompt if a seed was previous selected and has matching input fingerprint
return Destination(PSBTOverviewView)
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Select Signer",
is_button_text_centered=False,
button_data=button_data
).display()
button_data=self.button_data
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -69,13 +70,13 @@ class PSBTSelectSeedView(View):
# The remaining flows are a sub-flow; resume PSBT flow once the seed is loaded.
self.controller.resume_main_flow = Controller.FLOW__PSBT
if button_data[selected_menu_num] == SCAN_SEED:
if self.button_data[selected_menu_num] == self.SCAN_SEED:
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
if button_data[selected_menu_num] == TYPE_12WORD:
if button_data[selected_menu_num] == self.TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
else:
self.controller.storage.init_pending_mnemonic(num_words=24)
@@ -92,8 +93,10 @@ class PSBTOverviewView(View):
if not self.controller.psbt_parser or self.controller.psbt_parser.seed != self.controller.psbt_seed:
# The PSBTParser takes a while to read the PSBT. Run the loading screen while
# we wait.
from seedsigner.gui.screens.screen import LoadingScreenThread
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
self.loading_screen.start()
try:
self.controller.psbt_parser = PSBTParser(
self.controller.psbt,
@@ -128,23 +131,22 @@ class PSBTOverviewView(View):
else:
num_self_transfer_outputs += 1
screen = psbt_screens.PSBTOverviewScreen(
# Everything is set. Stop the loading screen
if self.loading_screen:
self.loading_screen.stop()
# Run the overview screen
selected_menu_num = self.run_screen(
PSBTOverviewScreen,
spend_amount=psbt_parser.spend_amount,
change_amount=psbt_parser.change_amount,
fee_amount=psbt_parser.fee_amount,
num_inputs=psbt_parser.num_inputs,
num_self_transfer_outputs=num_self_transfer_outputs,
num_change_outputs=num_change_outputs,
destination_addresses=psbt_parser.destination_addresses,
destination_addresses=psbt_parser.destination_addresses
)
# Everything is set. Stop the loading screen
if self.loading_screen:
self.loading_screen.stop()
# Run the overview screen
selected_menu_num = screen.display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
self.controller.psbt_seed = None
return Destination(BackStackView)
@@ -214,14 +216,15 @@ class PSBTMathView(View):
# Should not be able to get here
return Destination(MainMenuView)
selected_menu_num = psbt_screens.PSBTMathScreen(
selected_menu_num = self.run_screen(
PSBTMathScreen,
input_amount=psbt_parser.input_amount,
num_inputs=psbt_parser.num_inputs,
spend_amount=psbt_parser.spend_amount,
num_recipients=psbt_parser.num_destinations,
fee_amount=psbt_parser.fee_amount,
change_amount=psbt_parser.change_amount,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -238,7 +241,10 @@ class PSBTAddressDetailsView(View):
"""
Shows the recipient's address and amount they will receive
"""
def __init__(self, address_num, is_change=False):
NEXT = "Next"
button_data = []
def __init__(self, address_num):
super().__init__()
self.address_num = address_num
@@ -254,23 +260,27 @@ class PSBTAddressDetailsView(View):
if psbt_parser.num_destinations > 1:
title += f" (#{self.address_num + 1})"
button_data = []
if self.address_num < psbt_parser.num_destinations - 1:
button_data.append("Next Recipient")
self.NEXT = "Next Recipient"
else:
button_data.append("Next")
self.NEXT = "Next"
self.button_data.append(self.NEXT)
selected_menu_num = psbt_screens.PSBTAddressDetailsScreen(
selected_menu_num = self.run_screen(
PSBTAddressDetailsScreen,
title=title,
button_data=button_data,
button_data=self.button_data,
address=psbt_parser.destination_addresses[self.address_num],
amount=psbt_parser.destination_amounts[self.address_num],
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
if selected_menu_num == 0:
if self.button_data[selected_menu_num] == self.NEXT:
if self.address_num < len(psbt_parser.destination_addresses) - 1:
# Show the next receive addr
return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1, "is_change": False})
return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1})
elif psbt_parser.change_amount > 0:
# Move on to display change
@@ -280,14 +290,13 @@ class PSBTAddressDetailsView(View):
# There's no change output to verify. Move on to sign the PSBT.
return Destination(PSBTFinalizeView)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
class PSBTChangeDetailsView(View):
"""
"""
NEXT = "Next"
VERIFY_MULTISIG = "Verify Multisig Change"
button_data = [NEXT]
def __init__(self, change_address_num):
super().__init__()
self.change_address_num = change_address_num
@@ -327,14 +336,12 @@ class PSBTChangeDetailsView(View):
is_change_derivation_path = int(derivation_path.split("/")[-2]) == 1
derivation_path_addr_index = int(derivation_path.split("/")[-1])
NEXT = "Next"
if is_change_derivation_path:
title = "Your Change"
VERIFY_MULTISIG = "Verify Multisig Change"
self.VERIFY_MULTISIG = "Verify Multisig Change"
else:
title = "Self-Transfer"
VERIFY_MULTISIG = "Verify Multisig Addr"
self.VERIFY_MULTISIG = "Verify Multisig Addr"
# if psbt_parser.num_change_outputs > 1:
# title += f" (#{self.change_address_num + 1})"
@@ -343,11 +350,11 @@ class PSBTChangeDetailsView(View):
# if the known-good multisig descriptor is already onboard:
if self.controller.multisig_wallet_descriptor:
is_change_addr_verified = psbt_parser.verify_multisig_output(self.controller.multisig_wallet_descriptor, change_num=self.change_address_num)
button_data = [NEXT]
self.button_data = [self.NEXT]
else:
# Have the Screen offer to load in the multisig descriptor.
button_data = [VERIFY_MULTISIG, NEXT]
self.button_data = [self.VERIFY_MULTISIG, self.NEXT]
else:
# Single sig
@@ -356,6 +363,7 @@ class PSBTChangeDetailsView(View):
loading_screen_text = "Verifying Change..."
else:
loading_screen_text = "Verifying Self-Transfer..."
from seedsigner.gui.screens.screen import LoadingScreenThread
loading_screen = LoadingScreenThread(text=loading_screen_text)
loading_screen.start()
@@ -389,7 +397,7 @@ class PSBTChangeDetailsView(View):
if change_data["address"] == calc_address:
is_change_addr_verified = True
button_data = [NEXT]
self.button_data = [self.NEXT]
finally:
loading_screen.stop()
@@ -397,9 +405,10 @@ class PSBTChangeDetailsView(View):
if is_change_addr_verified == False and (not psbt_parser.is_multisig or self.controller.multisig_wallet_descriptor is not None):
return Destination(PSBTAddressVerificationFailedView, view_args=dict(is_change=is_change_derivation_path, is_multisig=psbt_parser.is_multisig), clear_history=True)
selected_menu_num = psbt_screens.PSBTChangeDetailsScreen(
selected_menu_num = self.run_screen(
PSBTChangeDetailsScreen,
title=title,
button_data=button_data,
button_data=self.button_data,
address=change_data.get("address"),
amount=change_data.get("amount"),
is_multisig=psbt_parser.is_multisig,
@@ -408,19 +417,19 @@ class PSBTChangeDetailsView(View):
is_change_derivation_path=is_change_derivation_path,
derivation_path_addr_index=derivation_path_addr_index,
is_change_addr_verified=is_change_addr_verified,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == NEXT:
elif self.button_data[selected_menu_num] == self.NEXT:
if self.change_address_num < psbt_parser.num_change_outputs - 1:
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
else:
# There's no more change to verify. Move on to sign the PSBT.
return Destination(PSBTFinalizeView)
elif button_data[selected_menu_num] == VERIFY_MULTISIG:
elif self.button_data[selected_menu_num] == self.VERIFY_MULTISIG:
from seedsigner.views.seed_views import LoadMultisigWalletDescriptorView
self.controller.resume_main_flow = Controller.FLOW__PSBT
return Destination(LoadMultisigWalletDescriptorView)
@@ -459,6 +468,9 @@ class PSBTAddressVerificationFailedView(View):
class PSBTFinalizeView(View):
"""
"""
APPROVE_PSBT = "Approve PSBT"
button_data = [APPROVE_PSBT]
def run(self):
psbt_parser: PSBTParser = self.controller.psbt_parser
psbt: PSBT = self.controller.psbt
@@ -466,12 +478,13 @@ class PSBTFinalizeView(View):
if not psbt_parser:
# Should not be able to get here
return Destination(MainMenuView)
selected_menu_num = self.run_screen(
PSBTFinalizeScreen,
button_data=self.button_data
)
selected_menu_num = psbt_screens.PSBTFinalizeScreen(
button_data=["Approve PSBT"]
).display()
if selected_menu_num == 0:
if self.button_data[selected_menu_num] == self.APPROVE_PSBT:
# Sign PSBT
sig_cnt = PSBTParser.sig_count(psbt)
psbt.sign_with(psbt_parser.root)
@@ -500,7 +513,7 @@ class PSBTSignedQRDisplayView(View):
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
)
QRDisplayScreen(qr_encoder=qr_encoder).display()
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
# We're done with this PSBT. Route back to MainMenuView which always
# clears all ephemeral data (except in-memory seeds).
+13 -7
View File
@@ -4,6 +4,7 @@ import re
from embit.descriptor import Descriptor
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
from seedsigner.gui.screens import scan_screens
from seedsigner.models import DecodeQR, Seed
from seedsigner.models.settings import SettingsConstants
@@ -12,19 +13,24 @@ from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Dest
class ScanView(View):
def __init__(self):
super().__init__()
# Set up the QR decoder here so we can inject data into it in the test suite's
# `before_run`.
self.wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
self.decoder = DecodeQR(wordlist_language_code=self.wordlist_language_code)
def run(self):
from seedsigner.gui.screens.scan_screens import ScanScreen
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
# Start the live preview and background QR reading
ScanScreen(decoder=self.decoder).display()
self.run_screen(scan_screens.ScanScreen, decoder=self.decoder)
# Handle the results
if self.decoder.is_complete:
if self.decoder.is_seed:
seed_mnemonic = self.decoder.get_seed_phrase()
if not seed_mnemonic:
# seed is not valid, Exit if not valid with message
raise Exception("Not yet implemented!")
@@ -33,7 +39,7 @@ class ScanView(View):
# pending (might set a passphrase, SeedXOR, etc) until finalized.
from .seed_views import SeedFinalizeView
self.controller.storage.set_pending_seed(
Seed(mnemonic=seed_mnemonic, wordlist_language_code=wordlist_language_code)
Seed(mnemonic=seed_mnemonic, wordlist_language_code=self.wordlist_language_code)
)
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED:
from seedsigner.views.seed_views import SeedAddPassphraseView
+160 -140
View File
@@ -13,7 +13,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustom
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 LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen
from seedsigner.models.decode_qr import DecodeQR
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.psbt_parser import PSBTParser
@@ -31,6 +31,8 @@ from .view import NotYetImplementedView, View, Destination, BackStackView, MainM
class SeedsMenuView(View):
LOAD = "Load a seed"
def __init__(self):
super().__init__()
self.seeds = []
@@ -50,11 +52,12 @@ class SeedsMenuView(View):
button_data.append((seed["fingerprint"], SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
button_data.append("Load a seed")
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title="In-Memory Seeds",
is_button_text_centered=False,
button_data=button_data
).display()
)
if len(self.seeds) > 0 and selected_menu_num < len(self.seeds):
return Destination(SeedOptionsView, view_args={"seed_num": selected_menu_num})
@@ -71,40 +74,41 @@ class SeedsMenuView(View):
Loading seeds, passphrases, etc
****************************************************************************"""
class LoadSeedView(View):
def run(self):
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
button_data=[
SEED_QR,
TYPE_12WORD,
TYPE_24WORD,
CREATE,
]
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
selected_menu_num = ButtonListScreen(
def run(self):
button_data = [
self.SEED_QR,
self.TYPE_12WORD,
self.TYPE_24WORD,
self.CREATE,
]
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Load A Seed",
is_button_text_centered=False,
button_data=button_data
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
if button_data[selected_menu_num] == SEED_QR:
if button_data[selected_menu_num] == self.SEED_QR:
from .scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] == TYPE_12WORD:
elif button_data[selected_menu_num] == self.TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == TYPE_24WORD:
elif button_data[selected_menu_num] == self.TYPE_24WORD:
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == CREATE:
elif button_data[selected_menu_num] == self.CREATE:
from .tools_views import ToolsMenuView
return Destination(ToolsMenuView)
@@ -119,22 +123,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)
@@ -174,34 +172,38 @@ class SeedMnemonicEntryView(View):
class SeedMnemonicInvalidView(View):
EDIT = "Review & Edit"
DISCARD = ("Discard", None, None, "red")
def __init__(self):
super().__init__()
self.mnemonic: List[str] = self.controller.storage.pending_mnemonic
def run(self):
EDIT = "Review & Edit"
DISCARD = ("Discard", None, None, "red")
button_data = [EDIT, DISCARD]
selected_menu_num = WarningScreen(
button_data = [self.EDIT, self.DISCARD]
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:
if button_data[selected_menu_num] == self.EDIT:
return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0})
elif button_data[selected_menu_num] == DISCARD:
elif button_data[selected_menu_num] == self.DISCARD:
self.controller.storage.discard_pending_mnemonic()
return Destination(MainMenuView)
class SeedFinalizeView(View):
FINALIZE = "Done"
PASSPHRASE = "BIP-39 Passphrase"
def __init__(self):
super().__init__()
self.seed = self.controller.storage.get_pending_seed()
@@ -209,25 +211,21 @@ class SeedFinalizeView(View):
def run(self):
FINALIZE = "Done"
PASSPHRASE = "BIP-39 Passphrase"
button_data = []
button_data.append(FINALIZE)
button_data = [self.FINALIZE]
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED:
button_data.append(PASSPHRASE)
button_data.append(self.PASSPHRASE)
selected_menu_num = seed_screens.SeedFinalizeScreen(
selected_menu_num = self.run_screen(
seed_screens.SeedFinalizeScreen,
fingerprint=self.fingerprint,
button_data=button_data,
).display()
)
if button_data[selected_menu_num] == FINALIZE:
if button_data[selected_menu_num] == self.FINALIZE:
seed_num = self.controller.storage.finalize_pending_seed()
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
elif button_data[selected_menu_num] == PASSPHRASE:
elif button_data[selected_menu_num] == self.PASSPHRASE:
return Destination(SeedAddPassphraseView)
@@ -239,7 +237,7 @@ class SeedAddPassphraseView(View):
def run(self):
ret = seed_screens.SeedAddPassphraseScreen(passphrase=self.seed.passphrase).display()
ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase)
if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -257,16 +255,15 @@ class SeedReviewPassphraseView(View):
"""
Display the completed passphrase back to the user.
"""
EDIT = "Edit passphrase"
DONE = "Done"
def __init__(self):
super().__init__()
self.seed = self.controller.storage.get_pending_seed()
def run(self):
EDIT = "Edit passphrase"
DONE = "Done"
button_data = [EDIT, DONE]
# Get the before/after fingerprints
network = self.settings.get_value(SettingsConstants.SETTING__NETWORK)
passphrase = self.seed.passphrase
@@ -275,26 +272,32 @@ class SeedReviewPassphraseView(View):
fingerprint_without = self.seed.get_fingerprint(network=network)
self.seed.set_passphrase(passphrase)
# Because we have ane explicit "Edit" button, we disable "BACK" to keep the
button_data = [self.EDIT, self.DONE]
# Because we have an explicit "Edit" button, we disable "BACK" to keep the
# routing options sane.
selected_menu_num = seed_screens.SeedReviewPassphraseScreen(
selected_menu_num = self.run_screen(
seed_screens.SeedReviewPassphraseScreen,
fingerprint_without=fingerprint_without,
fingerprint_with=fingerprint_with,
passphrase=self.seed.passphrase,
button_data=button_data,
show_back_button=False,
).display()
)
if button_data[selected_menu_num] == EDIT:
if button_data[selected_menu_num] == self.EDIT:
return Destination(SeedAddPassphraseView)
elif button_data[selected_menu_num] == DONE:
elif button_data[selected_menu_num] == self.DONE:
seed_num = self.controller.storage.finalize_pending_seed()
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
class SeedDiscardView(View):
KEEP = "Keep Seed"
DISCARD = ("Discard", None, None, "red")
def __init__(self, seed_num: int = None):
super().__init__()
self.seed_num = seed_num
@@ -305,27 +308,26 @@ class SeedDiscardView(View):
def run(self):
KEEP = "Keep Seed"
DISCARD = ("Discard", None, None, "red")
button_data = [KEEP, DISCARD]
button_data = [self.KEEP, self.DISCARD]
fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
selected_menu_num = WarningScreen(
selected_menu_num = self.run_screen(
WarningScreen,
title="Discard Seed?",
status_headline=None,
text=f"Wipe seed {fingerprint} from the device?",
show_back_button=False,
button_data=button_data,
).display()
)
if button_data[selected_menu_num] == KEEP:
if button_data[selected_menu_num] == self.KEEP:
# Use skip_current_view=True to prevent BACK from landing on this warning screen
if self.seed_num is not None:
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, skip_current_view=True)
else:
return Destination(SeedFinalizeView, skip_current_view=True)
elif button_data[selected_menu_num] == DISCARD:
elif button_data[selected_menu_num] == self.DISCARD:
if self.seed_num is not None:
self.controller.discard_seed(self.seed_num)
else:
@@ -338,6 +340,15 @@ class SeedDiscardView(View):
Views for actions on individual seeds:
****************************************************************************"""
class SeedOptionsView(View):
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
VERIFY_ADDRESS = "Verify Addr"
EXPORT_XPUB = "Export Xpub"
EXPLORER = "Address Explorer"
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
BIP85_CHILD_SEED = "BIP-85 Child Seed"
DISCARD = ("Discard Seed", None, None, "red")
def __init__(self, seed_num: int):
super().__init__()
self.seed_num = seed_num
@@ -347,90 +358,87 @@ class SeedOptionsView(View):
def run(self):
from seedsigner.views.psbt_views import PSBTOverviewView
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
VERIFY_ADDRESS = "Verify Addr"
EXPORT_XPUB = "Export Xpub"
EXPLORER = "Address Explorer"
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
BIP85_CHILD_SEED = "BIP-85 Child Seed"
DISCARD = ("Discard Seed", None, None, "red")
button_data = []
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
# Jump straight back into the address explorer script type selection flow
# But do ont cancel the `resume_main_flow` as we'll still need that after
# derivation path is specified.
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
if self.controller.unverified_address:
if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
# Jump straight back into the single sig addr verification flow
self.controller.resume_main_flow = None
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num), skip_current_view=True)
addr = self.controller.unverified_address["address"][:7]
VERIFY_ADDRESS += f" {addr}"
button_data.append(VERIFY_ADDRESS)
if self.controller.psbt:
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
# Re-route us directly back to the start of the PSBT flow
self.controller.resume_main_flow = None
self.controller.psbt_seed = self.seed
return Destination(PSBTOverviewView, skip_current_view=True)
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
# Jump straight back into the address explorer script type selection flow
# But don't cancel the `resume_main_flow` as we'll still need that after
# derivation path is specified.
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
button_data.append(SCAN_PSBT)
if self.controller.psbt:
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
# Re-route us directly back to the start of the PSBT flow
self.controller.resume_main_flow = None
self.controller.psbt_seed = self.seed
return Destination(PSBTOverviewView, skip_current_view=True)
button_data = []
if self.controller.unverified_address:
addr = self.controller.unverified_address["address"][:7]
self.VERIFY_ADDRESS += f" {addr}"
button_data.append(self.VERIFY_ADDRESS)
button_data.append(self.SCAN_PSBT)
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
button_data.append(EXPORT_XPUB)
button_data.append(self.EXPORT_XPUB)
button_data.append(EXPLORER)
button_data.append(BACKUP)
button_data.append(self.EXPLORER)
button_data.append(self.BACKUP)
if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED:
button_data.append(BIP85_CHILD_SEED)
button_data.append(self.BIP85_CHILD_SEED)
button_data.append(DISCARD)
selected_menu_num = seed_screens.SeedOptionsScreen(
button_data.append(self.DISCARD)
selected_menu_num = self.run_screen(
seed_screens.SeedOptionsScreen,
button_data=button_data,
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
has_passphrase=self.seed.passphrase is not None,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
# Force BACK to always return to the Main Menu
return Destination(MainMenuView)
if button_data[selected_menu_num] == SCAN_PSBT:
if button_data[selected_menu_num] == self.SCAN_PSBT:
from seedsigner.views.scan_views import ScanView
self.controller.psbt_seed = self.controller.get_seed(self.seed_num)
return Destination(ScanView)
elif button_data[selected_menu_num] == VERIFY_ADDRESS:
elif button_data[selected_menu_num] == self.VERIFY_ADDRESS:
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num))
elif button_data[selected_menu_num] == EXPORT_XPUB:
elif button_data[selected_menu_num] == self.EXPORT_XPUB:
return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num))
elif button_data[selected_menu_num] == EXPLORER:
elif button_data[selected_menu_num] == self.EXPLORER:
self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG))
elif button_data[selected_menu_num] == BACKUP:
elif button_data[selected_menu_num] == self.BACKUP:
return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num))
elif button_data[selected_menu_num] == BIP85_CHILD_SEED:
elif button_data[selected_menu_num] == self.BIP85_CHILD_SEED:
return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num})
elif button_data[selected_menu_num] == DISCARD:
elif button_data[selected_menu_num] == self.DISCARD:
return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num))
class SeedBackupView(View):
VIEW_WORDS = "View Seed Words"
EXPORT_SEEDQR = "Export as SeedQR"
def __init__(self, seed_num):
super().__init__()
self.seed_num = seed_num
@@ -438,23 +446,22 @@ class SeedBackupView(View):
def run(self):
VIEW_WORDS = "View Seed Words"
EXPORT_SEEDQR = "Export as SeedQR"
button_data = [VIEW_WORDS, EXPORT_SEEDQR]
button_data = [self.VIEW_WORDS, self.EXPORT_SEEDQR]
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Backup Seed",
button_data=button_data,
is_bottom_list=True,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == VIEW_WORDS:
elif button_data[selected_menu_num] == self.VIEW_WORDS:
return Destination(SeedWordsWarningView, view_args={"seed_num": self.seed_num})
elif button_data[selected_menu_num] == EXPORT_SEEDQR:
elif button_data[selected_menu_num] == self.EXPORT_SEEDQR:
return Destination(SeedTranscribeSeedQRFormatView, view_args={"seed_num": self.seed_num})
@@ -463,6 +470,9 @@ class SeedBackupView(View):
Export Xpub flow
****************************************************************************"""
class SeedExportXpubSigTypeView(View):
SINGLE_SIG = "Single Sig"
MULTISIG = "Multisig"
def __init__(self, seed_num: int):
super().__init__()
self.seed_num = seed_num
@@ -473,22 +483,21 @@ class SeedExportXpubSigTypeView(View):
# Nothing to select; skip this screen
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": self.settings.get_value(SettingsConstants.SETTING__SIG_TYPES)[0]}, skip_current_view=True)
SINGLE_SIG = "Single Sig"
MULTISIG = "Multisig"
button_data=[SINGLE_SIG, MULTISIG]
button_data=[self.SINGLE_SIG, self.MULTISIG]
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Export Xpub",
button_data=button_data
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
if button_data[selected_menu_num] == SINGLE_SIG:
if button_data[selected_menu_num] == self.SINGLE_SIG:
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.SINGLE_SIG})
elif button_data[selected_menu_num] == MULTISIG:
elif button_data[selected_menu_num] == self.MULTISIG:
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.MULTISIG})
@@ -512,21 +521,22 @@ class SeedExportXpubScriptTypeView(View):
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True)
else:
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
button_data = []
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
button_data.append(script_type)
title = "Export Xpub"
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
title = "Address Explorer"
selected_menu_num = ButtonListScreen(
button_data = []
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
button_data.append(script_type)
selected_menu_num = self.run_screen(
ButtonListScreen,
title=title,
is_button_text_centered=False,
button_data=button_data,
is_bottom_list=True,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
# If previous view is SeedOptionsView then that should be where resume_main_flow started (otherwise it would have been skipped).
@@ -558,10 +568,12 @@ class SeedExportXpubCustomDerivationView(View):
self.script_type = script_type
self.custom_derivation_path = "m/"
def run(self):
ret = seed_screens.SeedExportXpubCustomDerivationScreen(
ret = self.run_screen(
seed_screens.SeedExportXpubCustomDerivationScreen,
initial_value=self.custom_derivation_path,
).display()
)
if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -606,11 +618,12 @@ class SeedExportXpubCoordinatorView(View):
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Export Xpub",
is_button_text_centered=False,
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS),
).display()
)
if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)):
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num]
@@ -648,10 +661,11 @@ class SeedExportXpubWarningView(View):
# Skip the WarningView entirely
return destination
selected_menu_num = WarningScreen(
selected_menu_num = self.run_screen(
WarningScreen,
status_headline="Privacy Leak!",
text="""Xpub can be used to view all future transactions.""",
).display()
)
if selected_menu_num == 0:
# User clicked "I Understand"
@@ -694,6 +708,7 @@ class SeedExportXpubDetailsView(View):
else:
# The derivation calc takes a few moments. Run the loading screen while we wait.
from seedsigner.gui.screens.screen import LoadingScreenThread
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
self.loading_screen.start()
@@ -716,12 +731,13 @@ class SeedExportXpubDetailsView(View):
finally:
self.loading_screen.stop()
selected_menu_num = seed_screens.SeedExportXpubDetailsScreen(
selected_menu_num = self.run_screen(
seed_screens.SeedExportXpubDetailsScreen,
fingerprint=fingerprint,
has_passphrase=self.seed.passphrase is not None,
derivation_path=derivation_path,
xpub=xpub_base58,
).display()
)
if selected_menu_num == 0:
return Destination(
@@ -773,7 +789,10 @@ class SeedExportXpubQRDisplayView(View):
def run(self):
QRDisplayScreen(qr_encoder=self.qr_encoder).display()
self.run_screen(
QRDisplayScreen,
qr_encoder=self.qr_encoder
)
return Destination(MainMenuView)
@@ -803,9 +822,10 @@ class SeedWordsWarningView(View):
# Forward straight to showing the words
return destination
selected_menu_num = DireWarningScreen(
text="""Never input your seed phrase into a device that connects to the internet.""",
).display()
selected_menu_num = self.run_screen(
DireWarningScreen,
text="""You must keep your seed words private & away from all online devices.""",
)
if selected_menu_num == 0:
# User clicked "I Understand"
+30 -27
View File
@@ -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
@@ -9,6 +8,9 @@ from seedsigner.models.settings import SettingsConstants, SettingsDefinition
class SettingsMenuView(View):
IO_TEST = "I/O test"
DONATE = "Donate"
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0):
super().__init__()
self.visibility = visibility
@@ -19,9 +21,6 @@ class SettingsMenuView(View):
def run(self):
IO_TEST = "I/O test"
DONATE = "Donate"
settings_entries = SettingsDefinition.get_settings_entries(
visibility=self.visibility
)
@@ -39,34 +38,34 @@ class SettingsMenuView(View):
# Set up the next nested level of menuing
button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
button_data.append(IO_TEST)
button_data.append(DONATE)
button_data.append(self.IO_TEST)
button_data.append(self.DONATE)
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
title = "Advanced"
# So far there are no real Developer options; disabling for now
# button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
# next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
next = None
# next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
next_destination = None
elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER:
title = "Dev Options"
next = None
next_destination = 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:
@@ -77,12 +76,12 @@ class SettingsMenuView(View):
return Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
elif selected_menu_num == len(settings_entries):
return next
return next_destination
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == IO_TEST:
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.IO_TEST:
return Destination(IOTestView)
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == DONATE:
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.DONATE:
return Destination(DonateView)
else:
@@ -95,10 +94,10 @@ class SettingsEntryUpdateSelectionView(View):
Handles changes to all selection-type settings (Multiselect, SELECT_1,
Enabled/Disabled, etc).
"""
def __init__(self, attr_name: str, parent_initial_scroll: int = 0):
def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None):
super().__init__()
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
self.selected_button = None
self.selected_button = selected_button
self.parent_initial_scroll = parent_initial_scroll
@@ -111,7 +110,9 @@ class SettingsEntryUpdateSelectionView(View):
value, display_name = value
else:
display_name = value
button_data.append(display_name)
if (type(initial_value) == list and value in initial_value) or value == initial_value:
checked_buttons.append(i)
@@ -120,17 +121,18 @@ class SettingsEntryUpdateSelectionView(View):
# selected option).
self.selected_button = i
if not self.selected_button:
if self.selected_button is None:
self.selected_button = 0
ret_value = settings_screens.SettingsEntryUpdateSelectionScreen(
ret_value = self.run_screen(
settings_screens.SettingsEntryUpdateSelectionScreen,
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()
)
destination = None
settings_menu_view_destination = Destination(
@@ -178,7 +180,8 @@ 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()
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True)
@@ -187,7 +190,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 +198,6 @@ class IOTestView(View):
class DonateView(View):
def run(self):
settings_screens.DonateScreen().display()
self.run_screen(settings_screens.DonateScreen)
return Destination(SettingsMenuView)
+96 -88
View File
@@ -7,8 +7,6 @@ from embit.descriptor import Descriptor
from PIL import Image
from PIL.ImageOps import autocontrast
from seedsigner.controller import Controller
from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen
from seedsigner.hardware.camera import Camera
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
@@ -25,32 +23,34 @@ from .view import View, Destination, BackStackView
class ToolsMenuView(View):
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
DICE = ("New seed", FontAwesomeIconConstants.DICE)
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
EXPLORER = "Address Explorer"
def run(self):
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
DICE = ("New seed", FontAwesomeIconConstants.DICE)
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
EXPLORER = "Address Explorer"
button_data = [IMAGE, DICE, KEYBOARD, EXPLORER]
screen = ButtonListScreen(
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER]
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Tools",
is_button_text_centered=False,
button_data=button_data
)
selected_menu_num = screen.display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == IMAGE:
elif button_data[selected_menu_num] == self.IMAGE:
return Destination(ToolsImageEntropyLivePreviewView)
elif button_data[selected_menu_num] == DICE:
elif button_data[selected_menu_num] == self.DICE:
return Destination(ToolsDiceEntropyMnemonicLengthView)
elif button_data[selected_menu_num] == KEYBOARD:
elif button_data[selected_menu_num] == self.KEYBOARD:
return Destination(ToolsCalcFinalWordNumWordsView)
elif button_data[selected_menu_num] == EXPLORER:
elif button_data[selected_menu_num] == self.EXPLORER:
return Destination(ToolsAddressExplorerSelectSourceView)
@@ -232,28 +232,30 @@ class ToolsDiceEntropyEntryView(View):
Calc final word Views
****************************************************************************"""
class ToolsCalcFinalWordNumWordsView(View):
TWELVE = "12 words"
TWENTY_FOUR = "24 words"
def run(self):
TWELVE = "12 words"
TWENTY_FOUR = "24 words"
button_data = [TWELVE, TWENTY_FOUR]
selected_menu_num = ButtonListScreen(
button_data = [self.TWELVE, self.TWENTY_FOUR]
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Mnemonic Length",
is_bottom_list=True,
is_button_text_centered=True,
button_data=button_data,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == TWELVE:
elif button_data[selected_menu_num] == self.TWELVE:
self.controller.storage.init_pending_mnemonic(12)
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
elif button_data[selected_menu_num] == TWENTY_FOUR:
elif button_data[selected_menu_num] == self.TWENTY_FOUR:
self.controller.storage.init_pending_mnemonic(24)
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
@@ -423,29 +425,27 @@ class ToolsCalcFinalWordDoneView(View):
Address Explorer Views
****************************************************************************"""
class ToolsAddressExplorerSelectSourceView(View):
def run(self):
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
button_data = []
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
def run(self):
seeds = self.controller.storage.seeds
button_data = []
for seed in seeds:
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
button_data.append(SCAN_SEED)
button_data.append(SCAN_DESCRIPTOR)
button_data.append(TYPE_12WORD)
button_data.append(TYPE_24WORD)
selected_menu_num = ButtonListScreen(
button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD]
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Address Explorer",
button_data=button_data,
is_button_text_centered=False,
is_bottom_list=True,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -465,13 +465,13 @@ class ToolsAddressExplorerSelectSourceView(View):
)
)
elif button_data[selected_menu_num] in [SCAN_SEED, SCAN_DESCRIPTOR]:
elif button_data[selected_menu_num] in [self.SCAN_SEED, self.SCAN_DESCRIPTOR]:
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
if button_data[selected_menu_num] == TYPE_12WORD:
if button_data[selected_menu_num] == self.TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
else:
self.controller.storage.init_pending_mnemonic(num_words=24)
@@ -480,6 +480,10 @@ class ToolsAddressExplorerSelectSourceView(View):
class ToolsAddressExplorerAddressTypeView(View):
RECEIVE = "Receive Addresses"
CHANGE = "Change Addresses"
def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None):
"""
If the explorer source is a seed, `seed_num` and `script_type` must be
@@ -528,29 +532,28 @@ class ToolsAddressExplorerAddressTypeView(View):
def run(self):
data = self.controller.address_explorer_data
RECEIVE = "Receive Addresses"
CHANGE = "Change Addresses"
button_data = [RECEIVE, CHANGE]
wallet_descriptor_display_name = None
if "wallet_descriptor" in data:
wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
script_type = data["script_type"] if "script_type" in data else None
selected_menu_num = ToolsAddressExplorerAddressTypeScreen(
button_data = [self.RECEIVE, self.CHANGE]
selected_menu_num = self.run_screen(
ToolsAddressExplorerAddressTypeScreen,
button_data=button_data,
fingerprint=self.seed.get_fingerprint() if self.seed_num is not None else None,
wallet_descriptor_display_name=wallet_descriptor_display_name,
script_type=script_type,
custom_derivation_path=self.custom_derivation,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] in [RECEIVE, CHANGE]:
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == CHANGE))
elif button_data[selected_menu_num] in [self.RECEIVE, self.CHANGE]:
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == self.CHANGE))
@@ -565,21 +568,23 @@ class ToolsAddressExplorerAddressListView(View):
def run(self):
self.loading_screen = None
try:
addresses = []
button_data = []
data = self.controller.address_explorer_data
addrs_per_screen = 10
addr_storage_key = "receive_addrs"
if self.is_change:
addr_storage_key = "change_addrs"
addresses = []
button_data = []
data = self.controller.address_explorer_data
addrs_per_screen = 10
if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen:
# We already calculated this range addresses; just retrieve them
addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen]
addr_storage_key = "receive_addrs"
if self.is_change:
addr_storage_key = "change_addrs"
else:
if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen:
# We already calculated this range of addresses; just retrieve them
addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen]
else:
try:
from seedsigner.gui.screens.screen import LoadingScreenThread
self.loading_screen = LoadingScreenThread(text="Calculating addrs...")
self.loading_screen.start()
@@ -608,35 +613,36 @@ class ToolsAddressExplorerAddressListView(View):
else:
raise Exception("Single sig descriptors not yet supported")
for i, address in enumerate(addresses):
cur_index = i + self.start_index
if cur_index < 10:
end_digits = -6
elif cur_index < 100:
end_digits = -5
else:
end_digits = -4
button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}")
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
screen = ButtonListScreen(
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
button_data=button_data,
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4,
is_button_text_centered=False,
is_bottom_list=True,
selected_button=self.selected_button_index,
scroll_y_initial_offset=self.initial_scroll,
)
finally:
# Everything is set. Stop the loading screen
if self.loading_screen:
finally:
# Everything is set. Stop the loading screen
self.loading_screen.stop()
selected_menu_num = screen.display()
for i, address in enumerate(addresses):
cur_index = i + self.start_index
# Adjust the trailing addr display length based on available room
# (the index number will push it out on each order of magnitude)
if cur_index < 10:
end_digits = -6
elif cur_index < 100:
end_digits = -5
else:
end_digits = -4
button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}")
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
selected_menu_num = self.run_screen(
ButtonListScreen,
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
button_data=button_data,
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4,
is_button_text_centered=False,
is_bottom_list=True,
selected_button=self.selected_button_index,
scroll_y_initial_offset=self.initial_scroll,
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -646,7 +652,7 @@ class ToolsAddressExplorerAddressListView(View):
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen))
# Preserve the list's current scroll so we can return to the same spot
initial_scroll = screen.buttons[0].scroll_y
initial_scroll = self.screen.buttons[0].scroll_y
index = selected_menu_num + self.start_index
return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index, parent_initial_scroll=initial_scroll), skip_current_view=True)
@@ -664,10 +670,12 @@ class ToolsAddressExplorerAddressView(View):
def run(self):
from seedsigner.gui.screens.screen import QRDisplayScreen
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address)
QRDisplayScreen(
self.run_screen(
QRDisplayScreen,
qr_encoder=qr_encoder,
).display()
)
# Exiting/Cancelling the QR display screen always returns to the list
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index, initial_scroll=self.parent_initial_scroll), skip_current_view=True)
+76 -48
View File
@@ -1,9 +1,9 @@
from dataclasses import dataclass
from typing import List
from typing import Type
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
from seedsigner.gui.components import FontAwesomeIconConstants
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON
from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
from seedsigner.models.threads import BaseThread
from seedsigner.models import Settings
@@ -56,14 +56,20 @@ class View:
self.canvas_width = self.renderer.canvas_width
self.canvas_height = self.renderer.canvas_height
self.buttons = self.controller.buttons
self.screen = None
def run_screen(self, Screen_cls: Type[BaseScreen], **kwargs) -> int | str:
"""
Instantiates the provided Screen_cls and runs its interactive display.
Returns the user's input upon completion.
"""
self.screen = Screen_cls(**kwargs)
return self.screen.display()
def run(self, **kwargs):
if hasattr(self, "screen"):
self.screen.display()
else:
raise Exception("Must implement in the child class")
def run(self, **kwargs) -> 'Destination':
raise Exception("Must implement in the child class")
@@ -73,10 +79,10 @@ class Destination:
Basic struct to pass back to the Controller to tell it which View the user should
be presented with next.
"""
View_cls: View # The target View to route to
view_args: dict = None # The input args required to instantiate the target View
skip_current_view: bool = False # The current View is just forwarding; omit current View from history
clear_history: bool = False # Optionally clears the back_stack to prevent "back"
View_cls: Type[View] # The target View to route to
view_args: dict = None # The input args required to instantiate the target View
skip_current_view: bool = False # The current View is just forwarding; omit current View from history
clear_history: bool = False # Optionally clears the back_stack to prevent "back"
def __repr__(self):
@@ -93,12 +99,22 @@ class Destination:
return out
def run(self):
def _instantiate_view(self):
if not self.view_args:
# Can't unpack (**) None so we replace with an empty dict
self.view_args = {}
# Instantiate the `View_cls` and run() it with the `view_args` dict
return self.View_cls(**self.view_args).run()
# Instantiate the `View_cls` with the `view_args` dict
self.view = self.View_cls(**self.view_args)
def _run_view(self):
return self.view.run()
def run(self):
self._instantiate_view()
return self._run_view()
def __eq__(self, obj):
@@ -121,53 +137,63 @@ class Destination:
#
#########################################################################################
class MainMenuView(View):
def run(self):
from .seed_views import SeedsMenuView
from .settings_views import SettingsMenuView
from .scan_views import ScanView
from .tools_views import ToolsMenuView
from seedsigner.gui.screens import LargeButtonScreen
menu_items = [
(("Scan", FontAwesomeIconConstants.QRCODE), ScanView),
(("Seeds", FontAwesomeIconConstants.KEY), SeedsMenuView),
(("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH), ToolsMenuView),
(("Settings", FontAwesomeIconConstants.GEAR), SettingsMenuView),
]
SCAN = ("Scan", FontAwesomeIconConstants.QRCODE)
SEEDS = ("Seeds", FontAwesomeIconConstants.KEY)
TOOLS = ("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH)
SETTINGS = ("Settings", FontAwesomeIconConstants.GEAR)
screen = LargeButtonScreen(
def run(self):
button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS]
selected_menu_num = self.run_screen(
LargeButtonScreen,
title="Home",
title_font_size=26,
button_data=[entry[0] for entry in menu_items],
button_data=button_data,
show_back_button=False,
show_power_button=True,
)
selected_menu_num = screen.display()
if selected_menu_num == RET_CODE__POWER_BUTTON:
return Destination(PowerOptionsView)
return Destination(menu_items[selected_menu_num][1])
if button_data[selected_menu_num] == self.SCAN:
from .scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] == self.SEEDS:
from .seed_views import SeedsMenuView
return Destination(SeedsMenuView)
elif button_data[selected_menu_num] == self.TOOLS:
from .tools_views import ToolsMenuView
return Destination(ToolsMenuView)
elif button_data[selected_menu_num] == self.SETTINGS:
from .settings_views import SettingsMenuView
return Destination(SettingsMenuView)
class PowerOptionsView(View):
RESET = ("Restart", FontAwesomeIconConstants.ROTATE_RIGHT)
POWER_OFF = ("Power Off", FontAwesomeIconConstants.POWER_OFF)
def run(self):
RESET = ("Restart", FontAwesomeIconConstants.ROTATE_RIGHT)
POWER_OFF = ("Power Off", FontAwesomeIconConstants.POWER_OFF)
button_data = [RESET, POWER_OFF]
selected_menu_num = LargeButtonScreen(
button_data = [self.RESET, self.POWER_OFF]
selected_menu_num = self.run_screen(
LargeButtonScreen,
title="Reset / Power",
show_back_button=True,
button_data=button_data
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == RESET:
elif button_data[selected_menu_num] == self.RESET:
return Destination(RestartView)
elif button_data[selected_menu_num] == POWER_OFF:
elif button_data[selected_menu_num] == self.POWER_OFF:
return Destination(PowerOffView)
@@ -176,7 +202,7 @@ class RestartView(View):
def run(self):
thread = RestartView.DoResetThread()
thread.start()
ResetScreen().display()
self.run_screen(ResetScreen)
class DoResetThread(BaseThread):
@@ -200,12 +226,12 @@ class RestartView(View):
class PowerOffView(View):
def run(self):
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
PowerOffNotRequiredScreen().display()
self.run_screen(PowerOffNotRequiredScreen)
return Destination(BackStackView)
else:
thread = PowerOffView.PowerOffThread()
thread.start()
PowerOffScreen().display()
self.run_screen(PowerOffScreen)
class PowerOffThread(BaseThread):
@@ -223,30 +249,32 @@ class NotYetImplementedView(View):
Temporary View to use during dev.
"""
def run(self):
WarningScreen(
self.run_screen(
WarningScreen,
title="Work In Progress",
status_headline="Not Yet Implemented",
text="This is still on our to-do list!",
button_data=["Back to Main Menu"],
).display()
)
return Destination(MainMenuView)
class UnhandledExceptionView(View):
def __init__(self, error: List[str]):
def __init__(self, error: list[str]):
self.error = error
def run(self):
DireWarningScreen(
self.run_screen(
DireWarningScreen,
title="System Error",
status_headline=self.error[0],
text=self.error[1] + "\n" + self.error[2],
button_data=["OK"],
show_back_button=False,
allow_text_overflow=True, # Fit what we can, let the rest go off the edges
).display()
)
return Destination(MainMenuView, clear_history=True)
+8 -2
View File
@@ -1,7 +1,13 @@
# Running Tests
You must install the test-runner dependencies:
```
The tests are designed to be run on non-Raspi hardware.
On your testing machine you'll have to install:
```bash
# general dependencies
pip3 install -r requirements.txt
# test suite dependencies
pip3 install -r tests/requirements.txt
```
+208
View File
@@ -0,0 +1,208 @@
import sys
from dataclasses import dataclass
from mock import MagicMock, patch
from typing import Callable
# 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.views.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, FlowBasedTestException, StopFlowBasedTest
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
from seedsigner.models import Settings
from seedsigner.views.view import Destination, MainMenuView, View
class BaseTest:
@classmethod
def setup_class(cls):
# Ensure there are no on-disk artifacts after running tests.
Settings.SETTINGS_FILENAME = "settings-test.json"
# Mock out the loading screen so it can't spawn. View classes must import locally!
patch('seedsigner.gui.screens.screen.LoadingScreenThread').start()
@classmethod
def teardown_class(cls):
pass
@classmethod
def reset_settings(cls):
""" Wipe and re-initialize the Settings singleton """
Settings._instance = None
BaseTest.remove_settings()
@classmethod
def remove_settings(cls):
""" If settings were written to disk, delete """
import os
try:
if os.path.exists(Settings.SETTINGS_FILENAME):
os.remove(Settings.SETTINGS_FILENAME)
except:
print(f"{Settings.SETTINGS_FILENAME} could not be removed")
@classmethod
def reset_controller(cls):
""" Wipe and re-initialize the Controller singleton """
Controller._instance = None
Controller.configure_instance()
def setup_method(self):
""" Guarantee a clean/default Controller and Settings state for each test case """
BaseTest.reset_controller()
BaseTest.reset_settings()
self.controller = Controller.get_instance()
self.settings = Settings.get_instance()
def teardown_method(self):
BaseTest.remove_settings()
@dataclass
class FlowStep:
"""
Trivial helper class to express FlowTest sequences below.
* expected_view: verify that the current step in the sequence instantiates the right View.
* before_run: function that takes a View instance as an arg and modifies it before running the View.
* screen_return_value: mocked Screen interaction result: raw return value as if from the Screen.
* button_data_selection: mocked Screen interaction result: the View.button_data value of the desired option.
* is_redirect: expects the Destination to specify `skip_current_view=True`.
"""
expected_view: type[View] = None
before_run: Callable[[View], None] = None
screen_return_value: int | str = None
button_data_selection: str | tuple = None
is_redirect: bool = False
def __post_init__(self):
if self.screen_return_value is not None and self.button_data_selection is not None:
raise Exception("Can't specify both `screen_return_value` and `button_data_selection`")
class FlowTestInvalidButtonDataSelectionException(FlowBasedTestException):
""" The FlowStep's button_data_selection value was not found in the View's button_data """
pass
class FlowTestUnexpectedViewException(FlowBasedTestException):
""" The Controller's current View did not match the expected View in the FlowStep sequence """
pass
class FlowTestRunScreenNotExecutedException(FlowBasedTestException):
""" The View's run_screen() method was not called but the FlowStep expected it to need user input """
pass
class FlowTest(BaseTest):
""" Base class for any tests that do flow-based testing """
def stop_test(self):
""" Raise our special Exception to stop the test """
raise StopFlowBasedTest()
def run_sequence(self, sequence: list[FlowStep], initial_destination_view_args: dict = None) -> None:
"""
Run a pre-set sequence of Views w/manually-specified return values in order to test
the Controller's flow control logic and the routing from View to View.
"""
with patch("seedsigner.views.view.Destination._run_view", autospec=True) as mock_run_view:
with patch("seedsigner.views.view.View.run_screen", autospec=True) as mock_run_screen:
def run_view(destination: Destination, *args, **kwargs):
""" Replaces Destination._run_view() """
if len(sequence) == 0:
self.stop_test()
cur_flow_step = sequence[0]
# Verify that the View class specified in the test sequence matches the
# View class that is being run.
if destination.View_cls != cur_flow_step.expected_view:
raise FlowTestUnexpectedViewException(f"Expected {cur_flow_step.expected_view}, got {destination.View_cls}")
# Run the optional pre-run function to modify the View.
if cur_flow_step.before_run:
cur_flow_step.before_run(destination.view)
if cur_flow_step.is_redirect:
# The current View is going to auto-redirect without calling run_screen(),
# so we need to remove the current step from the sequence before the
# View.run() call below.
sequence.pop(0)
# Some Views reach into their Screen's variables directly (e.g.
# Screen.buttons to preserve the scroll position), so we need to mock out the
# Screen instance that is created by the View.
destination.view.screen = MagicMock()
prev_mock_run_screen_call_count = mock_run_screen.call_count
# Run the View (with our mocked run_screen) and get the next Destination that results
destination = destination.view.run()
if (cur_flow_step.button_data_selection or cur_flow_step.screen_return_value is not None) and mock_run_screen.call_count == prev_mock_run_screen_call_count:
# The FlowStep was expecting some kind of user interaction, but the View
# never called run_screen().
raise FlowTestRunScreenNotExecutedException(f"View.run_screen() was not run for {destination.View_cls.__name__}")
return destination
def run_screen(view: View, *args, **kwargs):
""" Replaces View.run_screen() """
# Return the return value specified in the test sequence and
# remove the completed test step from the sequence.
flow_step = sequence.pop(0)
if flow_step.button_data_selection:
# We're mocking out the View.run_screen() method, so we'll get all of the
# input args that are normally passed into the Screen.run() method,
# including the button_data kwarg.
if "button_data" in kwargs:
if flow_step.button_data_selection not in kwargs.get("button_data") and flow_step.button_data_selection not in [RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON]:
raise FlowTestInvalidButtonDataSelectionException(f"'{flow_step.button_data_selection}' not found in button_data: {kwargs.get('button_data')}")
return kwargs.get("button_data").index(flow_step.button_data_selection)
else:
raise Exception(f"Can't specify `FlowStep.button_data_selection` if `button_data` isn't a kwarg in {view.__class__.__name__}'s run_screen()")
elif type(flow_step.screen_return_value) in [StopFlowBasedTest, FlowBasedTestException]:
raise flow_step.screen_return_value
return flow_step.screen_return_value
# Mock out the Destination._run_view() method so we can verify the View class
# that is specified in the test sequence and then run the View.
mock_run_view.side_effect = run_view
# Mock out the View.run_screen() method so we can provide the
# return value that is specified in the test sequence.
mock_run_screen.side_effect = run_screen
# Start the Controller with the first View_cls specified in the test sequence
if sequence[0].expected_view != MainMenuView:
initial_destination = Destination(sequence[0].expected_view, view_args=initial_destination_view_args)
else:
initial_destination = None
# Start the Controller and run the sequence
Controller.get_instance().start(initial_destination=initial_destination)
+91 -93
View File
@@ -1,117 +1,115 @@
import configparser
import pytest
from mock import MagicMock
from seedsigner.hardware.microsd import MicroSD
# Must import this before the Controller
from base import BaseTest
from seedsigner.controller import Controller
from seedsigner.models.settings_definition import SettingsConstants
@pytest.fixture()
def reset_controller():
"""fixture to setup, then yield to run test, then tear down"""
class TestController(BaseTest):
# setup
Controller.configure_instance(disable_hardware=True)
def test_reset_controller(self):
""" The reset_controller util should completely reset the Controller singleton """
controller = Controller.get_instance()
controller.address_explorer_data = "foo"
# yield to run a single test
yield
# tear down
MicroSD._instance = None
Controller._instance = None
def test_singleton_init_fails(reset_controller):
""" The Controller should not allow any code to instantiate it via Controller() """
with pytest.raises(Exception):
c = Controller()
BaseTest.reset_controller()
controller = Controller.get_instance()
assert controller.address_explorer_data is None
def test_singleton_get_instance_preserves_state(reset_controller):
""" Changes to the Controller singleton should be preserved across calls to get_instance() """
# Initialize the instance and verify that it read the config settings
controller = Controller.get_instance()
assert controller.unverified_address is None
# Change a value in the instance...
controller.unverified_address = "123abc"
# ...get a new copy of the instance and confirm change
controller = Controller.get_instance()
assert controller.unverified_address == "123abc"
def test_singleton_init_fails(self):
""" The Controller should not allow any code to instantiate it via Controller() """
with pytest.raises(Exception):
c = Controller()
def test_handle_exception(reset_controller):
""" Handle exceptions that get caught by the controller """
def test_handle_exception(reset_controller):
""" Handle exceptions that get caught by the controller """
def process_exception_asserting_valid_error(exception_type, exception_msg=None):
"""
Exceptions caught by the controller are forwarded to the
UnhandledExceptionView with view_args["error"] being a list
of three strings, ie: [exception_type, line_info, exception_msg]
"""
try:
def process_exception_asserting_valid_error(exception_type, exception_msg=None):
"""
Exceptions caught by the controller are forwarded to the
UnhandledExceptionView with view_args["error"] being a list
of three strings, ie: [exception_type, line_info, exception_msg]
"""
try:
if exception_msg:
raise exception_type(exception_msg)
else:
raise exception_type()
except Exception as e:
error = controller.handle_exception(e).view_args["error"]
# assert that error structure is valid
assert len(error) == 3
assert error[0] in str(exception_type)
assert type(error[1]) == str
if exception_msg:
raise exception_type(exception_msg)
assert exception_msg in error[2]
else:
raise exception_type()
except Exception as e:
error = controller.handle_exception(e).view_args["error"]
assert error[2] == ""
# assert that error structure is valid
assert len(error) == 3
assert error[0] in str(exception_type)
assert type(error[1]) == str
if exception_msg:
assert exception_msg in error[2]
else:
assert error[2] == ""
# Initialize the controller
controller = Controller.get_instance()
# Initialize the controller
controller = Controller.get_instance()
exception_tests = [
# exceptions with an exception_msg
(Exception, "foo"),
(KeyError, "key not found"),
# exceptions without an exception_msg
(Exception, ""),
(Exception, None),
]
for exception_type, exception_msg in exception_tests:
process_exception_asserting_valid_error(exception_type, exception_msg)
exception_tests = [
# exceptions with an exception_msg
(Exception, "foo"),
(KeyError, "key not found"),
# exceptions without an exception_msg
(Exception, ""),
(Exception, None),
]
for exception_type, exception_msg in exception_tests:
process_exception_asserting_valid_error(exception_type, exception_msg)
def test_missing_settings_get_defaults(reset_controller):
""" Should gracefully handle all missing fields from `settings.json` """
def test_singleton_get_instance_preserves_state(self):
""" Changes to the Controller singleton should be preserved across calls to get_instance() """
controller = Controller.get_instance()
# Initialize the instance and verify that it read the config settings
controller = Controller.get_instance()
assert controller.unverified_address is None
# Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD
# Change a value in the instance...
controller.unverified_address = "123abc"
# Advanced Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__0
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
# ...get a new copy of the instance and confirm change
controller = Controller.get_instance()
assert controller.unverified_address == "123abc"
# Hidden Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 189
def test_missing_settings_get_defaults(self):
""" Should gracefully handle all missing fields from `settings.json` """
controller = Controller.get_instance()
# Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD
# Advanced Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__0
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
# Hidden Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 189
+131
View File
@@ -0,0 +1,131 @@
import pytest
# Must import test base before the Controller
from base import FlowTest, FlowStep, FlowTestUnexpectedViewException, FlowTestInvalidButtonDataSelectionException, FlowTestRunScreenNotExecutedException
from seedsigner.controller import Controller
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView
from seedsigner.views.view import MainMenuView, PowerOptionsView, UnhandledExceptionView
from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView
class TestFlowTest(FlowTest):
def test_simple_flow(self):
"""
Basic test to ensure the FlowTest can flow through a sequence of Views and
terminate via the StopControllerCommand.
"""
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD),
FlowStep(ToolsCalcFinalWordNumWordsView, button_data_selection=ToolsCalcFinalWordNumWordsView.TWELVE),
FlowStep(SeedMnemonicEntryView),
])
def test_FlowTestUnexpectedViewException(self):
"""
Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the next
View in the sequence is not the expected View.
"""
with pytest.raises(FlowTestUnexpectedViewException):
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=RET_CODE__POWER_BUTTON),
FlowStep(ToolsMenuView), # <-- Wrong target View! Should raise an AssertionError.
])
def test_FlowTestInvalidButtonDataSelectionException(self):
"""
Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the
specified button_data_selection in invalid.
"""
with pytest.raises(FlowTestInvalidButtonDataSelectionException):
self.run_sequence([
FlowStep(MainMenuView, button_data_selection="this is not a real button option!"),
])
def test_FlowTestRunScreenNotExecutedException(self):
"""
Ensure that the FlowTest will raise a FlowTestRunScreenNotExecutedException if the next
View in the sequence doesn't call its View.run_screen().
"""
# Disable dire warnings so that the SeedWordsWarningView won't execute its run_screen()
self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED)
self.controller.storage.set_pending_seed(Seed(mnemonic=["bacon"] * 24))
self.controller.storage.finalize_pending_seed()
with pytest.raises(FlowTestRunScreenNotExecutedException):
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP),
FlowStep(SeedBackupView, button_data_selection=SeedBackupView.VIEW_WORDS),
FlowStep(SeedWordsWarningView, screen_return_value=0),
],
)
def test_before_run_executes(self):
"""
Ensure that the FlowTest can execute a function before running a View.
"""
# TODO
pass
def test_back_button_flow(self):
"""
Ensure that the FlowTest works correctly with the Controller and its BackStack.
"""
self.run_sequence([
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
FlowStep(PowerOptionsView, screen_return_value=RET_CODE__BACK_BUTTON),
FlowStep(MainMenuView),
])
def test_initial_destination(self):
"""
Ensure that the FlowTest can start from a View other than MainMenuView.
"""
# Don't have to start at the MainMenuView; can jump straight in
self.run_sequence([
FlowStep(ToolsCalcFinalWordNumWordsView),
])
# And again, but this time with a View that requires input view_args
self.reset_controller()
self.controller = Controller.get_instance()
# Load a seed into the Controller
seed = Seed(mnemonic=["abandon "* 11 + "about"])
self.controller.storage.set_pending_seed(seed)
self.controller.storage.finalize_pending_seed()
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP),
FlowStep(SeedBackupView),
]
)
def test_raise_exception_via_screen_return_value(self):
"""
Ensure that the FlowTest can raise an exception via the screen_return_value.
"""
# A generic Exception should be caught by the Controller and routed to the
# UnhandledExceptionView.
self.run_sequence([
FlowStep(MainMenuView, screen_return_value=Exception("Test exception")),
FlowStep(UnhandledExceptionView),
])
+56
View File
@@ -0,0 +1,56 @@
from base import FlowTest, FlowStep
from seedsigner.controller import Controller
from seedsigner.views.view import MainMenuView
from seedsigner.views import scan_views, seed_views, psbt_views
class TestPSBTFlows(FlowTest):
def test_scan_psbt_first_then_correct_seedqr_flow(self):
"""
Selecting "Scan" from the MainMenuView and scanning a PSBT should enter the PSBTSelectSeedView flow
when Scan a Seed is selected from PSBTSelectSeedView it should enter the ScanView flow
when a SeedQR is scanned it should enter the PSBTOverviewView flow
since the PSBT has change no warning is displayed and it should enter the PSBTMathView flow
since the PSBT is not a self transfer it should enter the PSBTAddressDetailsView flow
"""
def load_psbt_into_decoder(view: scan_views.ScanView):
"""
PSBT Tx and Wallet Details
- Single Sig Wallet P2WPKH (Native Segwit) with no passphrase
- Regtest c751dc07 m/84'/1'/0' tpubDDZBrnxMxbVzqt8EoEiABPxeKzFWma5pra5UEbg3Wst1hrwr6feuvcy7Sov7cpuYx94ypuy1PQ9NDNoQagFs37wGALzLb5Ei3FvyJWPPPKZ
- 2 Inputs
- 56,522,834 sats
- 1,990,245,069 sats
- 4 Outputs
- 1 Output to another wallet (bcrt1q7cw0wzy8g6mq5qvkpvhnk5gsps5ncy3srp0n2j) of 123,456 sats
- 3 Outputs change
- 3 outputs to emulate a fake mix to increase privacy
- Change addresses are index 1/7, 1/8, 1/9
- 1/7 address bcrt1q53j0xwuskuf5gnvynadh0hlazyy8srydlucrhg with amount 123,456 sats
- 1/8 address bcrt1q5gtw3zfp4cx67yk5q42q6j6rfza8aqcwpyyslv with amount 1,990,121,477 sats
- 1/9 address bcrt1q9rrg7399m43cn0yg4tz0v0ate89jgf2d6kpz7v with amount 56,399,242 sats
- Fee 272 sats
"""
view.decoder.add_data("cHNidP8BANgCAAAAAsTXZs3fz/dmGb6M80+jjvJZdYya+cw5bT/dGuhZFdSlAAAAAAD9////qo6xg/UZAvUkcbse1F+C9zbP/FeZNjThx7SCIn6eMCgBAAAAAP3///8EQOIBAAAAAAAWABSkZPM7kLcTRE2En1t33/0RCHgMjQXYnnYAAAAAFgAUKMaPRKXdY4m8iKrE9j+rycskJU1A4gEAAAAAABYAFPYc9wiHRrYKAZYLLztREAwpPBIwipVcAwAAAAAWABSiFuiJIa4NrxLUBVQNS0NIun6DDtoRAABPAQQ1h88DBcQGZIAAAAA+0J+jlNL3dpWwlnBi8Dx+Ipg4e6uvB3HdjzFPX7r9CAOOlAIxgII+/xCcj+XoEenKH7wj5s5wlu7Q7CCZWFLGLhA5Su0UVAAAgAEAAIAAAACAAAEA7QIAAAAEE6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0BAAAAAP3///8TqeNf9+e+fuFuQohFzHM1gU5J9sJ015ad3sV7/VRxDQMAAAAA/f///xOp41/3575+4W5CiEXMczWBTkn2wnTXlp3exXv9VHENBAAAAAD9////E6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0GAAAAAP3///8CUnheAwAAAAAWABRCfygPJ+Fjsx4BknYvvm3A3qKn2xJ/XQcAAAAAF6kU1I4TAst5nAj15ey7vwe5cM3OFq+HlhEAAAEBH1J4XgMAAAAAFgAUQn8oDyfhY7MeAZJ2L75twN6ip9sBAwQBAAAAIgYCo7sfm78RQY3B5n0ac/QF8VtMAzFnci+h5D1MtpgRY7oYOUrtFFQAAIABAACAAAAAgAEAAAAGAAAAAAEAcQIAAAABxY7wh0nsfJQfzWrD/9rN9BYsM+iOmPaO6I0ANFgO/PcAAAAAAP3///8CptiUAAAAAAAWABRIm4HhQY/TzOjeWSPRrbuJo9MlW826oHYAAAAAFgAU0z+0L2QSLGtyQTn8FhbCpcI7jbliAQAAAQEfzbqgdgAAAAAWABTTP7QvZBIsa3JBOfwWFsKlwjuNuQEDBAEAAAAiBgITHmebEANk81CraV4xZIpqkNjjw0tIvezl1Ism1NRH3Rg5Su0UVAAAgAEAAIAAAACAAQAAAAAAAAAAIgICuTT7WnuiUTpObjWnZFHzIeEvW9PTB+1LLVFNQJVFeIIYOUrtFFQAAIABAACAAAAAgAEAAAAHAAAAACICAk8f3hpc5C35chgSg+Pe2zZ9IhHREd4aKW2+yAMRIFeqGDlK7RRUAACAAQAAgAAAAIABAAAACQAAAAAAIgIDjt1CjvrnMMnjbmTNKUAYoKEDRbmKjNjbq+6Ppqj3bqQYOUrtFFQAAIABAACAAAAAgAEAAAAIAAAAAA==")
def load_seed_into_decoder(view: scan_views.ScanView):
view.decoder.add_data("080115060387063104071857067618681125136207731354")
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder),
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(psbt_views.PSBTOverviewView),
FlowStep(psbt_views.PSBTMathView),
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=psbt_views.PSBTAddressDetailsView.NEXT),
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
FlowStep(psbt_views.PSBTSignedQRDisplayView),
FlowStep(MainMenuView)
])
+290
View File
@@ -0,0 +1,290 @@
# Must import test base before the Controller
from base import BaseTest, FlowTest, FlowStep
from base import FlowTestUnexpectedViewException, FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
import pytest
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.seed import Seed
from seedsigner.views.view import 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")
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(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(MainMenuView, button_data_selection=MainMenuView.SEEDS),
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD),
]
# Now add each manual word entry step
for word in mnemonic:
sequence.append(
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)
)
# With the mnemonic completely entered, we land on the SeedFinalizeView
sequence += [
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView),
]
self.run_sequence(sequence)
# 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())
BaseTest.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(MainMenuView, button_data_selection=MainMenuView.SEEDS),
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD),
]
for word in mnemonic[:-1]:
sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word))
sequence += [
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zoo"), # But finish with an INVALID checksum word
FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.EDIT),
]
# Restarts from first word
for word in mnemonic[:-1]:
sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word))
sequence += [
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zebra"), # provide yet another invalid checksum word
FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD),
FlowStep(MainMenuView),
]
self.run_sequence(sequence)
def test_export_xpub_standard_flow(self):
"""
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
"""
def flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple):
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_tuple[1]),
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_tuple[1]),
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coord_tuple[1]),
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
FlowStep(MainMenuView),
]
)
# Load a finalized Seed into the Controller
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
self.controller.storage.finalize_pending_seed()
# these are lists of (constant_value, display_name) tuples
sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES
script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES
coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS
# enable non-defaults so they're available in views
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types])
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types])
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators])
# exhaustively test flows thru standard sig_types, script_types, and coordinators
for sig_tuple in sig_types:
for script_tuple in script_types:
for coord_tuple in coordinators:
# skip custom derivation
if script_tuple[0] == SettingsConstants.CUSTOM_DERIVATION:
continue
# skip multisig taproot
elif sig_tuple[0] == SettingsConstants.MULTISIG and script_tuple[0] == SettingsConstants.TAPROOT:
continue
else:
print('\n\ntest_standard_xpubs(%s, %s, %s)' % (sig_tuple, script_tuple, coord_tuple))
flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple)
def test_export_xpub_disabled_not_available_flow(self):
"""
If sig_type/script_type/coordinator disabled, then these options are not available
"""
# Load a finalized Seed into the Controller
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
self.controller.storage.finalize_pending_seed()
# these are lists of (constant_value, display_name) tuples
sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES
script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES
coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS
# these are the disabled types that we will be testing
disabled_sig = SettingsConstants.MULTISIG
disabled_script = SettingsConstants.TAPROOT
disabled_coord = SettingsConstants.COORDINATOR__NUNCHUK
# enable all but our target disabled type
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types if x!=disabled_sig])
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types if x!=disabled_script])
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators if x!=disabled_coord])
# test that multisig is not an option via exception raised when redirected to next step instead of having a choice
with pytest.raises(FlowTestRunScreenNotExecutedException) as e:
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=disabled_sig),
]
)
# test that taproot is not an option via exception raised when choice is taproot
with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e:
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=disabled_script),
]
)
# test that nunchuk is not an option via exception raised when choice is nunchuk
with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e:
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=disabled_coord),
]
)
def test_export_xpub_custom_derivation_flow(self):
"""
Export XPUB flow for custom derivation finishes at MainMenuView
"""
# Load a finalized Seed into the Controller
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
self.controller.storage.finalize_pending_seed()
# enable custom derivation script_type setting (plus at least one more for a choice)
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [
SettingsConstants.NATIVE_SEGWIT,
SettingsConstants.NESTED_SEGWIT,
SettingsConstants.CUSTOM_DERIVATION
])
# get display names to access button choices in the views (ugh: hardcoding, is there a better way?)
sig_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SIG_TYPES)[0] # single sig
script_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2] # custom derivation
coordinator = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3] # specter
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_type),
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_type),
FlowStep(seed_views.SeedExportXpubCustomDerivationView, screen_return_value="m/0'/0'"),
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coordinator),
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
FlowStep(MainMenuView),
]
)
def test_export_xpub_skip_non_option_flow(self):
"""
Export XPUB flows w/o user choices when no other options for sig_types, script_types, and/or coordinators
"""
# Load a finalized Seed into the Controller
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
self.controller.storage.finalize_pending_seed()
# exclusively set only one choice for each of sig_types, script_types and coordinators
self.settings.update({
SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG,
SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT,
SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
}, disable_missing_entries=False)
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubCoordinatorView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
FlowStep(MainMenuView),
]
)
def test_discard_seed_flow(self):
"""
Selecting "Discard Seed" from the SeedOptionsView should enter the Discard Seed flow and
remove the in-memory seed from the Controller.
"""
# Load a finalized Seed into the Controller
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
self.controller.storage.finalize_pending_seed()
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.DISCARD),
FlowStep(seed_views.SeedDiscardView, button_data_selection=seed_views.SeedDiscardView.DISCARD),
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
FlowStep(seed_views.LoadSeedView),
]
)
+70
View File
@@ -0,0 +1,70 @@
import os
# Must import test base before the Controller
from base import FlowTest, FlowStep
from seedsigner.models import SettingsDefinition
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
from seedsigner.views.view import MainMenuView
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_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
# No settings file should exist before we enable persistent settings
assert os.path.exists(Settings.SETTINGS_FILENAME) == False
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED)),
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
FlowStep(settings_views.SettingsMenuView),
])
# Settings file should now exist
assert os.path.exists(Settings.SETTINGS_FILENAME) == True
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)
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit
FlowStep(settings_views.SettingsMenuView),
])
def test_io_test(self):
""" Basic flow from MainMenuView to I/O Test View """
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.IO_TEST),
FlowStep(settings_views.IOTestView),
FlowStep(settings_views.SettingsMenuView),
])
def test_donate(self):
""" Basic flow from MainMenuView to Donate View """
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.DONATE),
FlowStep(settings_views.DonateView),
FlowStep(settings_views.SettingsMenuView),
])
+72
View File
@@ -0,0 +1,72 @@
# Must import test base before the Controller
from base import FlowTest, FlowStep
from seedsigner.controller import Controller
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from seedsigner.views.view import MainMenuView
from seedsigner.views import scan_views, seed_views, tools_views
class TestToolsFlows(FlowTest):
def test_addressexplorer_flow(self):
"""
Test the simplest AddressExplorer flow when a seed is already loaded.
"""
controller = Controller.get_instance()
seed = Seed(mnemonic=["abandon "* 11 + "about"])
controller.storage.set_pending_seed(seed)
controller.storage.finalize_pending_seed()
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, screen_return_value=0), # ret 1st onboard seed
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT)),
FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE),
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=10), # ret NEXT page of addrs
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=4), # ret a specific addr from the list
FlowStep(tools_views.ToolsAddressExplorerAddressView), # runs until dismissed; no ret value
FlowStep(tools_views.ToolsAddressExplorerAddressListView),
])
def test_addressexplorer_loadseed_sideflow(self):
"""
Finalizing a seed during the Address Explorer flow should return to the next
Address Explorer step upon completion.
"""
controller = Controller.get_instance()
def load_seed_into_decoder(view: scan_views.ScanView):
view.decoder.add_data("0000" * 11 + "0003")
# Finalize the new seed w/out passphrase
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView),
])
assert controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER
# Reset
controller.storage.seeds.clear()
controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
# Finalize the new seed w/passphrase
self.run_sequence(
sequence=[
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="mypassphrase"),
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView),
]
)
+74
View File
@@ -0,0 +1,74 @@
from mock import patch
# Must import test base before the Controller
from base import FlowTest, FlowStep
from seedsigner.gui.screens.screen import RET_CODE__POWER_BUTTON
from seedsigner.models.settings import Settings
from seedsigner.views.tools_views import ToolsCalcFinalWordNumWordsView, ToolsMenuView
from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View
class TestViewFlows(FlowTest):
def test_restart_flow(self):
"""
Basic flow from MainMenuView to RestartView
"""
with patch('seedsigner.views.view.RestartView.DoResetThread'):
self.run_sequence([
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.RESET),
FlowStep(RestartView),
])
def test_power_off_flow(self):
"""
Basic flow from MainMenuView to PowerOffView
"""
with patch('seedsigner.views.view.PowerOffView.PowerOffThread'):
self.run_sequence([
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
FlowStep(PowerOffView),
])
# And again, but this time as if we were in the SeedSigner OS
Settings.HOSTNAME = Settings.SEEDSIGNER_OS
self.run_sequence([
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
FlowStep(PowerOffView), # returns BackStackView
FlowStep(PowerOptionsView),
])
def test_not_yet_implemented_flow(self):
"""
Run an incomplete View that returns None and ensure that we get the NotYetImplementedView
"""
class IncompleteView(View):
def run(self):
self.run_screen(None)
return None
self.run_sequence([
FlowStep(IncompleteView),
FlowStep(NotYetImplementedView),
FlowStep(MainMenuView),
])
def test_unhandled_exception_flow(self):
"""
Basic flow from any arbitrary View to the UnhandledExceptionView
"""
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD),
FlowStep(ToolsCalcFinalWordNumWordsView, screen_return_value=Exception("Test exception")), # <-- force an exception
FlowStep(UnhandledExceptionView),
FlowStep(MainMenuView),
])
+16
View File
@@ -0,0 +1,16 @@
from base import BaseTest
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
class TestSettings(BaseTest):
def test_reset_settings(self):
""" BaseTest.reset_settings() should wipe out any previous Settings changes """
settings = Settings.get_instance()
settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED)
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED
BaseTest.reset_settings()
settings = Settings.get_instance()
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED