diff --git a/tests/click_tests/common.py b/tests/click_tests/common.py new file mode 100644 index 0000000000..026dd0c653 --- /dev/null +++ b/tests/click_tests/common.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +from .. import buttons + +if TYPE_CHECKING: + from trezorlib.debuglink import DebugLink, LayoutContent + + +# Passphrases and addresses for both models +class CommonPass: + RANDOM_25 = "Y@14lw%p)JN@f54MYvys@zj'g" + RANDOM_25_ADDRESS = "mnkoxeaMzLgfCxUdDSZWrGactyJJerQVW6" + + SHORT = "abc123ABC_<>" + SHORT_ADDRESS = "mtHHfh6uHtJiACwp7kzJZ97yueT6sEdQiG" + + WITH_SPACE = "abc 123" + WITH_SPACE_ADDRESS = "mvqzZUb9NaUc62Buk9WCP4L7hunsXFyamT" + + EMPTY_ADDRESS = "mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q" + + +class PassphraseCategory(Enum): + MENU = "MENU" + DIGITS = "123" + LOWERCASE = "abc" + UPPERCASE = "ABC" + SPECIAL = "#$!" + + +def get_char_category(char: str) -> PassphraseCategory: + """What is the category of a character""" + if char.isdigit(): + return PassphraseCategory.DIGITS + if char.islower(): + return PassphraseCategory.LOWERCASE + if char.isupper(): + return PassphraseCategory.UPPERCASE + return PassphraseCategory.SPECIAL + + +def go_next(debug: "DebugLink", wait: bool = False) -> "LayoutContent" | None: + return debug.click(buttons.OK, wait=wait) # type: ignore + + +def go_back(debug: "DebugLink", wait: bool = False) -> "LayoutContent" | None: + return debug.click(buttons.CANCEL, wait=wait) # type: ignore diff --git a/tests/click_tests/recovery.py b/tests/click_tests/recovery.py index 5acfd59a2b..dbe0e0ea5e 100644 --- a/tests/click_tests/recovery.py +++ b/tests/click_tests/recovery.py @@ -13,35 +13,28 @@ def enter_word( for coords in buttons.type_word(typed_word, is_slip39=is_slip39): debug.click(coords) - # For BIP39 - double-click on CONFIRM WORD is needed in case the word - # is not already typed as a whole - if not is_slip39 and typed_word != word: - debug.click(buttons.CONFIRM_WORD) return debug.click(buttons.CONFIRM_WORD, wait=True) def confirm_recovery(debug: "DebugLink", legacy_ui: bool = False) -> None: - layout = debug.wait_layout() - if legacy_ui: - assert layout.text.startswith("Recovery mode") - else: - assert layout.get_title().startswith("WALLET RECOVERY") + if not legacy_ui: + layout = debug.wait_layout() + assert layout.title().startswith("WALLET RECOVERY") debug.click(buttons.OK, wait=True) def select_number_of_words( debug: "DebugLink", num_of_words: int = 20, legacy_ui: bool = False ) -> None: - layout = debug.read_layout() - # select number of words - assert "Select number of words" in layout.get_content() + if not legacy_ui: + assert "select the number of words" in debug.read_layout().text_content() layout = debug.click(buttons.OK, wait=True) if legacy_ui: - assert layout.text == "WordSelector" + assert layout.json_str == "WordSelector" else: # Two title options - assert layout.get_title() in ("SEED CHECK", "WALLET RECOVERY") + assert layout.title() in ("SEED CHECK", "WALLET RECOVERY") # click the number word_option_offset = 6 @@ -51,7 +44,12 @@ def select_number_of_words( ) # raises if num of words is invalid coords = buttons.grid34(index % 3, index // 3) layout = debug.click(coords, wait=True) - assert "Enter any share" in layout.get_content() + + if not legacy_ui: + if num_of_words in (20, 33): + assert "Enter any share" in layout.text_content() + else: + assert "enter your recovery seed" in layout.text_content() def enter_share( @@ -60,9 +58,9 @@ def enter_share( layout = debug.click(buttons.OK, wait=True) if legacy_ui: - assert layout.text == "Slip39Keyboard" + assert layout.json_str == "Slip39Keyboard" else: - assert layout.text == "< MnemonicKeyboard >" + assert layout.main_component() == "MnemonicKeyboard" for word in share.split(" "): layout = enter_word(debug, word, is_slip39=True) @@ -75,14 +73,26 @@ def enter_shares(debug: "DebugLink", shares: list[str]) -> None: expected_text = "Enter any share" remaining = len(shares) for share in shares: - assert expected_text in layout.get_content() + assert expected_text in layout.text_content() layout = enter_share(debug, share) remaining -= 1 expected_text = f"{remaining} more share" - assert "You have successfully recovered your wallet" in layout.get_content() + assert "You have finished recovering your wallet" in layout.text_content() + + +def enter_seed(debug: "DebugLink", seed_words: list[str]) -> None: + assert "enter" in debug.read_layout().text_content() + + layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "MnemonicKeyboard" + + for word in seed_words: + layout = enter_word(debug, word, is_slip39=False) + + assert "You have finished recovering your wallet" in layout.text_content() def finalize(debug: "DebugLink") -> None: layout = debug.click(buttons.OK, wait=True) - assert layout.text.startswith("< Homescreen ") + assert layout.main_component() == "Homescreen" diff --git a/tests/click_tests/reset.py b/tests/click_tests/reset.py index 29be8ed918..eefa1521e2 100644 --- a/tests/click_tests/reset.py +++ b/tests/click_tests/reset.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from shamir_mnemonic import shamir +from shamir_mnemonic import shamir # type: ignore from trezorlib import messages @@ -10,61 +10,83 @@ if TYPE_CHECKING: from trezorlib.debuglink import DebugLink -def confirm_wait(debug: "DebugLink", title: str) -> None: - layout = debug.wait_layout() - assert title.upper() in layout.get_title() +def confirm_new_wallet(debug: "DebugLink") -> None: + assert debug.wait_layout().title().startswith("WALLET CREATION") debug.click(buttons.OK, wait=True) -def confirm_read(debug: "DebugLink", title: str) -> None: +def confirm_read(debug: "DebugLink", title: str, hold: bool = False) -> None: layout = debug.read_layout() if title == "Caution": - assert "OK, I UNDERSTAND" in layout.text + # TODO: could look into button texts + assert "OK, I UNDERSTAND" in layout.json_str elif title == "Success": + # TODO: improve this assert any( - text in layout.get_content() for text in ("success", "finished", "done") + text in layout.text_content() + for text in ( + "success", + "finished", + "done", + "has been created", + "Keep it safe", + ) ) + elif title == "Checklist": + assert "number of shares" in layout.text_content().lower() else: - assert title.upper() in layout.get_title() + assert title.upper() in layout.title() + debug.click(buttons.OK, wait=True) def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> None: - layout = debug.read_layout() - assert "NumberInputDialog" in layout.text + assert "NumberInputDialog" in debug.read_layout().all_components() for _ in range(diff): - debug.click(button, wait=False) + debug.click(button) debug.click(buttons.OK, wait=True) -def read_words(debug: "DebugLink", is_advanced: bool = False) -> list[str]: +def read_words( + debug: "DebugLink", backup_type: messages.BackupType, do_htc: bool = True +) -> list[str]: words: list[str] = [] layout = debug.read_layout() - if is_advanced: - assert layout.get_title().startswith("GROUP") + + if backup_type == messages.BackupType.Slip39_Advanced: + assert layout.title().startswith("GROUP") + elif backup_type == messages.BackupType.Slip39_Basic: + assert layout.title().startswith("RECOVERY SHARE #") else: - assert layout.get_title().startswith("RECOVERY SHARE #") + assert layout.title() == "RECOVERY SEED" # Swiping through all the page and loading the words - for _ in range(layout.get_page_count() - 1): - words.extend(layout.get_seed_words()) + for _ in range(layout.page_count() - 1): + words.extend(layout.seed_words()) layout = debug.input(swipe=messages.DebugSwipeDirection.UP, wait=True) - words.extend(layout.get_seed_words()) + assert layout is not None + words.extend(layout.seed_words()) - debug.press_yes() + # There is hold-to-confirm button + if do_htc: + debug.click_hold(buttons.OK, hold_ms=1500) + else: + # It would take a very long time to test 16-of-16 with doing 1500 ms HTC after + # each word set + debug.press_yes() return words def confirm_words(debug: "DebugLink", words: list[str]) -> None: layout = debug.wait_layout() - assert "Select word" in layout.text + assert "Select word" in layout.text_content() for _ in range(3): # "Select word 3 of 20" # ^ - word_pos = int(layout.get_content().split()[2]) + word_pos = int(layout.text_content().split()[2]) # Unifying both the buttons and words to lowercase - btn_texts = [text.lower() for text in layout.get_button_texts()] + btn_texts = [text.lower() for text in layout.button_contents()] wanted_word = words[word_pos - 1].lower() button_pos = btn_texts.index(wanted_word) layout = debug.click(buttons.RESET_WORD_CHECK[button_pos], wait=True) diff --git a/tests/click_tests/test_autolock.py b/tests/click_tests/test_autolock.py index 7d7abd643f..90f698c4e8 100644 --- a/tests/click_tests/test_autolock.py +++ b/tests/click_tests/test_autolock.py @@ -31,6 +31,7 @@ from . import recovery if TYPE_CHECKING: from ..device_handler import BackgroundDeviceHandler + from trezorlib.debuglink import DebugLink, LayoutContent TX_CACHE_MAINNET = TxCache("Bitcoin") TX_CACHE_TESTNET = TxCache("Testnet") @@ -48,26 +49,24 @@ TXHASH_d5f65e = bytes.fromhex( PIN4 = "1234" WORDS_20 = buttons.grid34(2, 2) +CENTER_BUTTON = buttons.grid35(1, 2) def set_autolock_delay(device_handler: "BackgroundDeviceHandler", delay_ms: int): debug = device_handler.debuglink() - device_handler.run(device.apply_settings, auto_lock_delay_ms=delay_ms) + device_handler.run(device.apply_settings, auto_lock_delay_ms=delay_ms) # type: ignore + + assert debug.wait_layout().main_component() == "PinKeyboard" - layout = debug.wait_layout() - assert layout.text == "< PinKeyboard >" debug.input("1234") - layout = debug.wait_layout() assert ( f"auto-lock your device after {delay_ms // 1000} seconds" - in layout.get_content() + in debug.wait_layout().text_content() ) - debug.click(buttons.OK) - - layout = debug.wait_layout() - assert layout.text.startswith("< Homescreen") + layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "Homescreen" assert device_handler.result() == "Settings applied" @@ -92,17 +91,16 @@ def test_autolock_interrupts_signing(device_handler: "BackgroundDeviceHandler"): script_type=messages.OutputScriptType.PAYTOADDRESS, ) - device_handler.run( - btc.sign_tx, "Bitcoin", [inp1], [out1], prev_txes=TX_CACHE_MAINNET + device_handler.run(btc.sign_tx, "Bitcoin", [inp1], [out1], prev_txes=TX_CACHE_MAINNET) # type: ignore + + assert ( + "1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1" + in debug.wait_layout().text_content().replace(" ", "") ) - layout = debug.wait_layout() - assert "1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1" in layout.get_content().replace(" ", "") - debug.click(buttons.OK, wait=True) - layout = debug.click(buttons.OK, wait=True) - assert "Total amount: 0.0039 BTC" in layout.get_content() + assert "Total amount: 0.0039 BTC" in layout.text_content() # wait for autolock to kick in time.sleep(10.1) @@ -135,13 +133,15 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa btc.sign_tx, "Bitcoin", [inp1], [out1], prev_txes=TX_CACHE_MAINNET ) - layout = debug.wait_layout() - assert "1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1" in layout.get_content().replace(" ", "") + assert ( + "1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1" + in debug.wait_layout().text_content().replace(" ", "") + ) debug.click(buttons.OK, wait=True) layout = debug.click(buttons.OK, wait=True) - assert "Total amount: 0.0039 BTC" in layout.get_content() + assert "Total amount: 0.0039 BTC" in layout.text_content() def sleepy_filter(msg: MessageType) -> MessageType: time.sleep(10.1) @@ -166,18 +166,18 @@ def test_autolock_passphrase_keyboard(device_handler: "BackgroundDeviceHandler") debug = device_handler.debuglink() # get address - device_handler.run(common.get_test_address) + device_handler.run(common.get_test_address) # type: ignore + + assert debug.wait_layout().main_component() == "PassphraseKeyboard" # enter passphrase - slowly - layout = debug.wait_layout() - assert layout.text == "< PassphraseKeyboard >" - - CENTER_BUTTON = buttons.grid35(1, 2) # keep clicking for long enough to trigger the autolock if it incorrectly ignored key presses for _ in range(math.ceil(11 / 1.5)): + # click at "j" debug.click(CENTER_BUTTON) time.sleep(1.5) + # Confirm the passphrase debug.click(buttons.OK, wait=True) assert device_handler.result() == "mnF4yRWJXmzRB6EuBzuVigqeqTqirQupxJ" @@ -188,13 +188,11 @@ def test_autolock_interrupts_passphrase(device_handler: "BackgroundDeviceHandler debug = device_handler.debuglink() # get address - device_handler.run(common.get_test_address) + device_handler.run(common.get_test_address) # type: ignore + + assert debug.wait_layout().main_component() == "PassphraseKeyboard" # enter passphrase - slowly - layout = debug.wait_layout() - assert layout.text == "< PassphraseKeyboard >" - - CENTER_BUTTON = buttons.grid35(1, 2) # autolock must activate even if we pressed some buttons for _ in range(math.ceil(6 / 1.5)): debug.click(CENTER_BUTTON) @@ -202,41 +200,51 @@ def test_autolock_interrupts_passphrase(device_handler: "BackgroundDeviceHandler # wait for autolock to kick in time.sleep(10.1) - layout = debug.wait_layout() - assert layout.text.startswith("< Lockscreen") + assert debug.wait_layout().main_component() == "Lockscreen" with pytest.raises(exceptions.Cancelled): device_handler.result() +def unlock_dry_run(debug: "DebugLink") -> "LayoutContent": + assert ( + "Do you really want to check the recovery seed?" + in debug.wait_layout().text_content() + ) + layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "PinKeyboard" + + layout = debug.input(PIN4, wait=True) + assert layout is not None + return layout + + @pytest.mark.setup_client(pin=PIN4) def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() - device_handler.run(device.recover, dry_run=True) + device_handler.run(device.recover, dry_run=True) # type: ignore - # unlock - layout = debug.wait_layout() - assert "Do you really want to check the recovery seed?" in layout.get_content() - layout = debug.click(buttons.OK, wait=True) - assert layout.text == "< PinKeyboard >" - layout = debug.input(PIN4, wait=True) - assert "Select number of words " in layout.get_content() + layout = unlock_dry_run(debug) + assert "select the number of words " in layout.text_content() # wait for autolock to trigger time.sleep(10.1) - layout = debug.wait_layout() - assert layout.text.startswith("< Lockscreen") + assert debug.wait_layout().main_component() == "Lockscreen" with pytest.raises(exceptions.Cancelled): device_handler.result() # unlock + debug.wait_layout( + wait_for_external_change=True + ) # lockscreen triggered automatically layout = debug.click(buttons.OK, wait=True) - assert layout.text == "< PinKeyboard >" + assert layout.main_component() == "PinKeyboard" layout = debug.input(PIN4, wait=True) + assert layout is not None # we are back at homescreen - assert "Select number of words" in layout.get_content() + assert "select the number of words" in layout.text_content() @pytest.mark.setup_client(pin=PIN4) @@ -244,24 +252,18 @@ def test_dryrun_locks_at_word_entry(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() - device_handler.run(device.recover, dry_run=True) + device_handler.run(device.recover, dry_run=True) # type: ignore - # unlock - layout = debug.wait_layout() - assert "Do you really want to check the recovery seed?" in layout.get_content() - layout = debug.click(buttons.OK, wait=True) - assert layout.text == "< PinKeyboard >" - layout = debug.input(PIN4, wait=True) + unlock_dry_run(debug) # select 20 words recovery.select_number_of_words(debug, 20) layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "MnemonicKeyboard" # make sure keyboard locks - assert layout.text == "< MnemonicKeyboard >" time.sleep(10.1) - layout = debug.wait_layout() - assert layout.text.startswith("< Lockscreen") + assert debug.wait_layout().main_component() == "Lockscreen" with pytest.raises(exceptions.Cancelled): device_handler.result() @@ -271,27 +273,24 @@ def test_dryrun_enter_word_slowly(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() - device_handler.run(device.recover, dry_run=True) + device_handler.run(device.recover, dry_run=True) # type: ignore - # unlock - layout = debug.wait_layout() - assert "Do you really want to check the recovery seed?" in layout.get_content() - layout = debug.click(buttons.OK, wait=True) - assert layout.text == "< PinKeyboard >" - layout = debug.input(PIN4, wait=True) + unlock_dry_run(debug) # select 20 words recovery.select_number_of_words(debug, 20) layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "MnemonicKeyboard" + # type the word OCEAN slowly - assert layout.text == "< MnemonicKeyboard >" for coords in buttons.type_word("ocea", is_slip39=True): time.sleep(9) debug.click(coords) layout = debug.click(buttons.CONFIRM_WORD, wait=True) # should not have locked, even though we took 9 seconds to type each letter - assert layout.text == "< MnemonicKeyboard >" + assert layout.main_component() == "MnemonicKeyboard" + device_handler.kill_task() diff --git a/tests/click_tests/test_lock.py b/tests/click_tests/test_lock.py index bb29f78391..545b173acb 100644 --- a/tests/click_tests/test_lock.py +++ b/tests/click_tests/test_lock.py @@ -32,36 +32,40 @@ PIN4 = "1234" def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"): debug = device_handler.debuglink() + short_duration = 1000 + lock_duration = 3500 + def hold(duration: int, wait: bool = True) -> None: debug.input(x=13, y=37, hold_ms=duration, wait=wait) - time.sleep(duration / 1000 + 0.5) assert device_handler.features().unlocked is False # unlock with message device_handler.run(common.get_test_address) - layout = debug.wait_layout() - assert layout.text == "< PinKeyboard >" + + assert debug.wait_layout().main_component() == "PinKeyboard" debug.input("1234", wait=True) assert device_handler.result() assert device_handler.features().unlocked is True # short touch - hold(1000, wait=False) + hold(short_duration) + + time.sleep(0.5) # so that the homescreen appears again (hacky) assert device_handler.features().unlocked is True # lock - hold(3500) + hold(lock_duration) assert device_handler.features().unlocked is False # unlock by touching layout = debug.click(buttons.INFO, wait=True) - assert layout.text == "< PinKeyboard >" + assert layout.main_component() == "PinKeyboard" debug.input("1234", wait=True) assert device_handler.features().unlocked is True # lock - hold(3500) + hold(lock_duration) assert device_handler.features().unlocked is False diff --git a/tests/click_tests/test_passphrase_tt.py b/tests/click_tests/test_passphrase_tt.py new file mode 100644 index 0000000000..a2f54f8c05 --- /dev/null +++ b/tests/click_tests/test_passphrase_tt.py @@ -0,0 +1,299 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2023 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import time +from contextlib import contextmanager +from typing import TYPE_CHECKING, Generator, Optional + +import pytest + +from .. import buttons +from ..common import get_test_address +from .common import CommonPass, PassphraseCategory, get_char_category + +if TYPE_CHECKING: + from ..device_handler import BackgroundDeviceHandler + from trezorlib.debuglink import DebugLink + + +pytestmark = pytest.mark.skip_t1 + +# TODO: it is not possible to cancel the passphrase entry on TT +# NOTE: the prompt (underscoring) is not there when a space is entered + +TT_CATEGORIES = [ + PassphraseCategory.DIGITS, + PassphraseCategory.LOWERCASE, + PassphraseCategory.UPPERCASE, + PassphraseCategory.SPECIAL, +] +# TODO: better read this from the trace +TT_CATEGORY = PassphraseCategory.LOWERCASE +TT_COORDS_PREV: buttons.Coords = (0, 0) + +# Testing the maximum length is really 50 +# TODO: show some UI message when length reaches 50? +# (it currently disabled typing and greys out the buttons) + +DA_50 = 25 * "da" +DA_50_ADDRESS = "mg5L2i8HZKUvceK1sfmGHhE4gichFSsdvm" +assert len(DA_50) == 50 + +DA_49 = DA_50[:-1] +DA_49_ADDRESS = "mxrB75ydMS3ZzqmYKK28fj4bNMEx7dDw6e" +assert len(DA_49) == 49 +assert DA_49_ADDRESS != DA_50_ADDRESS + +DA_51 = DA_50 + "d" +DA_51_ADDRESS = DA_50_ADDRESS +assert len(DA_51) == 51 +assert DA_51_ADDRESS == DA_50_ADDRESS + + +@contextmanager +def prepare_passphrase_dialogue( + device_handler: "BackgroundDeviceHandler", address: Optional[str] = None +) -> Generator["DebugLink", None, None]: + debug = device_handler.debuglink() + device_handler.run(get_test_address) # type: ignore + assert debug.wait_layout().main_component() == "PassphraseKeyboard" + + # Resetting the category as it could have been changed by previous tests + global TT_CATEGORY + TT_CATEGORY = PassphraseCategory.LOWERCASE # type: ignore + + yield debug + + result = device_handler.result() + if address is not None: + assert result == address + + +def go_to_category(debug: "DebugLink", category: PassphraseCategory) -> None: + """Go to a specific category""" + global TT_CATEGORY + global TT_COORDS_PREV + + # Already there + if TT_CATEGORY == category: + return + + current_index = TT_CATEGORIES.index(TT_CATEGORY) + target_index = TT_CATEGORIES.index(category) + if target_index > current_index: + for _ in range(target_index - current_index): + debug.swipe_left(wait=True) + else: + for _ in range(current_index - target_index): + debug.swipe_right(wait=True) + TT_CATEGORY = category # type: ignore + # Category changed, reset coordinates + TT_COORDS_PREV = (0, 0) # type: ignore + + +def press_char(debug: "DebugLink", char: str) -> None: + """Press a character""" + global TT_COORDS_PREV + + # Space and couple others are a special case + if char in " *#": + char_category = PassphraseCategory.LOWERCASE + else: + char_category = get_char_category(char) + + go_to_category(debug, char_category) + + coords, amount = buttons.passphrase(char) + # If the button is the same as for the previous char, + # waiting a second before pressing it again. + # (not for a space) + if coords == TT_COORDS_PREV and char != " ": + time.sleep(1.1) + TT_COORDS_PREV = coords # type: ignore + for _ in range(amount): + debug.click(coords, wait=True) + + +def input_passphrase(debug: "DebugLink", passphrase: str, check: bool = True) -> None: + """Input a passphrase with validation it got added""" + if check: + before = debug.read_layout().passphrase() + for char in passphrase: + press_char(debug, char) + if check: + after = debug.read_layout().passphrase() + assert after == before + passphrase + + +def enter_passphrase(debug: "DebugLink") -> None: + """Enter a passphrase""" + coords = buttons.pin_passphrase_grid(11) + debug.click(coords, wait=True) + + +def delete_char(debug: "DebugLink") -> None: + """Deletes the last char""" + coords = buttons.pin_passphrase_grid(9) + debug.click(coords, wait=True) + + +VECTORS = ( # passphrase, address + (CommonPass.SHORT, CommonPass.SHORT_ADDRESS), + (CommonPass.WITH_SPACE, CommonPass.WITH_SPACE_ADDRESS), + (CommonPass.RANDOM_25, CommonPass.RANDOM_25_ADDRESS), + (DA_49, DA_49_ADDRESS), + (DA_50, DA_50_ADDRESS), +) + + +@pytest.mark.parametrize("passphrase, address", VECTORS) +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_input( + device_handler: "BackgroundDeviceHandler", passphrase: str, address: str +): + with prepare_passphrase_dialogue(device_handler, address) as debug: + input_passphrase(debug, passphrase) + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_input_over_50_chars(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler, DA_51_ADDRESS) as debug: # type: ignore + input_passphrase(debug, DA_51, check=False) + assert debug.read_layout().passphrase() == DA_50 + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_delete(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler, CommonPass.SHORT_ADDRESS) as debug: + input_passphrase(debug, CommonPass.SHORT[:8]) + + for _ in range(4): + delete_char(debug) + debug.wait_layout() + + input_passphrase(debug, CommonPass.SHORT[8 - 4 :]) + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_delete_all( + device_handler: "BackgroundDeviceHandler", +): + with prepare_passphrase_dialogue(device_handler, CommonPass.EMPTY_ADDRESS) as debug: + passphrase = "trezor" + input_passphrase(debug, passphrase) + + for _ in range(len(passphrase)): + delete_char(debug) + + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_loop_all_characters(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler, CommonPass.EMPTY_ADDRESS) as debug: + for category in ( + PassphraseCategory.DIGITS, + PassphraseCategory.LOWERCASE, + PassphraseCategory.UPPERCASE, + PassphraseCategory.SPECIAL, + ): + go_to_category(debug, category) + debug.wait_layout() + + enter_passphrase(debug) + coords = buttons.pin_passphrase_grid(11) + debug.click(coords) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_click_same_button_many_times( + device_handler: "BackgroundDeviceHandler", +): + with prepare_passphrase_dialogue(device_handler) as debug: + a_coords, _ = buttons.passphrase("a") + for _ in range(10): + debug.click(a_coords) + + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_prompt_disappears( + device_handler: "BackgroundDeviceHandler", +): + with prepare_passphrase_dialogue(device_handler) as debug: + input_passphrase(debug, "a") + + # Wait a second for the prompt to disappear + time.sleep(1.1) + + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_long_spaces_deletion( + device_handler: "BackgroundDeviceHandler", +): + with prepare_passphrase_dialogue(device_handler) as debug: + input_passphrase( + debug, + "a" + + " " * 7 + + "b" + + " " * 7 + + "c" + + " " * 7 + + "d" + + " " * 7 + + "e" + + " " * 7 + + "f", + ) + for _ in range(12): + delete_char(debug) + + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_dollar_sign_deletion( + device_handler: "BackgroundDeviceHandler", +): + # Checks that dollar signs will not leave one pixel on the top after deleting + # (was a bug previously) + with prepare_passphrase_dialogue(device_handler, CommonPass.EMPTY_ADDRESS) as debug: + passphrase = "$$ I want $$" + input_passphrase(debug, passphrase) + + for _ in range(len(passphrase)): + delete_char(debug) + + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_cycle_through_last_character( + device_handler: "BackgroundDeviceHandler", +): + # Checks that we can cycle through the last (50th) passphrase character + # (was a bug previously) + with prepare_passphrase_dialogue(device_handler) as debug: + passphrase = DA_49 + "i" # for i we need to cycle through "ghi" three times + input_passphrase(debug, passphrase) + enter_passphrase(debug) diff --git a/tests/click_tests/test_pin.py b/tests/click_tests/test_pin.py new file mode 100644 index 0000000000..cd7c383417 --- /dev/null +++ b/tests/click_tests/test_pin.py @@ -0,0 +1,274 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2023 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from contextlib import contextmanager +from enum import Enum +from typing import TYPE_CHECKING, Generator + +import pytest + +from trezorlib import device, exceptions + +from .. import buttons +from .common import go_back, go_next + +if TYPE_CHECKING: + from ..device_handler import BackgroundDeviceHandler + from trezorlib.debuglink import DebugLink + + +pytestmark = pytest.mark.skip_t1 + +PIN_CANCELLED = pytest.raises(exceptions.TrezorFailure, match="PIN entry cancelled") +PIN_INVALID = pytest.raises(exceptions.TrezorFailure, match="PIN invalid") + +PIN4 = "1234" +PIN24 = "875163065288639289952973" +PIN50 = "31415926535897932384626433832795028841971693993751" +PIN60 = PIN50 + "9" * 10 + +TR_PIN_ACTIONS = [ + "DELETE", + "SHOW", + "ENTER", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", +] + + +class Situation(Enum): + PIN_INPUT = 1 + PIN_SETUP = 2 + PIN_CHANGE = 3 + WIPE_CODE_SETUP = 4 + + +@contextmanager +def prepare( + device_handler: "BackgroundDeviceHandler", + situation: Situation = Situation.PIN_INPUT, + old_pin: str = "", +) -> Generator["DebugLink", None, None]: + debug = device_handler.debuglink() + # So that the digit order is the same. Needed for UI tests. + # Even though it should be done in conftest::client fixture (used by device_handler), + # without reseeding "again", the results are still random. + debug.reseed(0) + + # Setup according to the wanted situation + if situation == Situation.PIN_INPUT: + # Any action triggering the PIN dialogue + device_handler.run(device.apply_settings, auto_lock_delay_ms=300_000) # type: ignore + elif situation == Situation.PIN_SETUP: + # Set new PIN + device_handler.run(device.change_pin) # type: ignore + assert "enable PIN protection" in debug.wait_layout().text_content() + layout = go_next(debug, wait=True) + assert "access this device" in layout.text_content() + go_next(debug) + elif situation == Situation.PIN_CHANGE: + # Change PIN + device_handler.run(device.change_pin) # type: ignore + _input_see_confirm(debug, old_pin) + assert "change your PIN" in debug.read_layout().text_content() + go_next(debug, wait=True) + _input_see_confirm(debug, old_pin) + elif situation == Situation.WIPE_CODE_SETUP: + # Set wipe code + device_handler.run(device.change_wipe_code) # type: ignore + if old_pin: + _input_see_confirm(debug, old_pin) + assert "enable wipe code" in debug.wait_layout().text_content() + layout = go_next(debug, wait=True) + assert "erase all data" in layout.text_content() + go_next(debug) + if old_pin: + debug.wait_layout() + _input_see_confirm(debug, old_pin) + + debug.wait_layout() + _assert_pin_entry(debug) + yield debug + go_next(debug) + device_handler.result() + + +def _assert_pin_entry(debug: "DebugLink") -> None: + assert debug.read_layout().main_component() == "PinKeyboard" + + +def _input_pin(debug: "DebugLink", pin: str, check: bool = False) -> None: + """Input the PIN""" + before = debug.read_layout().pin() + + digits_order = debug.read_layout().tt_pin_digits_order() + for digit in pin: + digit_index = digits_order.index(digit) + coords = buttons.pin_passphrase_index(digit_index) + debug.click(coords, wait=True) + + if check: + after = debug.read_layout().pin() + assert before + pin == after + + +def _see_pin(debug: "DebugLink") -> None: + """Navigate to "SHOW" and press it""" + debug.click(buttons.TOP_ROW, wait=True) + + +def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) -> None: + """Navigate to "DELETE" and press it how many times requested""" + if check: + before = debug.read_layout().pin() + + for _ in range(digits_to_delete): + debug.click(buttons.pin_passphrase_grid(9), wait=True) + + if check: + after = debug.read_layout().pin() + assert before[:-digits_to_delete] == after + + +def _cancel_pin(debug: "DebugLink") -> None: + """Navigate to "CANCEL" and press it""" + # It is the same button as DELETE + _delete_pin(debug, 1, check=False) + + +def _confirm_pin(debug: "DebugLink") -> None: + """Navigate to "ENTER" and press it""" + debug.click(buttons.pin_passphrase_grid(11), wait=True) + + +def _input_see_confirm(debug: "DebugLink", pin: str) -> None: + _input_pin(debug, pin) + _see_pin(debug) + _confirm_pin(debug) + + +def _enter_two_times(debug: "DebugLink", pin1: str, pin2: str) -> None: + _input_see_confirm(debug, pin1) + # Please re-enter + debug.click(buttons.OK, wait=True) + _input_see_confirm(debug, pin2) + + +@pytest.mark.setup_client(pin=PIN4) +def test_pin_short(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler) as debug: + _input_see_confirm(debug, PIN4) + + +@pytest.mark.setup_client(pin=PIN24) +def test_pin_long(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler) as debug: + _input_see_confirm(debug, PIN24) + + +@pytest.mark.setup_client(pin=PIN24) +def test_pin_long_delete(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler) as debug: + _input_pin(debug, PIN24) + _see_pin(debug) + + _delete_pin(debug, 10) + _see_pin(debug) + + _input_see_confirm(debug, PIN24[-10:]) + + +@pytest.mark.setup_client(pin=PIN60[:50]) +def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler) as debug: + _input_pin(debug, PIN60, check=False) + + # What is over 50 digits was not entered + # TODO: do some UI change when limit is reached? + assert debug.read_layout().pin() == PIN60[:50] + + _see_pin(debug) + _confirm_pin(debug) + + +@pytest.mark.setup_client(pin=PIN4) +def test_pin_incorrect(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler) as debug: + _input_see_confirm(debug, "1235") + # debug.wait_layout() + _input_see_confirm(debug, PIN4) + + +@pytest.mark.setup_client(pin=PIN4) +def test_pin_cancel(device_handler: "BackgroundDeviceHandler"): + with PIN_CANCELLED, prepare(device_handler) as debug: + _input_pin(debug, PIN4) + _see_pin(debug) + _delete_pin(debug, len(PIN4)) + _see_pin(debug) + _cancel_pin(debug) + + +@pytest.mark.setup_client() +def test_pin_setup(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler, Situation.PIN_SETUP) as debug: + _enter_two_times(debug, PIN4, PIN4) + + +@pytest.mark.setup_client() +def test_pin_setup_mismatch(device_handler: "BackgroundDeviceHandler"): + with PIN_CANCELLED, prepare(device_handler, Situation.PIN_SETUP) as debug: + _enter_two_times(debug, "1", "2") + go_next(debug) + _cancel_pin(debug) + + +@pytest.mark.setup_client(pin="1") +def test_pin_change(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler, Situation.PIN_CHANGE, old_pin="1") as debug: + _enter_two_times(debug, "2", "2") + + +@pytest.mark.setup_client(pin="1") +def test_wipe_code_setup(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler, Situation.WIPE_CODE_SETUP, old_pin="1") as debug: + _enter_two_times(debug, "2", "2") + + +@pytest.mark.setup_client(pin="1") +def test_wipe_code_same_as_pin(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler, Situation.WIPE_CODE_SETUP, old_pin="1") as debug: + _input_see_confirm(debug, "1") + # Try again + go_next(debug, wait=True) + _enter_two_times(debug, "2", "2") + + +@pytest.mark.setup_client() +def test_pin_same_as_wipe_code(device_handler: "BackgroundDeviceHandler"): + with prepare(device_handler, Situation.WIPE_CODE_SETUP) as debug: + _enter_two_times(debug, "1", "1") + with PIN_INVALID, prepare(device_handler, Situation.PIN_SETUP) as debug: + _enter_two_times(debug, "1", "1") + go_back(debug) diff --git a/tests/click_tests/test_recovery.py b/tests/click_tests/test_recovery.py index 414f424b1f..5f07a9f59b 100644 --- a/tests/click_tests/test_recovery.py +++ b/tests/click_tests/test_recovery.py @@ -14,35 +14,56 @@ # You should have received a copy of the License along with this library. # If not, see . -from typing import TYPE_CHECKING +from contextlib import contextmanager +from typing import TYPE_CHECKING, Generator import pytest from trezorlib import device, messages -from ..common import MNEMONIC_SLIP39_BASIC_20_3of6 +from ..common import MNEMONIC12, MNEMONIC_SLIP39_BASIC_20_3of6 from . import recovery if TYPE_CHECKING: from ..device_handler import BackgroundDeviceHandler + from trezorlib.debuglink import DebugLink -@pytest.mark.skip_t1 -@pytest.mark.setup_client(uninitialized=True) -def test_recovery(device_handler: "BackgroundDeviceHandler"): +pytestmark = [pytest.mark.skip_t1] + + +@contextmanager +def prepare_recovery_and_evaluate( + device_handler: "BackgroundDeviceHandler", +) -> Generator["DebugLink", None, None]: features = device_handler.features() debug = device_handler.debuglink() - assert features.initialized is False - device_handler.run(device.recover, pin_protection=False) + device_handler.run(device.recover, pin_protection=False) # type: ignore - recovery.confirm_recovery(debug) - - recovery.select_number_of_words(debug) - recovery.enter_shares(debug, MNEMONIC_SLIP39_BASIC_20_3of6) - recovery.finalize(debug) + yield debug assert isinstance(device_handler.result(), messages.Success) features = device_handler.features() assert features.initialized is True assert features.recovery_mode is False + + +@pytest.mark.setup_client(uninitialized=True) +def test_recovery_slip39_basic(device_handler: "BackgroundDeviceHandler"): + with prepare_recovery_and_evaluate(device_handler) as debug: + recovery.confirm_recovery(debug) + + recovery.select_number_of_words(debug) + recovery.enter_shares(debug, MNEMONIC_SLIP39_BASIC_20_3of6) + recovery.finalize(debug) + + +@pytest.mark.setup_client(uninitialized=True) +def test_recovery_bip39(device_handler: "BackgroundDeviceHandler"): + with prepare_recovery_and_evaluate(device_handler) as debug: + recovery.confirm_recovery(debug) + + recovery.select_number_of_words(debug, num_of_words=12) + recovery.enter_seed(debug, MNEMONIC12.split()) + recovery.finalize(debug) diff --git a/tests/click_tests/test_reset_bip39.py b/tests/click_tests/test_reset_bip39.py new file mode 100644 index 0000000000..93fd92045b --- /dev/null +++ b/tests/click_tests/test_reset_bip39.py @@ -0,0 +1,77 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from typing import TYPE_CHECKING + +import pytest + +from trezorlib import device, messages + +from ..common import WITH_MOCK_URANDOM +from . import reset + +if TYPE_CHECKING: + from ..device_handler import BackgroundDeviceHandler + + +pytestmark = [pytest.mark.skip_t1] + + +@pytest.mark.setup_client(uninitialized=True) +@WITH_MOCK_URANDOM +def test_reset_bip39(device_handler: "BackgroundDeviceHandler"): + features = device_handler.features() + debug = device_handler.debuglink() + + assert features.initialized is False + + device_handler.run( + device.reset, + strength=128, + backup_type=messages.BackupType.Bip39, + pin_protection=False, + ) + + # confirm new wallet + reset.confirm_new_wallet(debug) + + # confirm back up + reset.confirm_read(debug, "Success") + + # confirm backup warning (hold-to-confirm on TR) + reset.confirm_read(debug, "Caution", hold=True) + + # read words + words = reset.read_words(debug, messages.BackupType.Bip39) + + # confirm words + reset.confirm_words(debug, words) + + # confirm backup done + reset.confirm_read(debug, "Success") + + # Your backup is done + debug.press_yes() + + # TODO: some validation of the generated secret? + + assert device_handler.result() == "Initialized" + features = device_handler.features() + assert features.initialized is True + assert features.needs_backup is False + assert features.pin_protection is False + assert features.passphrase_protection is False + assert features.backup_type is messages.BackupType.Bip39 diff --git a/tests/click_tests/test_reset_slip39_advanced.py b/tests/click_tests/test_reset_slip39_advanced.py index aff056a4f1..b0e6590fda 100644 --- a/tests/click_tests/test_reset_slip39_advanced.py +++ b/tests/click_tests/test_reset_slip39_advanced.py @@ -15,30 +15,37 @@ # If not, see . from typing import TYPE_CHECKING -from unittest import mock import pytest from trezorlib import device, messages from .. import buttons -from ..common import generate_entropy +from ..common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy from . import reset if TYPE_CHECKING: from ..device_handler import BackgroundDeviceHandler -EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 - -with_mock_urandom = mock.patch("os.urandom", mock.Mock(return_value=EXTERNAL_ENTROPY)) +pytestmark = [pytest.mark.skip_t1] -@pytest.mark.skip_t1 @pytest.mark.setup_client(uninitialized=True) -@with_mock_urandom -def test_reset_slip39_advanced_2of2groups_2of2shares( +@pytest.mark.parametrize( + "group_count, group_threshold, share_count, share_threshold", + [ + pytest.param(2, 2, 2, 2, id="2of2"), + pytest.param(16, 16, 16, 16, id="16of16", marks=pytest.mark.slow), + ], +) +@WITH_MOCK_URANDOM +def test_reset_slip39_advanced( device_handler: "BackgroundDeviceHandler", + group_count: int, + group_threshold: int, + share_count: int, + share_threshold: int, ): features = device_handler.features() debug = device_handler.debuglink() @@ -52,7 +59,7 @@ def test_reset_slip39_advanced_2of2groups_2of2shares( ) # confirm new wallet - reset.confirm_wait(debug, "Wallet creation") + reset.confirm_new_wallet(debug) # confirm back up reset.confirm_read(debug, "Success") @@ -60,119 +67,54 @@ def test_reset_slip39_advanced_2of2groups_2of2shares( # confirm checklist reset.confirm_read(debug, "Checklist") - # set num of groups - reset.set_selection(debug, buttons.RESET_MINUS, 3) + # set num of groups - default is 5 + if group_count < 5: + reset.set_selection(debug, buttons.RESET_MINUS, 5 - group_count) + else: + reset.set_selection(debug, buttons.RESET_PLUS, group_count - 5) # confirm checklist reset.confirm_read(debug, "Checklist") # set group threshold - reset.set_selection(debug, buttons.RESET_MINUS, 0) + # TODO: could make it general as well + if group_count == 2 and group_threshold == 2: + reset.set_selection(debug, buttons.RESET_PLUS, 0) + elif group_count == 16 and group_threshold == 16: + reset.set_selection(debug, buttons.RESET_PLUS, 11) + else: + raise RuntimeError("not a supported combination") # confirm checklist reset.confirm_read(debug, "Checklist") # set share num and threshold for groups - for _ in range(2): - # set num of shares - reset.set_selection(debug, buttons.RESET_MINUS, 3) + for _ in range(group_count): + # set num of shares - default is 5 + if share_count < 5: + reset.set_selection(debug, buttons.RESET_MINUS, 5 - share_count) + else: + reset.set_selection(debug, buttons.RESET_PLUS, share_count - 5) # set share threshold - reset.set_selection(debug, buttons.RESET_MINUS, 0) + # TODO: could make it general as well + if share_count == 2 and share_threshold == 2: + reset.set_selection(debug, buttons.RESET_PLUS, 0) + elif share_count == 16 and share_threshold == 16: + reset.set_selection(debug, buttons.RESET_PLUS, 11) + else: + raise RuntimeError("not a supported combination") - # confirm backup warning - reset.confirm_read(debug, "Caution") + # confirm backup warning (hold-to-confirm on TR) + reset.confirm_read(debug, "Caution", hold=True) all_words: list[str] = [] - for _ in range(2): - for _ in range(2): + for _ in range(group_count): + for _ in range(share_count): # read words - words = reset.read_words(debug, True) - - # confirm words - reset.confirm_words(debug, words) - - # confirm share checked - reset.confirm_read(debug, "Success") - - all_words.append(" ".join(words)) - - # confirm backup done - reset.confirm_read(debug, "Success") - - # generate secret locally - internal_entropy = debug.state().reset_entropy - assert internal_entropy is not None - secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) - - # validate that all combinations will result in the correct master secret - reset.validate_mnemonics(all_words, secret) - - assert device_handler.result() == "Initialized" - - features = device_handler.features() - assert features.initialized is True - assert features.needs_backup is False - assert features.pin_protection is False - assert features.passphrase_protection is False - assert features.backup_type is messages.BackupType.Slip39_Advanced - - -@pytest.mark.skip_t1 -@pytest.mark.setup_client(uninitialized=True) -@pytest.mark.slow -@with_mock_urandom -def test_reset_slip39_advanced_16of16groups_16of16shares( - device_handler: "BackgroundDeviceHandler", -): - features = device_handler.features() - debug = device_handler.debuglink() - - assert features.initialized is False - - device_handler.run( - device.reset, - backup_type=messages.BackupType.Slip39_Advanced, - pin_protection=False, - ) - - # confirm new wallet - reset.confirm_wait(debug, "Wallet creation") - - # confirm back up - reset.confirm_read(debug, "Success") - - # confirm checklist - reset.confirm_read(debug, "Checklist") - - # set num of groups - reset.set_selection(debug, buttons.RESET_PLUS, 11) - - # confirm checklist - reset.confirm_read(debug, "Checklist") - - # set group threshold - reset.set_selection(debug, buttons.RESET_PLUS, 11) - - # confirm checklist - reset.confirm_read(debug, "Checklist") - - # set share num and threshold for groups - for _ in range(16): - # set num of shares - reset.set_selection(debug, buttons.RESET_PLUS, 11) - - # set share threshold - reset.set_selection(debug, buttons.RESET_PLUS, 11) - - # confirm backup warning - reset.confirm_read(debug, "Caution") - - all_words: list[str] = [] - for _ in range(16): - for _ in range(16): - # read words - words = reset.read_words(debug, True) + words = reset.read_words( + debug, messages.BackupType.Slip39_Advanced, do_htc=False + ) # confirm words reset.confirm_words(debug, words) diff --git a/tests/click_tests/test_reset_slip39_basic.py b/tests/click_tests/test_reset_slip39_basic.py index afae51f1ed..922e81ff25 100644 --- a/tests/click_tests/test_reset_slip39_basic.py +++ b/tests/click_tests/test_reset_slip39_basic.py @@ -15,68 +15,83 @@ # If not, see . from typing import TYPE_CHECKING -from unittest import mock import pytest from trezorlib import device, messages from .. import buttons -from ..common import generate_entropy +from ..common import EXTERNAL_ENTROPY, WITH_MOCK_URANDOM, generate_entropy from . import reset if TYPE_CHECKING: from ..device_handler import BackgroundDeviceHandler -EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 +pytestmark = [pytest.mark.skip_t1] -@pytest.mark.skip_t1 +@pytest.mark.parametrize( + "num_of_shares, threshold", + [ + pytest.param(1, 1, id="1of1"), + pytest.param(16, 16, id="16of16"), + ], +) @pytest.mark.setup_client(uninitialized=True) -def test_reset_slip39_basic_1of1(device_handler: "BackgroundDeviceHandler"): +@WITH_MOCK_URANDOM +def test_reset_slip39_basic( + device_handler: "BackgroundDeviceHandler", num_of_shares: int, threshold: int +): features = device_handler.features() debug = device_handler.debuglink() assert features.initialized is False - os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY) - with mock.patch("os.urandom", os_urandom), device_handler: - device_handler.run( - device.reset, - strength=128, - backup_type=messages.BackupType.Slip39_Basic, - pin_protection=False, - ) + device_handler.run( + device.reset, + strength=128, + backup_type=messages.BackupType.Slip39_Basic, + pin_protection=False, + ) - # confirm new wallet - reset.confirm_wait(debug, "Wallet creation") + # confirm new wallet + reset.confirm_new_wallet(debug) - # confirm back up - reset.confirm_read(debug, "Success") + # confirm back up + reset.confirm_read(debug, "Success") - # confirm checklist - reset.confirm_read(debug, "Checklist") + # confirm checklist + reset.confirm_read(debug, "Checklist") - # set num of shares - # default is 5 so we press RESET_MINUS 4 times - reset.set_selection(debug, buttons.RESET_MINUS, 4) + # set num of shares - default is 5 + if num_of_shares < 5: + reset.set_selection(debug, buttons.RESET_MINUS, 5 - num_of_shares) + else: + reset.set_selection(debug, buttons.RESET_PLUS, num_of_shares - 5) - # confirm checklist - reset.confirm_read(debug, "Checklist") + # confirm checklist + reset.confirm_read(debug, "Checklist") - # set threshold - # threshold will default to 1 - reset.set_selection(debug, buttons.RESET_MINUS, 0) + # set threshold + # TODO: could make it general as well + if num_of_shares == 1 and threshold == 1: + reset.set_selection(debug, buttons.RESET_PLUS, 0) + elif num_of_shares == 16 and threshold == 16: + reset.set_selection(debug, buttons.RESET_PLUS, 11) + else: + raise RuntimeError("not a supported combination") - # confirm checklist - reset.confirm_read(debug, "Checklist") + # confirm checklist + reset.confirm_read(debug, "Checklist") - # confirm backup warning - reset.confirm_read(debug, "Caution") + # confirm backup warning (hold-to-confirm on TR) + reset.confirm_read(debug, "Caution", hold=True) + all_words: list[str] = [] + for _ in range(num_of_shares): # read words - words = reset.read_words(debug) + words = reset.read_words(debug, messages.BackupType.Slip39_Basic) # confirm words reset.confirm_words(debug, words) @@ -84,98 +99,23 @@ def test_reset_slip39_basic_1of1(device_handler: "BackgroundDeviceHandler"): # confirm share checked reset.confirm_read(debug, "Success") - # confirm backup done - reset.confirm_read(debug, "Success") + all_words.append(" ".join(words)) - # generate secret locally - internal_entropy = debug.state().reset_entropy - assert internal_entropy is not None - secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + # confirm backup done + reset.confirm_read(debug, "Success") - # validate that all combinations will result in the correct master secret - validate = [" ".join(words)] - reset.validate_mnemonics(validate, secret) + # generate secret locally + internal_entropy = debug.state().reset_entropy + assert internal_entropy is not None + secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) - assert device_handler.result() == "Initialized" - features = device_handler.features() - assert features.initialized is True - assert features.needs_backup is False - assert features.pin_protection is False - assert features.passphrase_protection is False - assert features.backup_type is messages.BackupType.Slip39_Basic + # validate that all combinations will result in the correct master secret + reset.validate_mnemonics(all_words, secret) - -@pytest.mark.skip_t1 -@pytest.mark.setup_client(uninitialized=True) -def test_reset_slip39_basic_16of16(device_handler: "BackgroundDeviceHandler"): + assert device_handler.result() == "Initialized" features = device_handler.features() - debug = device_handler.debuglink() - - assert features.initialized is False - - os_urandom = mock.Mock(return_value=EXTERNAL_ENTROPY) - with mock.patch("os.urandom", os_urandom), device_handler: - device_handler.run( - device.reset, - strength=128, - backup_type=messages.BackupType.Slip39_Basic, - pin_protection=False, - ) - - # confirm new wallet - reset.confirm_wait(debug, "Wallet creation") - - # confirm back up - reset.confirm_read(debug, "Success") - - # confirm checklist - reset.confirm_read(debug, "Checklist") - - # set num of shares - # default is 5 so we add 11 - reset.set_selection(debug, buttons.RESET_PLUS, 11) - - # confirm checklist - reset.confirm_read(debug, "Checklist") - - # set threshold - # default is 5 so we add 11 - reset.set_selection(debug, buttons.RESET_PLUS, 11) - - # confirm checklist - reset.confirm_read(debug, "Checklist") - - # confirm backup warning - reset.confirm_read(debug, "Caution") - - all_words: list[str] = [] - for _ in range(16): - # read words - words = reset.read_words(debug) - - # confirm words - reset.confirm_words(debug, words) - - # confirm share checked - reset.confirm_read(debug, "Success") - - all_words.append(" ".join(words)) - - # confirm backup done - reset.confirm_read(debug, "Success") - - # generate secret locally - internal_entropy = debug.state().reset_entropy - assert internal_entropy is not None - secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) - - # validate that all combinations will result in the correct master secret - reset.validate_mnemonics(all_words, secret) - - assert device_handler.result() == "Initialized" - features = device_handler.features() - assert features.initialized is True - assert features.needs_backup is False - assert features.pin_protection is False - assert features.passphrase_protection is False - assert features.backup_type is messages.BackupType.Slip39_Basic + assert features.initialized is True + assert features.needs_backup is False + assert features.pin_protection is False + assert features.passphrase_protection is False + assert features.backup_type is messages.BackupType.Slip39_Basic