diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 670318484..29b4213ed 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -252,6 +252,11 @@ class LayoutContent(UnstructuredJSONReader): text_lines = flow_page["text"] return "".join(text_lines) + # Looking for any "text": "something" values + text_values = self.find_values_by_key("text", only_type=str) + if text_values: + return "\n".join(text_values) + # Default when not finding anything return self.main_component() diff --git a/tests/common.py b/tests/common.py index 56832be53..c6c83e13d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -163,15 +163,14 @@ def recovery_enter_shares_tt( """ word_count = len(shares[0].split(" ")) - # Homescreen - proceed to word number selection - yield - debug.press_yes() # Input word number br = yield assert br.code == ButtonRequestType.MnemonicWordCount + assert "number of words" in debug.wait_layout().text_content() debug.input(str(word_count)) # Homescreen - proceed to share entry yield + assert "Enter any share" in debug.wait_layout().text_content() debug.press_yes() # Enter shares for share in shares: @@ -220,9 +219,11 @@ def recovery_enter_shares_tr( # Homescreen - proceed to word number selection yield + assert "number of words" in debug.wait_layout().text_content() debug.press_yes() # Input word number br = yield + assert "NUMBER OF WORDS" in debug.wait_layout().title() assert br.code == ButtonRequestType.MnemonicWordCount debug.input(str(word_count)) # Homescreen - proceed to share entry @@ -233,9 +234,10 @@ def recovery_enter_shares_tr( debug.press_yes() # Enter shares - for share in shares: + for index, share in enumerate(shares): br = yield assert br.code == ButtonRequestType.MnemonicInput + assert "MnemonicKeyboard" in debug.wait_layout().all_components() # Enter mnemonic words for word in share.split(" "): diff --git a/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py b/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py index 9cd05336f..f5be6efc1 100644 --- a/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py +++ b/tests/device_tests/reset_recovery/test_recovery_slip39_basic_dryrun.py @@ -39,7 +39,7 @@ INVALID_SHARES_20_2of3 = [ @pytest.mark.setup_client(mnemonic=SHARES_20_2of3[0:2]) def test_2of3_dryrun(client: Client): with client: - IF = InputFlowSlip39BasicRecovery(client, SHARES_20_2of3[1:3]) + IF = InputFlowSlip39BasicRecovery(client, SHARES_20_2of3[1:3], dry_run=True) client.set_input_flow(IF.get()) ret = device.recover( client, @@ -62,7 +62,7 @@ def test_2of3_invalid_seed_dryrun(client: Client): with client, pytest.raises( TrezorFailure, match=r"The seed does not match the one in the device" ): - IF = InputFlowSlip39BasicRecovery(client, INVALID_SHARES_20_2of3) + IF = InputFlowSlip39BasicRecovery(client, INVALID_SHARES_20_2of3, dry_run=True) client.set_input_flow(IF.get()) device.recover( client, diff --git a/tests/input_flows.py b/tests/input_flows.py index af7309508..b359823b1 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -49,7 +49,6 @@ class InputFlowBase: def __init__(self, client: Client): self.client = client self.debug: DebugLink = client.debug - self.layout = client.debug.wait_layout def model(self) -> Optional[str]: return self.client.features.model @@ -75,6 +74,18 @@ class InputFlowBase: """Special for TR""" raise NotImplementedError + def text_content(self) -> str: + return self.debug.wait_layout().text_content() + + def main_component(self) -> str: + return self.debug.wait_layout().main_component() + + def all_components(self) -> list[str]: + return self.debug.wait_layout().all_components() + + def title(self) -> list[str]: + return self.debug.wait_layout().title() + class InputFlowSetupDevicePINWIpeCode(InputFlowBase): def __init__(self, client: Client, pin: str, wipe_code: str): @@ -121,11 +132,14 @@ class InputFlowNewCodeMismatch(InputFlowBase): def input_two_different_pins(): yield # enter new PIN/wipe_code + assert "PinKeyboard" in self.all_components() self.debug.input(self.first_code) if self.debug.model == "R": yield # Please re-enter PIN to confirm + assert "re-enter PIN" in self.text_content() self.debug.press_yes() yield # enter new PIN/wipe_code again (but different) + assert "PinKeyboard" in self.all_components() self.debug.input(self.second_code) yield from input_two_different_pins() @@ -159,13 +173,16 @@ class InputFlowCodeChangeFail(InputFlowBase): self.debug.input(self.current_pin) yield # enter new pin + assert "PinKeyboard" in self.all_components() self.debug.input(self.new_pin_1) if self.debug.model == "R": yield # Please re-enter PIN to confirm + assert "re-enter PIN" in self.text_content() self.debug.press_yes() yield # enter new pin again (but different) + assert "PinKeyboard" in self.all_components() self.debug.input(self.new_pin_2) yield # PIN mismatch @@ -321,7 +338,7 @@ class InputFlowShowMultisigXPUBs(InputFlowBase): assert layout.text_content().replace(" ", "") == self.address self.debug.click(buttons.CORNER_BUTTON) - assert "Qr" in self.debug.wait_layout().all_components() + assert "Qr" in self.all_components() layout = self.debug.swipe_left(wait=True) # address details @@ -353,7 +370,7 @@ class InputFlowShowMultisigXPUBs(InputFlowBase): assert layout.text_content().replace(" ", "") == self.address self.debug.press_right() - assert "Qr" in self.debug.wait_layout().all_components() + assert "Qr" in self.all_components() layout = self.debug.press_right(wait=True) # address details @@ -396,14 +413,14 @@ class InputFlowPaymentRequestDetails(InputFlowBase): self.debug.press_info() yield # confirm first output - assert self.outputs[0].address[:16] in self.layout().text_content() # type: ignore + assert self.outputs[0].address[:16] in self.text_content() # type: ignore self.debug.press_yes() yield # confirm first output self.debug.wait_layout() self.debug.press_yes() yield # confirm second output - assert self.outputs[1].address[:16] in self.layout().text_content() # type: ignore + assert self.outputs[1].address[:16] in self.text_content() # type: ignore self.debug.press_yes() yield # confirm second output self.debug.wait_layout() @@ -922,13 +939,16 @@ class InputFlowBip39ResetPIN(InputFlowBase): self.debug.press_yes() yield # Enter new PIN + assert "PinKeyboard" in self.all_components() self.debug.input("654") if self.debug.model == "R": yield # Re-enter PIN + assert "re-enter PIN" in self.text_content() self.debug.press_yes() yield # Confirm PIN + assert "PinKeyboard" in self.all_components() self.debug.input("654") br = yield # Confirm entropy @@ -1273,10 +1293,6 @@ def enter_recovery_seed_dry_run_tt( assert "check the recovery seed" in debug.wait_layout().text_content() debug.click(buttons.OK) - yield - assert "number of words" in debug.wait_layout().text_content() - debug.click(buttons.OK) - yield assert "SelectWordCount" in debug.wait_layout().all_components() # click the correct number @@ -1309,12 +1325,13 @@ class InputFlowBip39RecoveryDryRun(InputFlowBase): def input_flow_tr(self) -> GeneratorType: yield - assert "check the recovery seed" in self.layout().text_content() + assert "check the recovery seed" in self.text_content() self.debug.press_yes() yield from enter_recovery_seed_tr(self.debug, self.mnemonic) yield + assert "is valid" in self.text_content() self.debug.press_yes() @@ -1328,20 +1345,16 @@ class InputFlowBip39RecoveryDryRunInvalid(InputFlowBase): br = yield assert br.code == messages.ButtonRequestType.Warning - assert "Invalid recovery seed" in self.layout().text_content() + assert "Invalid recovery seed" in self.text_content() self.debug.click(buttons.OK) - yield # retry screen - assert "number of words" in self.layout().text_content() - self.debug.click(buttons.CANCEL) - yield - assert "ABORT BACKUP CHECK" == self.layout().title() - self.debug.click(buttons.OK) + assert "SelectWordCount" in self.all_components() + self.client.cancel() def input_flow_tr(self) -> GeneratorType: yield - assert "check the recovery seed" in self.layout().text_content() + assert "check the recovery seed" in self.text_content() self.debug.press_right() mnemonic = ["stick"] * 12 @@ -1349,15 +1362,15 @@ class InputFlowBip39RecoveryDryRunInvalid(InputFlowBase): br = yield assert br.code == messages.ButtonRequestType.Warning - assert "Invalid recovery seed" in self.layout().text_content() + assert "Invalid recovery seed" in self.text_content() self.debug.press_middle() yield # retry screen - assert "number of words" in self.layout().text_content() + assert "number of words" in self.text_content() self.debug.press_left() yield - assert "abort" in self.layout().text_content() + assert "abort the backup check" in self.text_content() self.debug.press_right() @@ -1378,10 +1391,6 @@ def bip39_recovery_possible_pin_tt( assert "PinKeyboard" in debug.wait_layout().all_components() debug.input(pin) - yield - assert "number of words" in debug.wait_layout().text_content() - debug.press_yes() - yield assert "SelectWordCount" in debug.wait_layout().all_components() debug.input(str(len(mnemonic))) @@ -1409,20 +1418,18 @@ def bip39_recovery_possible_pin_tr( assert "trezor.io/tos" in debug.wait_layout().text_content() debug.press_yes() - yield - assert "safe to eject" in debug.wait_layout().text_content() - debug.press_yes() - # PIN when requested if pin is not None: yield + assert "PinKeyboard" in debug.wait_layout().all_components() debug.input("654") yield - assert "re-enter PIN to confirm" in debug.wait_layout().text_content() + assert "re-enter PIN" in debug.wait_layout().text_content() debug.press_right() yield + assert "PinKeyboard" in debug.wait_layout().all_components() debug.input("654") yield from enter_recovery_seed_tr(debug, mnemonic) @@ -1449,10 +1456,9 @@ def enter_recovery_seed_tr(debug: DebugLink, mnemonic: list[str]) -> GeneratorTy debug.press_yes() yield + assert "MnemonicKeyboard" in debug.wait_layout().all_components() for index, word in enumerate(mnemonic): - title = debug.wait_layout().title() - assert "WORD" in title - assert str(index + 1) in title + assert f"WORD {index + 1}" in debug.wait_layout().title() debug.input(word) @@ -1487,21 +1493,18 @@ class InputFlowSlip39AdvancedRecoveryDryRun(InputFlowBase): def input_flow_common(self) -> GeneratorType: yield # Confirm Dryrun + assert "check the recovery seed" in self.text_content() self.debug.press_yes() # run recovery flow yield from recovery_enter_shares(self.debug, self.shares, groups=True) def confirm_recovery(debug: DebugLink) -> GeneratorType: - if debug.model == "T": - yield # Confirm Recovery - debug.press_yes() - elif debug.model == "R": - yield # Confirm Recovery + yield # Confirm Recovery + assert "By continuing you agree" in debug.wait_layout().text_content() + if debug.model == "R": debug.press_right() - debug.press_yes() - yield # Safe to eject - debug.press_yes() + debug.press_yes() class InputFlowSlip39AdvancedRecovery(InputFlowBase): @@ -1523,9 +1526,18 @@ class InputFlowSlip39AdvancedRecoveryAbort(InputFlowBase): def input_flow_common(self) -> GeneratorType: yield from confirm_recovery(self.debug) - yield # Homescreen - abort process + if self.debug.model == "T": + # Need to choose the word amount for TT + yield + self.debug.input("12") + yield + assert "Enter your backup" in self.text_content() + else: + yield + assert "number of words" in self.text_content() self.debug.press_no() yield # Homescreen - confirm abort + assert "abort the recovery" in self.text_content() self.debug.press_yes() @@ -1534,13 +1546,47 @@ class InputFlowSlip39AdvancedRecoveryNoAbort(InputFlowBase): super().__init__(client) self.shares = shares - def input_flow_common(self) -> GeneratorType: + def input_flow_tt(self) -> GeneratorType: yield from confirm_recovery(self.debug) + yield + self.debug.input("20") yield # Homescreen - abort process + assert "Enter any share" in self.text_content() self.debug.press_no() yield # Homescreen - go back to process - if self.debug.model == "R": - self.debug.press_right() + assert "abort the recovery" in self.text_content() + self.debug.press_no() + yield + assert "Enter any share" in self.text_content() + self.debug.press_yes() + + # TODO: make it reusable + for index, share in enumerate(self.shares): + yield + assert "MnemonicKeyboard" in self.debug.wait_layout().all_components() + for word in share.split(" "): + self.debug.input(word) + + yield + if index == len(self.shares) - 1: + assert "Wallet recovered" in self.text_content() + else: + assert "You have entered" in self.text_content() + self.debug.press_yes() + yield + assert "More shares needed" in self.text_content() + self.debug.press_yes() + + yield + + def input_flow_tr(self) -> GeneratorType: + yield from confirm_recovery(self.debug) + yield # Homescreen - abort process + assert "number of words" in self.text_content() + self.debug.press_no() + yield # Homescreen - go back to process + assert "abort the recovery" in self.text_content() + self.debug.press_right() self.debug.press_no() yield from recovery_enter_shares(self.debug, self.shares, groups=True) @@ -1556,15 +1602,22 @@ class InputFlowSlip39AdvancedRecoveryTwoSharesWarning(InputFlowBase): yield from slip39_recovery_setup_and_first_share(self.debug, self.first_share) yield # Continue to next share + assert "You have entered" in self.text_content() self.debug.press_yes() yield # Homescreen - next share + assert "More shares needed" in self.text_content() self.debug.press_yes() yield # Enter next share + assert "MnemonicKeyboard" in self.debug.wait_layout().all_components() for word in self.second_share: self.debug.input(word) br = yield assert br.code == messages.ButtonRequestType.Warning + assert ( + "Share already entered" in self.text_content() + or "Group threshold reached" in self.text_content() + ) self.debug.press_yes() yield @@ -1572,23 +1625,27 @@ class InputFlowSlip39AdvancedRecoveryTwoSharesWarning(InputFlowBase): def slip39_recovery_possible_pin( - debug: DebugLink, shares: list[str], pin: Optional[str] + debug: DebugLink, shares: list[str], pin: Optional[str], dry_run: bool = False ) -> GeneratorType: yield # Confirm Recovery/Dryrun - if debug.model == "R" and "BACKUP CHECK" not in debug.wait_layout().title(): - # dryruns do not have extra dialogs - debug.press_right() - debug.press_yes() - yield + if dry_run: + assert "check the recovery seed" in debug.wait_layout().text_content() + else: + assert "By continuing you agree" in debug.wait_layout().text_content() + if debug.model == "R": + debug.press_right() debug.press_yes() if pin is not None: yield # Enter PIN + assert "PinKeyboard" in debug.wait_layout().all_components() debug.input(pin) if debug.model == "R": yield # Reenter PIN + assert "re-enter PIN" in debug.wait_layout().text_content() debug.press_yes() yield # Enter PIN again + assert "PinKeyboard" in debug.wait_layout().all_components() debug.input(pin) # Proceed with recovery @@ -1596,12 +1653,15 @@ def slip39_recovery_possible_pin( class InputFlowSlip39BasicRecovery(InputFlowBase): - def __init__(self, client: Client, shares: list[str]): + def __init__(self, client: Client, shares: list[str], dry_run: bool = False): super().__init__(client) self.shares = shares + self.dry_run = dry_run def input_flow_common(self) -> GeneratorType: - yield from slip39_recovery_possible_pin(self.debug, self.shares, pin=None) + yield from slip39_recovery_possible_pin( + self.debug, self.shares, pin=None, dry_run=self.dry_run + ) class InputFlowSlip39BasicRecoveryPIN(InputFlowBase): @@ -1618,11 +1678,26 @@ class InputFlowSlip39BasicRecoveryAbort(InputFlowBase): def __init__(self, client: Client): super().__init__(client) - def input_flow_common(self) -> GeneratorType: + def input_flow_tt(self) -> GeneratorType: + yield from confirm_recovery(self.debug) + yield + assert "number of words" in self.text_content() + self.debug.input("20") + yield + assert "Enter any share" in self.text_content() + self.debug.press_no() + yield + assert "abort the recovery" in self.text_content() + self.debug.press_yes() + yield + + def input_flow_tr(self) -> GeneratorType: yield from confirm_recovery(self.debug) yield # Homescreen - abort process + assert "number of words" in self.text_content() self.debug.press_no() yield # Homescreen - confirm abort + assert "abort the recovery" in self.text_content() self.debug.press_yes() @@ -1631,13 +1706,45 @@ class InputFlowSlip39BasicRecoveryNoAbort(InputFlowBase): super().__init__(client) self.shares = shares - def input_flow_common(self) -> GeneratorType: + def input_flow_tt(self) -> GeneratorType: + yield from confirm_recovery(self.debug) + yield + assert "number of words" in self.text_content() + self.debug.input("20") + yield + assert "Enter any share" in self.text_content() + self.debug.press_no() + yield + assert "abort the recovery" in self.text_content() + self.debug.press_no() + yield + assert "Enter any share" in self.text_content() + self.debug.press_yes() + # run recovery flow + # TODO: make this a reusable function + for index, share in enumerate(self.shares): + yield + assert "MnemonicKeyboard" in self.debug.wait_layout().all_components() + for word in share.split(" "): + self.debug.input(word) + + yield + if index == len(self.shares) - 1: + assert "Wallet recovered" in self.text_content() + else: + assert "shares entered successfully" in self.text_content() + self.debug.press_yes() + + yield + + def input_flow_tr(self) -> GeneratorType: yield from confirm_recovery(self.debug) yield # Homescreen - abort process + assert "number of words" in self.text_content() self.debug.press_no() yield # Homescreen - go back to process - if self.debug.model == "R": - self.debug.press_right() + assert "abort the recovery" in self.text_content() + self.debug.press_right() self.debug.press_no() # run recovery flow yield from recovery_enter_shares(self.debug, self.shares) @@ -1646,18 +1753,35 @@ class InputFlowSlip39BasicRecoveryNoAbort(InputFlowBase): def slip39_recovery_setup_and_first_share( debug: DebugLink, first_share: list[str] ) -> GeneratorType: - yield # Homescreen - start process - debug.press_yes() - yield # Enter number of words - debug.input(str(len(first_share))) - yield # Homescreen - proceed to share entry if debug.model == "R": + yield + assert "number of words" in debug.wait_layout().text_content() + debug.press_yes() + yield + assert "NUMBER OF WORDS" in debug.wait_layout().title() + debug.input(str(len(first_share))) + yield # Homescreen - proceed to share entry + assert "Enter any share" in debug.wait_layout().text_content() debug.press_right(wait=True) debug.press_right(wait=True) - debug.press_yes() - yield # Enter first share - for word in first_share: - debug.input(word) + debug.press_yes() + yield # Enter first share + assert "MnemonicKeyboard" in debug.wait_layout().all_components() + for index, word in enumerate(first_share): + assert f"WORD {index + 1}" in debug.wait_layout().title() + debug.input(word) + else: + yield # Enter number of words + assert "number of words" in debug.wait_layout().text_content() + debug.input(str(len(first_share))) + yield # Homescreen - proceed to share entry + assert "Enter any share" in debug.wait_layout().text_content() + debug.press_yes() + yield # Enter first share + assert debug.wait_layout().main_component() == "MnemonicKeyboard" + for index, word in enumerate(first_share): + assert f"Type word {index + 1}" in debug.wait_layout().text_content() + debug.input(word) class InputFlowSlip39BasicRecoveryRetryFirst(InputFlowBase): @@ -1672,6 +1796,7 @@ class InputFlowSlip39BasicRecoveryRetryFirst(InputFlowBase): br = yield # Invalid share assert br.code == messages.ButtonRequestType.Warning + assert "Invalid recovery share" in self.text_content() self.debug.press_yes() first_share = ["slush"] * 33 @@ -1679,11 +1804,26 @@ class InputFlowSlip39BasicRecoveryRetryFirst(InputFlowBase): br = yield # Invalid share assert br.code == messages.ButtonRequestType.Warning + assert "Invalid recovery share" in self.text_content() self.debug.press_yes() yield # Homescreen + assert "number of words" in self.text_content() + if self.debug.model == "R": + self.debug.press_yes() + yield + assert "NUMBER OF WORDS" in self.title() + + # Cancelling the recovery process + # (needs to be manual to be compatible with the next input-flow) + + self.debug.input("20") + yield # Homescreen - proceed to share entry + assert "Enter any share" in self.text_content() + self.debug.press_no() yield # Confirm abort + assert "abort the recovery process" in self.text_content() if self.debug.model == "R": self.debug.press_right(wait=True) self.debug.press_yes() @@ -1702,28 +1842,29 @@ class InputFlowSlip39BasicRecoveryRetrySecond(InputFlowBase): yield from slip39_recovery_setup_and_first_share(self.debug, first_share) yield # More shares needed + assert "more shares needed" in self.text_content() self.debug.press_yes() yield # Enter another share + assert "MnemonicKeyboard" in self.debug.wait_layout().all_components() invalid_share = first_share[:3] + ["slush"] * 17 for word in invalid_share: self.debug.input(word) - br = yield # Invalid share - assert br.code == messages.ButtonRequestType.Warning + yield # Invalid share + assert "Invalid recovery share" in self.text_content() self.debug.press_yes() yield # Proceed to next share + assert "MnemonicKeyboard" in self.debug.wait_layout().all_components() second_share = self.shares[1].split(" ") for word in second_share: self.debug.input(word) yield # More shares needed - self.debug.press_no() - yield # Confirm abort - if self.debug.model == "R": - self.debug.press_right(wait=True) - self.debug.press_yes() + assert "1 more share needed" in self.text_content() + + self.client.cancel() class InputFlowSlip39BasicRecoveryWrongNthWord(InputFlowBase): @@ -1739,8 +1880,10 @@ class InputFlowSlip39BasicRecoveryWrongNthWord(InputFlowBase): yield from slip39_recovery_setup_and_first_share(self.debug, self.share) yield # Continue to next share + assert "more shares needed" in self.text_content() self.debug.press_yes() yield # Enter next share + assert "MnemonicKeyboard" in self.debug.wait_layout().all_components() for i, word in enumerate(self.share): if i < self.nth_word: self.debug.input(word) @@ -1750,9 +1893,10 @@ class InputFlowSlip39BasicRecoveryWrongNthWord(InputFlowBase): br = yield assert br.code == messages.ButtonRequestType.Warning + assert "entered a share from another" in self.text_content() self.debug.press_yes() - yield + yield self.client.cancel() @@ -1769,13 +1913,16 @@ class InputFlowSlip39BasicRecoverySameShare(InputFlowBase): yield from slip39_recovery_setup_and_first_share(self.debug, self.first_share) yield # Continue to next share + assert "more shares needed" in self.text_content() self.debug.press_yes() yield # Enter next share + assert "MnemonicKeyboard" in self.debug.wait_layout().all_components() for word in self.second_share: self.debug.input(word) br = yield assert br.code == messages.ButtonRequestType.Warning + assert "Share already entered" in self.text_content() self.debug.press_yes() yield @@ -1788,12 +1935,15 @@ class InputFlowResetSkipBackup(InputFlowBase): def input_flow_common(self) -> GeneratorType: yield # Confirm Recovery + assert "By continuing you agree to" in self.text_content() if self.debug.model == "R": self.debug.press_right() self.debug.press_yes() yield # Skip Backup + assert "New wallet created" in self.text_content() if self.debug.model == "R": self.debug.press_right() self.debug.press_no() yield # Confirm skip backup + assert "skip the backup" in self.text_content() self.debug.press_no()