diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index aa10616..b4c8417 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -17,6 +17,11 @@ on: - develop pull_request: +permissions: + contents: write + actions: write + pull-requests: write + jobs: build: name: Build application using the reusable workflow diff --git a/.github/workflows/codeql-workflow.yml b/.github/workflows/codeql-workflow.yml index 63b58b5..0daf793 100644 --- a/.github/workflows/codeql-workflow.yml +++ b/.github/workflows/codeql-workflow.yml @@ -12,33 +12,12 @@ on: - '.github/workflows/*.yml' - 'tests/*' +permissions: + contents: read + security-events: write + jobs: analyse: - name: Analyse - strategy: - fail-fast: false - matrix: - sdk: ["$NANOX_SDK", "$NANOSP_SDK", "$STAX_SDK", "$FLEX_SDK", "$APEX_P_SDK"] - # 'cpp' covers C and C++ - language: ['cpp'] - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest - - steps: - - name: Clone - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-and-quality - - # CodeQL will create the database during the compilation - - name: Build - run: | - make BOLOS_SDK=${{ matrix.SDK }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + name: Call Ledger CodeQL analysis + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_codeql_checks.yml@v1 + secrets: inherit diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 1694c15..9614f1e 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -9,19 +9,13 @@ on: - develop pull_request: +permissions: + contents: read + jobs: misspell: name: Check misspellings - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - - name: Check misspellings - uses: codespell-project/actions-codespell@v2 - with: - builtin: clear,rare - check_filenames: true - ignore_words_list: ontop - skip: ./clients/wui/yarn.lock, + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_spell_check.yml@v1 + with: + ignore_words_list: ontop + skip_path: ./clients/wui/yarn.lock, diff --git a/.github/workflows/guidelines_enforcer.yml b/.github/workflows/guidelines_enforcer.yml index 5ab28b9..6c6c95f 100644 --- a/.github/workflows/guidelines_enforcer.yml +++ b/.github/workflows/guidelines_enforcer.yml @@ -9,6 +9,10 @@ on: - develop pull_request: +permissions: + contents: read + actions: write + jobs: guidelines_enforcer: name: Call Ledger guidelines_enforcer diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml index 5654ab9..ed17973 100644 --- a/.github/workflows/lint-workflow.yml +++ b/.github/workflows/lint-workflow.yml @@ -10,6 +10,9 @@ on: - '.github/workflows/*.yml' - 'tests/*' +permissions: + contents: read + jobs: check_linting: name: Check linting using the reusable workflow @@ -20,7 +23,4 @@ jobs: yamllint: name: Check yaml files - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: yamllint . + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_yaml_lint.yml@v1 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 63f5115..c7e1776 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -9,57 +9,12 @@ on: - develop pull_request: +permissions: + contents: read + actions: write + jobs: job_unit_test: - name: Unit test - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest - - steps: - - name: Clone - uses: actions/checkout@v4 - - - name: Install cmocka - run: | - apt update && apt install -y libcmocka-dev lcov libbsd-dev - - - name: Compile the tests - run: | - cd tests/unit/ - rm -rf build/ - cmake -B build -H. - make -C build - - - name: Run the tests - run: | - cd tests/unit/ - CTEST_OUTPUT_ON_FAILURE=1 make -C build test - - - name: Generate code coverage - run: | - cd tests/unit/ - lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base - lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture - lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info - lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/tests/unit/*' -o coverage.info - genhtml coverage.info -o coverage - - - uses: actions/upload-artifact@v4 - with: - name: code-coverage - path: tests/unit/coverage - - - name: Install codecov dependencies - run: apt install --no-install-recommends -y curl gpg - - - name: Upload to codecov.io - uses: codecov/codecov-action@v5 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - with: - files: ./tests/unit/coverage.info - flags: unittests - name: codecov-app-passwords - fail_ci_if_error: true - verbose: true + name: Call Ledger unit_test + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_unit_tests.yml@v1 + secrets: inherit diff --git a/.github/workflows/wui.yml b/.github/workflows/wui.yml index 800c15b..e40e811 100644 --- a/.github/workflows/wui.yml +++ b/.github/workflows/wui.yml @@ -11,29 +11,35 @@ on: - develop - master +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + jobs: generate: name: Generate the site runs-on: ubuntu-latest + defaults: + run: + working-directory: clients/wui/ steps: - name: Clone - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set Node.js 16.x uses: actions/setup-node@v5 with: node-version: 16.x + cache: yarn + cache-dependency-path: clients/wui/yarn.lock - name: Run install - uses: borales/actions-yarn@v4 - with: - cmd: install - dir: clients/wui/ + run: yarn install --frozen-lockfile - name: Run build - uses: borales/actions-yarn@v4 - with: - cmd: build - dir: clients/wui/ + run: yarn build - name: Upload documentation bundle - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: site path: clients/wui/build/ @@ -42,6 +48,8 @@ jobs: name: Deploy the site on GitHub pages runs-on: ubuntu-latest needs: generate + permissions: + contents: write if: | github.event_name == 'push' && (github.ref == 'refs/heads/master' || @@ -49,9 +57,13 @@ jobs: startsWith(github.ref, 'refs/tags/')) steps: - name: Download documentation bundle - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 + with: + name: site + path: site - name: Deploy documentation on pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: site + cname: passwords.ledger.com diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba6da8..897fe09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.1] - 2026-06-01 + +### Fix + +- Reject password creation when the chosen nickname is already in use. +- Hide actions that require existing passwords from the main menu when the + database is empty (only "Create a new password" is exposed in that case). +- Settings switches on the second page (separators, special characters, "press + enter") no longer display a stale value after toggling and re-entering the + settings. + +### Change + +- Reorder the main menu so "Create a new password" appears first. +- WUI deployment workflow: replace the third-party `borales/actions-yarn` + action with native `yarn` steps, enable yarn dependency caching via + `setup-node`, and set least-privilege `GITHUB_TOKEN` permissions. +- Pin the `passwords.ledger.com` custom domain in the deploy step (`cname`) + so it survives every GitHub Pages publish. + +### Add + +- Functional tests support Nano S+ / Nano X (Ragger button-based navigator). +- Unit tests for the new `nickname_exists` check. + ## [1.3.0] - 2025-09-11 ### Change diff --git a/Makefile b/Makefile index dbeadc3..4721126 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ APPNAME ="Passwords" # Application version APPVERSION_M=1 APPVERSION_N=3 -APPVERSION_P=0 +APPVERSION_P=1 APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P) # Application source files diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index e556d6a..0000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -markers = - requires_phyical_device: mark a test that should run only with a real device \ No newline at end of file diff --git a/src/metadata.c b/src/metadata.c index b32e044..ff7b804 100644 --- a/src/metadata.c +++ b/src/metadata.c @@ -62,7 +62,7 @@ uint32_t get_metadata(uint32_t nth) { unsigned int offset = 0; for (;;) { if (METADATA_DATALEN(offset) == 0) { - return -1UL; // end of file + return UINT32_MAX; // end of file } if (METADATA_KIND(offset) != META_ERASED) { if (nth == 0) { diff --git a/src/password.c b/src/password.c index 19767eb..6c44e57 100644 --- a/src/password.c +++ b/src/password.c @@ -43,3 +43,24 @@ void show_password_at_offset(const size_t offset, uint8_t* dest_buffer) { error_type_t delete_password_at_offset(const size_t offset) { return erase_metadata(offset); } + +bool nickname_exists(const char* const pwd_name, const size_t pwd_size) { + // write_metadata silently caps the total data block at MAX_METANAME bytes + // (1 charset byte + nickname), so any nickname longer than MAX_METANAME - 1 + // is truncated at storage time. Mirror that truncation here, otherwise a + // user could bypass duplicate detection just by typing a name longer than + // what the device can actually persist. + const size_t effective_size = + (pwd_size > (size_t) (MAX_METANAME - 1)) ? (size_t) (MAX_METANAME - 1) : pwd_size; + for (size_t i = 0; i < N_storage.metadata_count; i++) { + uint32_t offset = get_metadata(i); + if (offset == UINT32_MAX) { + break; + } + if (METADATA_NICKNAME_LEN(offset) == effective_size && + memcmp((const void*) METADATA_NICKNAME(offset), pwd_name, effective_size) == 0) { + return true; + } + } + return false; +} diff --git a/src/password.h b/src/password.h index 6ac77dc..2843925 100644 --- a/src/password.h +++ b/src/password.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include "error.h" @@ -14,3 +15,4 @@ error_type_t create_new_password(const char* const pwd_name, const size_t pwd_si void type_password_at_offset(const size_t offset); void show_password_at_offset(const size_t offset, uint8_t* dest_buffer); error_type_t delete_password_at_offset(const size_t offset); +bool nickname_exists(const char* const pwd_name, const size_t pwd_size); diff --git a/src/password_list.c b/src/password_list.c index eba93e9..4cd5b29 100644 --- a/src/password_list.c +++ b/src/password_list.c @@ -45,7 +45,7 @@ void password_list_reset() { size_t password_list_get_offset(const size_t index) { if (index >= MAX_METADATA_COUNT) { - PRINTF("[password_list_get_offset] Index %d out of bounds\n", index); + PRINTF("[password_list_get_offset] Index %ld out of bounds\n", index); return -1; } return passwordList.offsets[index]; @@ -57,7 +57,7 @@ size_t password_list_get_current_offset() { const char *password_list_get_password(const size_t index) { if (index >= MAX_METADATA_COUNT) { - PRINTF("[password_list_get_password] Index %d out of bounds\n", index); + PRINTF("[password_list_get_password] Index %ld out of bounds\n", index); return NULL; } return passwordList.passwords[index]; @@ -72,7 +72,7 @@ bool password_list_add_password(const size_t index, const char *const password, const size_t length) { if (index >= MAX_METADATA_COUNT) { - PRINTF("[password_list_add_password] Index %d out of bounds\n", index); + PRINTF("[password_list_add_password] Index %ld out of bounds\n", index); return false; } passwordList.offsets[index] = offset; diff --git a/src/ui/menu_nbgl.c b/src/ui/menu_nbgl.c index 7e64cde..5c565d1 100644 --- a/src/ui/menu_nbgl.c +++ b/src/ui/menu_nbgl.c @@ -43,6 +43,9 @@ enum { EXT_SYMBOLS_TOKEN, NO_ENTER_TOKEN, KBD_TOKEN, +#ifndef SCREEN_SIZE_WALLET + KBD_BAR_TOKEN, +#endif CHOICE_WRITE_TOKEN, CHOICE_DISPLAY_TOKEN, CHOICE_CREATE_TOKEN, @@ -87,9 +90,6 @@ enum { static void display_home_page(void); -// settings init display page index (allows to target keyboard layout) -uint8_t initSettingPage; - // Settings switches static nbgl_contentSwitch_t switches[SETTINGS_SWITCHES_NB] = {0}; @@ -98,9 +98,9 @@ static const char *const availableKbd[KBD_OPTIONS_NB] = {"QWERTY", "QWERTY INT." bool kbdMappingInit = false; // App info -#define SETTING_INFO_NB 2 -static const char* const INFO_TYPES[SETTING_INFO_NB] = {"Version", "Developer"}; -static const char* const INFO_CONTENTS[SETTING_INFO_NB] = {APPVERSION, "(c) 2017-2025 Ledger"}; +#define SETTING_INFO_NB 3 +static const char* const INFO_TYPES[SETTING_INFO_NB] = {"Version", "Developer", "Copyright"}; +static const char* const INFO_CONTENTS[SETTING_INFO_NB] = {APPVERSION, "Ledger", "Ledger (c) 2026"}; static const nbgl_contentInfoList_t infoList = { .nbInfos = SETTING_INFO_NB, .infoTypes = INFO_TYPES, @@ -116,19 +116,33 @@ static const nbgl_genericContents_t settingContents = { }; static nbgl_homeAction_t homeAction = {0}; +// "Host keyboard" is the BARS_LIST label on Nano AND the sub-menu / +// first-launch page title on both platforms. Keep one source of truth. +static const char *const kbdSettingsBarsTexts[1] = {"Host keyboard"}; + +#ifdef SCREEN_SIZE_NANO +// Nano-only: the keyboard layout CHOICES_LIST lives outside the inline +// settings flow. Reused by the first-launch post-disclaimer +// page and the settings sub-menu reached from the BARS_LIST entry. +static nbgl_content_t kbdLayoutChoicesContent = {0}; +static const uint8_t kbdSettingsBarsTokens[1] = {KBD_BAR_TOKEN}; +#endif + // Main Action menu choices #define ACTIONS_NB 5 +// "Create" stays first so it remains reachable when the database is empty +// (see choice_navigation_callback: nbBars shrinks to 1 in that case). static const char *const barsTexts[ACTIONS_NB] = { + "Create a new password", "Type a password", "Show a password", - "Create a new password", "Delete a password", "Delete all passwords" }; static const uint8_t barsToken[ACTIONS_NB] = { + CHOICE_CREATE_TOKEN, CHOICE_WRITE_TOKEN, CHOICE_DISPLAY_TOKEN, - CHOICE_CREATE_TOKEN, CHOICE_DELETE_TOKEN, CHOICE_DELETE_ALL_TOKENS }; @@ -138,6 +152,43 @@ pwd_actionCallback_t selector_callback = NULL; // clang-format on +#ifdef SCREEN_SIZE_NANO +/** + * @brief Quit callback for the layout sub-menu: re-enter settings on the + * "Host keyboard" page. The Nano horizontal flow numbers + * steps 0..SETTINGS_SWITCHES_NB-1 for the switches, then + * SETTINGS_SWITCHES_NB for the BARS_LIST entry. + */ +static void return_to_settings(void) { + nbgl_useCaseHomeAndSettings(APPNAME, + &ICON_APP_HOME, + "Manage passwords on your device", + SETTINGS_SWITCHES_NB, + &settingContents, + &infoList, + &homeAction, + app_exit); +} + +/** + * @brief Nano-only: settings sub-menu opened from the "Host keyboard" + * BARS_LIST entry. Reuses kbdLayoutChoicesContent (same CHOICES_LIST + * as the first-launch page). Back returns to the settings page on + * the BARS_LIST step. + */ +static void display_kbd_sub_menu(void) { + static const nbgl_genericContents_t kbdChoiceContents = { + .callbackCallNeeded = false, + .contentsList = &kbdLayoutChoicesContent, + .nbContents = 1, + }; + nbgl_useCaseGenericConfiguration(kbdSettingsBarsTexts[0], + 0, + &kbdChoiceContents, + return_to_settings); +} +#endif + /** * @brief Settings Action callback * @@ -147,27 +198,35 @@ pwd_actionCallback_t selector_callback = NULL; * */ static void controls_callback(int token, uint8_t index, int page) { - UNUSED(index); UNUSED(page); + // NBGL re-reads switches[].initState on every page re-render, so we must + // keep it in sync with NVM after each toggle (otherwise an exit/re-enter + // would show the pre-toggle value). switch (token) { case UPPERCASE_TOKEN: set_charset_option(UPPERCASE_BITFLAG); + switches[UPPERCASE_ID].initState = has_charset_option(UPPERCASE_BITFLAG); break; case LOWERCASE_TOKEN: set_charset_option(LOWERCASE_BITFLAG); + switches[LOWERCASE_ID].initState = has_charset_option(LOWERCASE_BITFLAG); break; case NUMBERS_TOKEN: set_charset_option(NUMBERS_BITFLAG); + switches[NUMBERS_ID].initState = has_charset_option(NUMBERS_BITFLAG); break; case BARS_TOKEN: set_charset_option(BARS_BITFLAG); + switches[BARS_ID].initState = has_charset_option(BARS_BITFLAG); break; case EXT_SYMBOLS_TOKEN: set_charset_option(EXT_SYMBOLS_BITFLAG); + switches[EXT_SYMBOLS_ID].initState = has_charset_option(EXT_SYMBOLS_BITFLAG); break; case NO_ENTER_TOKEN: change_enter_options(); + switches[NO_ENTER_ID].initState = N_storage.press_enter_after_typing; break; case KBD_TOKEN: switch (index) { @@ -183,12 +242,22 @@ static void controls_callback(int token, uint8_t index, int page) { default: break; } +#ifdef SCREEN_SIZE_WALLET contents[SETTING_KBD_TYPE].content.choicesList.initChoice = index; +#else + kbdLayoutChoicesContent.content.choicesList.initChoice = index; +#endif + // Auto-exit only on the first-launch (post-disclaimer) flow. if (kbdMappingInit) { kbdMappingInit = false; display_home_page(); } break; +#ifndef SCREEN_SIZE_WALLET + case KBD_BAR_TOKEN: + display_kbd_sub_menu(); + break; +#endif default: break; } @@ -256,18 +325,41 @@ static void init_settings(void) { contents[SETTING_OPTIONS].content.switchesList.switches = switches; contents[SETTING_OPTIONS].contentActionCallback = controls_callback; +#ifdef SCREEN_SIZE_WALLET contents[SETTING_KBD_TYPE].type = CHOICES_LIST; contents[SETTING_KBD_TYPE].content.choicesList.nbChoices = KBD_OPTIONS_NB; contents[SETTING_KBD_TYPE].content.choicesList.names = availableKbd; contents[SETTING_KBD_TYPE].content.choicesList.token = KBD_TOKEN; contents[SETTING_KBD_TYPE].contentActionCallback = controls_callback; -#ifdef HAVE_PIEZO_SOUND contents[SETTING_KBD_TYPE].content.choicesList.tuneId = TUNE_TAP_CASUAL; -#endif if (N_storage.keyboard_layout != HID_MAPPING_NONE) { contents[SETTING_KBD_TYPE].content.choicesList.initChoice = N_storage.keyboard_layout - 1; } +#else + // Nano: settings show a single "Host keyboard" bar; selecting it opens + // the layout CHOICES_LIST in a sub-menu (kbdLayoutChoicesContent). The + // SDK automatically draws a downward chevron at the bottom of every + // BARS_LIST bar on Nano (counterpart of Stax/Flex's PUSH_ICON), so no + // app-side icon configuration is needed. + contents[SETTING_KBD_TYPE].type = BARS_LIST; + contents[SETTING_KBD_TYPE].content.barsList.nbBars = 1; + contents[SETTING_KBD_TYPE].content.barsList.barTexts = kbdSettingsBarsTexts; + contents[SETTING_KBD_TYPE].content.barsList.tokens = kbdSettingsBarsTokens; + contents[SETTING_KBD_TYPE].contentActionCallback = controls_callback; + + kbdLayoutChoicesContent.type = CHOICES_LIST; + kbdLayoutChoicesContent.content.choicesList.nbChoices = KBD_OPTIONS_NB; + kbdLayoutChoicesContent.content.choicesList.names = availableKbd; + kbdLayoutChoicesContent.content.choicesList.token = KBD_TOKEN; + kbdLayoutChoicesContent.contentActionCallback = controls_callback; + kbdLayoutChoicesContent.content.choicesList.selectionIcon = &C_icon_validate_10; + kbdLayoutChoicesContent.content.choicesList.title = kbdSettingsBarsTexts[0]; + + if (N_storage.keyboard_layout != HID_MAPPING_NONE) { + kbdLayoutChoicesContent.content.choicesList.initChoice = N_storage.keyboard_layout - 1; + } +#endif // Initialize Home page Action // --------------------------- @@ -317,7 +409,7 @@ static void choice_callback(const int token, const uint8_t index) { static bool choice_navigation_callback(const uint8_t page, nbgl_pageContent_t *content) { UNUSED(page); content->type = BARS_LIST; - content->barsList.nbBars = ACTIONS_NB; + content->barsList.nbBars = (N_storage.metadata_count == 0) ? 1 : ACTIONS_NB; content->barsList.barTexts = barsTexts; content->barsList.tokens = barsToken; #ifdef HAVE_PIEZO_SOUND @@ -356,13 +448,60 @@ static void display_home_page(void) { #else "Manage passwords on your device", #endif - initSettingPage, + INIT_HOME_PAGE, &settingContents, &infoList, &homeAction, app_exit); } +/** + * @brief Back/quit callback while on the first-launch keyboard layout page + * @note If the user exits without picking a layout, default to QWERTY in NVM + * (rather than leaving keyboard_layout at HID_MAPPING_NONE, which would + * force the disclaimer + layout page to re-appear at every startup + * while still silently behaving like QWERTY at typing time). The user + * can change the layout later via the regular settings. + * kbdMappingInit is also cleared so a subsequent layout selection + * through the regular settings does not unexpectedly re-trigger the + * transition-to-home redirect in controls_callback(). + */ +static void startup_kbd_quit_callback(void) { + set_keyboard_layout(HID_MAPPING_QWERTY); +#ifdef SCREEN_SIZE_WALLET + contents[SETTING_KBD_TYPE].content.choicesList.initChoice = KBD_QWERTY_ID; +#else + kbdLayoutChoicesContent.content.choicesList.initChoice = KBD_QWERTY_ID; +#endif + kbdMappingInit = false; + display_home_page(); +} + +/** + * @brief First-launch keyboard layout selection page + * @note Shown right after the disclaimer is accepted. We deliberately use + * nbgl_useCaseGenericConfiguration with only the KBD_TYPE content + * rather than jumping into the full settings flow: this keeps the + * startup navigation isolated from the home/settings navigation. + * Once the user picks a layout, controls_callback's KBD_TOKEN branch + * clears kbdMappingInit and calls display_home_page(). + */ +static void display_kbd_layout_selection(void) { + static const nbgl_genericContents_t kbdLayoutOnlyContents = { + .callbackCallNeeded = false, +#ifdef SCREEN_SIZE_WALLET + .contentsList = &contents[SETTING_KBD_TYPE], +#else + .contentsList = &kbdLayoutChoicesContent, +#endif + .nbContents = 1, + }; + nbgl_useCaseGenericConfiguration(kbdSettingsBarsTexts[0], + 0, + &kbdLayoutOnlyContents, + startup_kbd_quit_callback); +} + /** * @brief UI startup callback * @note At 1st init of the app, it allows the user to select the keyboard type @@ -371,22 +510,9 @@ static void display_home_page(void) { * */ static void startup_callback(bool confirm) { - uint8_t index = 0; - if (confirm) { kbdMappingInit = true; - initSettingPage = 0; - // Find the page index after the switches, where the settings show the keyboard type - while (index < SETTINGS_SWITCHES_NB) { - index += - nbgl_useCaseGetNbSwitchesInPage(SETTINGS_SWITCHES_NB - index, - &contents[SETTING_OPTIONS].content.switchesList, - index, - false); - initSettingPage++; - } - display_home_page(); - initSettingPage = INIT_HOME_PAGE; + display_kbd_layout_selection(); } else { app_exit(); } @@ -397,7 +523,6 @@ static void startup_callback(bool confirm) { * */ void ui_idle(void) { - initSettingPage = INIT_HOME_PAGE; init_settings(); // First start: the keyboard layout is not selected yet if (N_storage.keyboard_layout == HID_MAPPING_NONE) { diff --git a/src/ui/ui_passwords.c b/src/ui/ui_passwords.c index 7a03a96..ce9c3d1 100644 --- a/src/ui/ui_passwords.c +++ b/src/ui/ui_passwords.c @@ -46,24 +46,14 @@ static char msgBuffer[MAX_MSG_BUFFER_SIZE] = {0}; static char password_to_display[PASSWORD_MAX_SIZE + 1] = {0}; static const char *ptrToPwd[2] = {0}; -// Pointer to the layout context -static nbgl_layout_t *layoutContext = NULL; - // Flag to indicate if all passwords are being deleted static bool all_passwords; // Keyboard contexts static char password_name[MAX_METANAME + 1] = {0}; -static int keyboardIndex = 0; static nbgl_genericContents_t genericContent = {0}; static nbgl_content_t contentsList = {0}; -#ifdef SCREEN_SIZE_WALLET -static nbgl_layoutConfirmationButton_t confirmButton = {0}; -static nbgl_layoutKeyboardContent_t keyboardContent = {0}; -#else -static int textIndex = 0; -#endif static size_t nbPasswordsPerPage = 0; /** @@ -92,17 +82,6 @@ static void display_success_page(const char *string) { nbgl_useCaseStatus(string, true, display_choice_page); } -/** - * @brief Cleanup UI context - * - */ -static void release_context(void) { - if (layoutContext != NULL) { - nbgl_layoutRelease(layoutContext); - layoutContext = NULL; - } -} - /** * @brief Delete password * @@ -188,6 +167,10 @@ void confirm_password_deletion_cb(const size_t index) { * */ void confirm_all_passwords_deletion(void) { + if (N_storage.metadata_count == 0) { + nbgl_useCaseStatus("No passwords available", false, display_choice_page); + return; + } confirm_password_deletion(-1); } @@ -218,7 +201,7 @@ void display_password_list(void) { explicit_bzero(&contentsList, sizeof(contentsList)); while (nbPasswords < N_storage.metadata_count) { pwdOffset = get_metadata(nbPasswords); - if (pwdOffset == -1UL) { + if (pwdOffset == UINT32_MAX) { break; } const size_t pwdLength = METADATA_NICKNAME_LEN(pwdOffset) + 1; @@ -292,6 +275,8 @@ static void create_password(void) { const size_t password_size = strlen(password_name); if (password_size == 0) { nbgl_useCaseStatus("The nickname\ncan't be empty", false, &display_create_pwd); + } else if (nickname_exists(password_name, password_size)) { + nbgl_useCaseStatus("This nickname\nalready exists", false, &display_create_pwd); } else { error_type_t error = create_new_password(password_name, password_size); if (error == OK) { @@ -302,155 +287,36 @@ static void create_password(void) { } } -#ifdef SCREEN_SIZE_WALLET -/** - * @brief Keyboard control callback - * - * @param[in] token button Id pressed - * @param[in] index widget index on the page - * - */ -static void keyboard_control_callback(const int token, const uint8_t index) { - UNUSED(index); - switch (token) { - case BACK_BUTTON_TOKEN: - release_context(); - display_choice_page(); - break; - case CREATE_TOKEN: - create_password(); - break; - default: - break; - } -}; -#endif - -/** - * @brief Keyboard press callback - * - * @param[in] touchedKey key pressed - * - */ -static void key_press_callback(const char touchedKey) { - uint32_t mask = 0; - size_t textLen = strlen(password_name); - if (touchedKey == BACKSPACE_KEY) { - if (textLen == 0) { - return; - } - password_name[--textLen] = '\0'; -#ifdef SCREEN_SIZE_NANO - } else if (touchedKey == VALIDATE_KEY) { - create_password(); - return; -#endif - } else { - password_name[textLen] = touchedKey; - password_name[++textLen] = '\0'; - } - if (textLen >= MAX_METANAME) { - // password name length can't be greater than MAX_METANAME, so we mask - // every characters - mask = -1; - } -#ifdef SCREEN_SIZE_WALLET - nbgl_layoutUpdateKeyboardContent(layoutContext, &keyboardContent); - nbgl_layoutUpdateKeyboard(layoutContext, keyboardIndex, mask, false, LOWER_CASE); - nbgl_refreshSpecialWithPostRefresh(BLACK_AND_WHITE_REFRESH, POST_REFRESH_FORCE_POWER_ON); -#else - nbgl_layoutUpdateKeyboard(layoutContext, keyboardIndex, mask); - nbgl_layoutUpdateEnteredText(layoutContext, textIndex, password_name); - nbgl_refresh(); -#endif -} - /** * @brief Display the passwords creation page * */ void display_create_pwd(void) { - nbgl_layoutDescription_t layoutDescription = {0}; - nbgl_layoutKbd_t kbdInfo = {.callback = &key_press_callback}; - + nbgl_kbdButtonParams_t confirmParams = { #ifdef SCREEN_SIZE_WALLET - nbgl_layoutHeader_t headerDesc = { - .type = HEADER_BACK_AND_TEXT, - .backAndText.token = BACK_BUTTON_TOKEN, -#ifdef HAVE_PIEZO_SOUND - .backAndText.tuneId = TUNE_TAP_CASUAL, + .buttonText = "Create password", #endif + .onButtonCallback = &create_password, }; - confirmButton = (nbgl_layoutConfirmationButton_t){ - .text = "Create password", - .token = CREATE_TOKEN, - .active = true, - }; - keyboardContent = (nbgl_layoutKeyboardContent_t){ + + nbgl_keyboardParams_t keyboardParams = { .type = KEYBOARD_WITH_BUTTON, +#ifdef SCREEN_SIZE_WALLET .title = "New password nickname", - .text = password_name, - .textToken = KBD_TEXT_TOKEN, - .confirmationButton = confirmButton, -#ifdef HAVE_PIEZO_SOUND - .tuneId = TUNE_TAP_CASUAL, +#else + .title = "Create password", #endif + .entryBuffer = password_name, + .entryMaxLen = sizeof(password_name), + .lettersOnly = false, +#ifdef SCREEN_SIZE_WALLET + .mode = MODE_LETTERS, + .casing = LOWER_CASE, +#else + .mode = MODE_NONE, +#endif + .confirmationParams = confirmParams, }; - layoutDescription.onActionCallback = &keyboard_control_callback; - kbdInfo.mode = MODE_LETTERS; - kbdInfo.casing = LOWER_CASE; password_name[0] = '\0'; - - // Create page layout - release_context(); - layoutContext = nbgl_layoutGet(&layoutDescription); - - // Add header - nbgl_layoutAddHeader(layoutContext, &headerDesc); - - // Add keyboard - keyboardIndex = nbgl_layoutAddKeyboard(layoutContext, &kbdInfo); - if (keyboardIndex < 0) { - // Error - release_context(); - return; - } - - nbgl_layoutAddKeyboardContent(layoutContext, &keyboardContent); - -#else // SCREEN_SIZE_WALLET - - nbgl_layoutCenteredInfo_t centeredInfo = {.text1 = "Create password", .onTop = true}; - nbgl_layoutNavigation_t navInfo = {.direction = HORIZONTAL_NAV, - .indication = LEFT_ARROW | RIGHT_ARROW}; - password_name[0] = '\0'; - - // Create page layout - release_context(); - layoutContext = nbgl_layoutGet(&layoutDescription); - - // add description - nbgl_layoutAddCenteredInfo(layoutContext, ¢eredInfo); - - // Add keyboard - keyboardIndex = nbgl_layoutAddKeyboard(layoutContext, &kbdInfo); - if (keyboardIndex < 0) { - // Error - release_context(); - return; - } - - // add empty entered text - textIndex = nbgl_layoutAddEnteredText(layoutContext, "", true); - if (textIndex < 0) { - // Error - release_context(); - return; - } - nbgl_layoutAddNavigation(layoutContext, &navInfo); - -#endif // SCREEN_SIZE_WALLET - - nbgl_layoutDraw(layoutContext); - nbgl_refresh(); + nbgl_useCaseKeyboard(&keyboardParams, &display_choice_page); } diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index e34f48e..75e989f 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -1,3 +1,7 @@ +# Pytest fixtures are injected by name, so parameters in fixture-consuming +# functions intentionally match module-level fixture names. Silence the noisy +# pylint warning for the whole file rather than per-function. +# pylint: disable=redefined-outer-name import pytest from ragger.backend import RaisePolicy, BackendInterface from ragger.navigator import Navigator @@ -5,6 +9,7 @@ from ledgered.devices import Device from passwordsManager_cmd import PasswordsManagerCommand from tests_vectors import tests_vectors +from nano.navigator import CustomNanoNavigator from touch.navigator import CustomTouchNavigator pytest_plugins = ("ragger.conftest.base_conftest", ) @@ -18,7 +23,7 @@ def pytest_generate_tests(metafunc): @pytest.fixture -def custom_backend(backend): +def custom_backend(backend: BackendInterface): backend.raise_policy = RaisePolicy.RAISE_NOTHING yield backend @@ -31,25 +36,7 @@ def cmd(custom_backend: BackendInterface, navigator: Navigator, device: Device): @pytest.fixture def navigator(custom_backend, device, golden_run): - touchNav = CustomTouchNavigator(custom_backend, device, golden_run) - yield touchNav - - -@pytest.fixture(autouse=True) -def use_on_device(request, device: Device): - if request.node.get_closest_marker('use_on_device'): - dev_list = [d.lower() for d in request.node.get_closest_marker('use_on_device').args[0]] - if device.name not in dev_list: - pytest.skip(f'skipped on this device: "{device}" is not in ' - f'{dev_list}') - - -def pytest_configure(config): - config.addinivalue_line( - "markers", - "use_on_device(device): skip test if not on the specified device", - ) - config.addinivalue_line( - "markers", - "requires_physical_device(): skip test if not on a physical device" - ) + if device.is_nano: + yield CustomNanoNavigator(custom_backend, device, golden_run) + else: + yield CustomTouchNavigator(custom_backend, device, golden_run) diff --git a/tests/functional/nano/__init__.py b/tests/functional/nano/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional/nano/navigator.py b/tests/functional/nano/navigator.py new file mode 100644 index 0000000..10c49ec --- /dev/null +++ b/tests/functional/nano/navigator.py @@ -0,0 +1,258 @@ +from time import sleep + +from ledgered.devices import Device +from ragger.backend import BackendInterface +from ragger.navigator import NavInsID +from ragger.navigator.navigator import Navigator + +# Reuse the same CustomNavInsID enum as the touch navigator so that test files +# can be shared across device families. The touch/ and nano/ directories are +# imported as top-level packages by pytest (no __init__.py at tests/functional/), +# so the import is absolute, not relative. +from touch.navigator import CustomNavInsID + + +# Keyboard layout on Nano, MODE_NONE (see nbgl_obj_keyboard_nanos.c): +# - First a mode-choice screen is shown with three icons (lower, upper, digits) +# mapped to selectedCharIndex 0, 1, 2. both_click commits to that mode. +# - Once a mode is selected, selectedCharIndex cycles through keysByMode[mode]; +# the last three characters are backspace ('\b'), validate ('\n') and the +# mode-switch icon ('\r' -- returns to MODE_NONE). +_KEY_MAPS = ( + "abcdefghijklmnopqrstuvwxyz\b\n\r", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\b\n\r", + "0123456789 '\"`&/?!:;.,~*$=+-[](){}^<>\\_#@|%\b\n\r", +) +_MODE_LOWER = 0 +_MODE_UPPER = 1 +_MODE_DIGITS = 2 + + +class CustomNanoNavigator(Navigator): + """Button-based navigator for Nano S+ / Nano X. + + Layout assumptions (from src/ui/menu_nbgl.c): + - Home (nbgl_useCaseHomeAndSettings) pages: Home, Action, Settings, + Info x N, Quit (each reached via right_click; both_click confirms). + - Main action menu (nbgl_useCaseNavigableContent BARS_LIST): one bar per + page, in the order declared in barsTexts[]. After the bars comes the + implicit "Back" entry. + - Settings (nbgl_useCaseHomeAndSettings settings flow): one page per + switch then keyboard layout choices then "Back". + - Keyboard (nbgl_useCaseKeyboard with MODE_NONE): starts on a mode-choice + screen (lower / upper / digits); see _KEY_MAPS for the per-mode rings. + The navigator tracks (mode, selectedCharIndex) so each WRITE picks the + shortest left/right path to the target character. + """ + + def __init__(self, backend: BackendInterface, device: Device, golden_run: bool = False): + self._backend = backend + # Tracks the on-screen keyboard state between successive instructions. + # `None` means the device is on the mode-choice screen (MODE_NONE) with + # selectedCharIndex pointing at lower-case letters; once a mode is + # picked, _kbd_mode holds its index (0/1/2) and _kbd_index follows + # selectedCharIndex inside keysByMode[mode]. + self._kbd_mode: int | None = None + self._kbd_index: int = 0 + + callbacks = { + NavInsID.WAIT: sleep, + NavInsID.WAIT_FOR_SCREEN_CHANGE: backend.wait_for_screen_change, + # First-launch disclaimer (nbgl_useCaseChoice): the message spans + # several pages on Nano, so step through them before confirming. + CustomNavInsID.DISCLAIMER_CONFIRM: self._disclaimer_confirm, + # First-launch keyboard layout choice (CHOICES_LIST): QWERTY is the + # first entry and shown by default. + CustomNavInsID.CHOOSE_KBL_QWERTY: self._both, + # Home -> action / settings / quit + CustomNavInsID.HOME_TO_MENU: self._home_to_menu, + CustomNavInsID.HOME_TO_SETTINGS: self._home_to_settings, + # Settings navigation + CustomNavInsID.SETTINGS_NEXT: self._right, + CustomNavInsID.SETTINGS_TO_HOME: self._settings_to_home, + # Main menu bars (see src/ui/menu_nbgl.c: Create, Type, Show, + # Delete, Delete all). Each bar is a page on Nano. + CustomNavInsID.MENU_TO_CREATE: lambda: self._menu_select_bar(0), + CustomNavInsID.MENU_TO_TYPE: lambda: self._menu_select_bar(1), + CustomNavInsID.MENU_TO_DISPLAY: lambda: self._menu_select_bar(2), + CustomNavInsID.MENU_TO_DELETE: lambda: self._menu_select_bar(3), + CustomNavInsID.MENU_TO_DELETE_ALL: lambda: self._menu_select_bar(4), + # "Back" entry of the main menu + CustomNavInsID.MENU_TO_HOME: self._menu_to_home, + # Password list (CHOICES_LIST): LIST_CHOOSE(n) selects the n-th + # entry (1-based, matching the touch navigator semantics). + CustomNavInsID.LIST_CHOOSE: self._list_choose, + CustomNavInsID.LIST_TO_MENU: self._list_to_menu, + # Deletion confirmation (nbgl_useCaseChoice): on Nano the message + # paginates before the "Confirm" action page, so step forward until + # that page is on screen and only then commit. + CustomNavInsID.CONFIRM_YES: self._confirm_yes, + # APDU approval prompt + CustomNavInsID.BUTTON_APPROVE: self._both, + # Keyboard (nbgl_useCaseKeyboard with MODE_NONE on Nano). + CustomNavInsID.KEYBOARD_WRITE: self._write, + CustomNavInsID.KEYBOARD_TO_CONFIRM: self._keyboard_confirm, + } + super().__init__(backend, device, callbacks, golden_run=golden_run) + + # ---- low-level helpers ------------------------------------------------- + + def _right(self): + self._backend.right_click() + + def _left(self): + self._backend.left_click() + + def _both(self): + self._backend.both_click() + + def _navigate_until_text(self, text: str, max_steps: int = 12) -> None: + """Press right_click until `text` appears on screen, then return.""" + for _ in range(max_steps): + try: + self._backend.wait_for_text_on_screen(text, timeout=0.5) + return + except Exception: + self._backend.right_click() + raise RuntimeError(f"Could not find text '{text}' on screen after {max_steps} steps") + + # ---- first-launch disclaimer ------------------------------------------ + + def _disclaimer_confirm(self): + # The disclaimer message paginates across 4 screens before the confirm + # action page is reached on Nano. + for _ in range(4): + self._right() + self._both() + + # ---- home -------------------------------------------------------------- + + def _home_to_menu(self): + # Home page is page 0; Action ("Tap to manage") is page 1. + self._right() + self._both() + + def _home_to_settings(self): + # Settings is page 2 from home. + self._right() + self._right() + self._both() + + # ---- settings ---------------------------------------------------------- + + def _settings_to_home(self): + self._navigate_until_text("Back") + self._both() + + # ---- main menu --------------------------------------------------------- + + def _menu_select_bar(self, index: int): + # On entry the menu is at bar 0; navigate forward `index` times and + # confirm. + for _ in range(index): + self._right() + self._both() + + def _menu_to_home(self): + # "Back" sits after all the bars; jump there with wait_for_text. + self._navigate_until_text("Back") + self._both() + + # ---- confirmation choice ---------------------------------------------- + + def _confirm_yes(self): + # The "Confirm the deletion ..." message is one page on Nano; the + # standalone "Confirm" action page comes next. If a future message + # ends up spanning multiple pages, raise the count of right_clicks + # here (or switch to a wait_for_text loop). + self._right() + self._both() + + # ---- password list ----------------------------------------------------- + + def _list_choose(self, position: int): + # 1-based position to match the touch navigator's contract. + for _ in range(position - 1): + self._right() + self._both() + + def _list_to_menu(self): + self._navigate_until_text("Back") + self._both() + + # ---- keyboard (MODE_NONE) ---------------------------------------------- + + def _kbd_reset_state(self) -> None: + """Drop the cached keyboard cursor (the device left the keyboard).""" + self._kbd_mode = None + self._kbd_index = 0 + + def _kbd_navigate_to(self, target_index: int, ring_size: int) -> None: + """Right- or left-click along the shortest path to `target_index`.""" + right_steps = (target_index - self._kbd_index) % ring_size + left_steps = (self._kbd_index - target_index) % ring_size + if right_steps <= left_steps: + steps, action = right_steps, self._right + else: + steps, action = left_steps, self._left + for _ in range(steps): + action() + self._kbd_index = target_index + + def _kbd_enter_mode(self, target_mode: int) -> None: + """Select `target_mode` from the MODE_NONE screen.""" + self._kbd_navigate_to(target_mode, ring_size=3) + self._both() + self._kbd_mode = target_mode + self._kbd_index = 0 # firmware resets cursor on mode entry + + def _kbd_leave_mode(self) -> None: + """Press the mode-switch key ('\\r') to go back to MODE_NONE.""" + assert self._kbd_mode is not None + keys = _KEY_MAPS[self._kbd_mode] + self._kbd_navigate_to(len(keys) - 1, ring_size=len(keys)) # '\r' + self._both() + self._kbd_mode = None + self._kbd_index = 0 + + @staticmethod + def _required_mode(char: str) -> int: + if char.isupper(): + return _MODE_UPPER + if char.islower(): + return _MODE_LOWER + return _MODE_DIGITS # digits + every supported symbol + + def _kbd_ensure_mode(self, target_mode: int) -> None: + if self._kbd_mode == target_mode: + return + if self._kbd_mode is not None: + self._kbd_leave_mode() + self._kbd_enter_mode(target_mode) + + def _kbd_type_char(self, char: str) -> None: + target_mode = self._required_mode(char) + self._kbd_ensure_mode(target_mode) + keys = _KEY_MAPS[self._kbd_mode] + try: + target_idx = keys.index(char) + except ValueError as exc: + raise ValueError( + f"Character {char!r} cannot be typed in MODE_NONE keyboard") from exc + self._kbd_navigate_to(target_idx, ring_size=len(keys)) + self._both() + + def _write(self, characters: str) -> None: + for char in characters: + self._kbd_type_char(char) + + def _keyboard_confirm(self) -> None: + # Validate is the '\n' entry in the current mode. If no character was + # typed we are still on the mode-choice screen: pick lower first. + if self._kbd_mode is None: + self._kbd_enter_mode(_MODE_LOWER) + keys = _KEY_MAPS[self._kbd_mode] + self._kbd_navigate_to(keys.index("\n"), ring_size=len(keys)) + self._both() + # The keyboard page is destroyed after validation. + self._kbd_reset_state() diff --git a/tests/functional/snapshots/apex_p/delete_all_password/00006.png b/tests/functional/snapshots/apex_p/delete_all_password/00006.png index 32b816e..599cd9b 120000 --- a/tests/functional/snapshots/apex_p/delete_all_password/00006.png +++ b/tests/functional/snapshots/apex_p/delete_all_password/00006.png @@ -1 +1 @@ -../menu_screen.png \ No newline at end of file +../short_menu_screen.png \ No newline at end of file diff --git a/tests/functional/snapshots/apex_p/menu_screen.png b/tests/functional/snapshots/apex_p/menu_screen.png index e1b8b90..c91065e 100644 Binary files a/tests/functional/snapshots/apex_p/menu_screen.png and b/tests/functional/snapshots/apex_p/menu_screen.png differ diff --git a/tests/functional/snapshots/apex_p/settings/00004.png b/tests/functional/snapshots/apex_p/settings/00004.png index 68d3de2..4472c1e 100644 Binary files a/tests/functional/snapshots/apex_p/settings/00004.png and b/tests/functional/snapshots/apex_p/settings/00004.png differ diff --git a/tests/functional/snapshots/apex_p/short_menu_screen.png b/tests/functional/snapshots/apex_p/short_menu_screen.png new file mode 100644 index 0000000..c4567eb Binary files /dev/null and b/tests/functional/snapshots/apex_p/short_menu_screen.png differ diff --git a/tests/functional/snapshots/apex_p/startup_choose_kbl.png b/tests/functional/snapshots/apex_p/startup_choose_kbl.png index a9ea5a4..3666431 100644 Binary files a/tests/functional/snapshots/apex_p/startup_choose_kbl.png and b/tests/functional/snapshots/apex_p/startup_choose_kbl.png differ diff --git a/tests/functional/snapshots/flex/create_password/00006.png b/tests/functional/snapshots/flex/create_password/00006.png index 9691020..f9dcc62 100644 Binary files a/tests/functional/snapshots/flex/create_password/00006.png and b/tests/functional/snapshots/flex/create_password/00006.png differ diff --git a/tests/functional/snapshots/flex/create_password/00007.png b/tests/functional/snapshots/flex/create_password/00007.png index 0cc04f8..5173ad1 100644 Binary files a/tests/functional/snapshots/flex/create_password/00007.png and b/tests/functional/snapshots/flex/create_password/00007.png differ diff --git a/tests/functional/snapshots/flex/create_password/00008.png b/tests/functional/snapshots/flex/create_password/00008.png index 01b2193..3590784 100644 Binary files a/tests/functional/snapshots/flex/create_password/00008.png and b/tests/functional/snapshots/flex/create_password/00008.png differ diff --git a/tests/functional/snapshots/flex/create_password/00009.png b/tests/functional/snapshots/flex/create_password/00009.png index 141a734..34dcb89 100644 Binary files a/tests/functional/snapshots/flex/create_password/00009.png and b/tests/functional/snapshots/flex/create_password/00009.png differ diff --git a/tests/functional/snapshots/flex/create_password/00010.png b/tests/functional/snapshots/flex/create_password/00010.png index 08a3a07..9ce733d 100644 Binary files a/tests/functional/snapshots/flex/create_password/00010.png and b/tests/functional/snapshots/flex/create_password/00010.png differ diff --git a/tests/functional/snapshots/flex/create_password/00012.png b/tests/functional/snapshots/flex/create_password/00012.png index 33bb639..69a4588 100644 Binary files a/tests/functional/snapshots/flex/create_password/00012.png and b/tests/functional/snapshots/flex/create_password/00012.png differ diff --git a/tests/functional/snapshots/flex/delete_all_password/00004.png b/tests/functional/snapshots/flex/delete_all_password/00004.png index 6ec33ee..922a430 100644 Binary files a/tests/functional/snapshots/flex/delete_all_password/00004.png and b/tests/functional/snapshots/flex/delete_all_password/00004.png differ diff --git a/tests/functional/snapshots/flex/delete_all_password/00005.png b/tests/functional/snapshots/flex/delete_all_password/00005.png index e8bd1e5..51c20a4 100644 Binary files a/tests/functional/snapshots/flex/delete_all_password/00005.png and b/tests/functional/snapshots/flex/delete_all_password/00005.png differ diff --git a/tests/functional/snapshots/flex/delete_all_password/00006.png b/tests/functional/snapshots/flex/delete_all_password/00006.png index 32b816e..599cd9b 120000 --- a/tests/functional/snapshots/flex/delete_all_password/00006.png +++ b/tests/functional/snapshots/flex/delete_all_password/00006.png @@ -1 +1 @@ -../menu_screen.png \ No newline at end of file +../short_menu_screen.png \ No newline at end of file diff --git a/tests/functional/snapshots/flex/delete_all_password/00007.png b/tests/functional/snapshots/flex/delete_all_password/00007.png index 3a7b835..f9b2c1b 100644 Binary files a/tests/functional/snapshots/flex/delete_all_password/00007.png and b/tests/functional/snapshots/flex/delete_all_password/00007.png differ diff --git a/tests/functional/snapshots/flex/delete_one_password/00004.png b/tests/functional/snapshots/flex/delete_one_password/00004.png index 72455fa..7d7c592 100644 Binary files a/tests/functional/snapshots/flex/delete_one_password/00004.png and b/tests/functional/snapshots/flex/delete_one_password/00004.png differ diff --git a/tests/functional/snapshots/flex/delete_one_password/00005.png b/tests/functional/snapshots/flex/delete_one_password/00005.png index 02de8ac..a31e244 100644 Binary files a/tests/functional/snapshots/flex/delete_one_password/00005.png and b/tests/functional/snapshots/flex/delete_one_password/00005.png differ diff --git a/tests/functional/snapshots/flex/delete_one_password/00006.png b/tests/functional/snapshots/flex/delete_one_password/00006.png index b50a3aa..c3fb374 100644 Binary files a/tests/functional/snapshots/flex/delete_one_password/00006.png and b/tests/functional/snapshots/flex/delete_one_password/00006.png differ diff --git a/tests/functional/snapshots/flex/delete_one_password/00008.png b/tests/functional/snapshots/flex/delete_one_password/00008.png index 71eff06..b4866a9 100644 Binary files a/tests/functional/snapshots/flex/delete_one_password/00008.png and b/tests/functional/snapshots/flex/delete_one_password/00008.png differ diff --git a/tests/functional/snapshots/flex/disclaimer.png b/tests/functional/snapshots/flex/disclaimer.png index acf3a91..a19cadd 100644 Binary files a/tests/functional/snapshots/flex/disclaimer.png and b/tests/functional/snapshots/flex/disclaimer.png differ diff --git a/tests/functional/snapshots/flex/home_screen.png b/tests/functional/snapshots/flex/home_screen.png index 064821e..0ad334b 100644 Binary files a/tests/functional/snapshots/flex/home_screen.png and b/tests/functional/snapshots/flex/home_screen.png differ diff --git a/tests/functional/snapshots/flex/list_screen_populated.png b/tests/functional/snapshots/flex/list_screen_populated.png index 72455fa..7d7c592 100644 Binary files a/tests/functional/snapshots/flex/list_screen_populated.png and b/tests/functional/snapshots/flex/list_screen_populated.png differ diff --git a/tests/functional/snapshots/flex/menu_screen.png b/tests/functional/snapshots/flex/menu_screen.png index 9483ac9..10f2f59 100644 Binary files a/tests/functional/snapshots/flex/menu_screen.png and b/tests/functional/snapshots/flex/menu_screen.png differ diff --git a/tests/functional/snapshots/flex/settings/00003.png b/tests/functional/snapshots/flex/settings/00003.png index 1b8792a..ec952e3 100644 Binary files a/tests/functional/snapshots/flex/settings/00003.png and b/tests/functional/snapshots/flex/settings/00003.png differ diff --git a/tests/functional/snapshots/flex/settings/00004.png b/tests/functional/snapshots/flex/settings/00004.png index f262641..00b27e1 100644 Binary files a/tests/functional/snapshots/flex/settings/00004.png and b/tests/functional/snapshots/flex/settings/00004.png differ diff --git a/tests/functional/snapshots/flex/settings/00005.png b/tests/functional/snapshots/flex/settings/00005.png index f4230cb..1ef7cb8 100644 Binary files a/tests/functional/snapshots/flex/settings/00005.png and b/tests/functional/snapshots/flex/settings/00005.png differ diff --git a/tests/functional/snapshots/flex/short_menu_screen.png b/tests/functional/snapshots/flex/short_menu_screen.png new file mode 100644 index 0000000..034c311 Binary files /dev/null and b/tests/functional/snapshots/flex/short_menu_screen.png differ diff --git a/tests/functional/snapshots/flex/startup_choose_kbl.png b/tests/functional/snapshots/flex/startup_choose_kbl.png index f4230cb..948d23b 100644 Binary files a/tests/functional/snapshots/flex/startup_choose_kbl.png and b/tests/functional/snapshots/flex/startup_choose_kbl.png differ diff --git a/tests/functional/snapshots/nanosp b/tests/functional/snapshots/nanosp new file mode 120000 index 0000000..24939bb --- /dev/null +++ b/tests/functional/snapshots/nanosp @@ -0,0 +1 @@ +nanox \ No newline at end of file diff --git a/tests/functional/snapshots/nanox/create_password/00000.png b/tests/functional/snapshots/nanox/create_password/00000.png new file mode 100644 index 0000000..4a3a89f Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00000.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00001.png b/tests/functional/snapshots/nanox/create_password/00001.png new file mode 100644 index 0000000..7b72526 Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00001.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00002.png b/tests/functional/snapshots/nanox/create_password/00002.png new file mode 100644 index 0000000..cb9aae8 Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00002.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00003.png b/tests/functional/snapshots/nanox/create_password/00003.png new file mode 100644 index 0000000..7b6645a Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00003.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00004.png b/tests/functional/snapshots/nanox/create_password/00004.png new file mode 100644 index 0000000..23ec845 Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00004.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00005.png b/tests/functional/snapshots/nanox/create_password/00005.png new file mode 100644 index 0000000..7b6645a Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00005.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00006.png b/tests/functional/snapshots/nanox/create_password/00006.png new file mode 100644 index 0000000..00540ea Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00006.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00007.png b/tests/functional/snapshots/nanox/create_password/00007.png new file mode 100644 index 0000000..6864cd7 Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00007.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00008.png b/tests/functional/snapshots/nanox/create_password/00008.png new file mode 100644 index 0000000..faccf04 Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00008.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00009.png b/tests/functional/snapshots/nanox/create_password/00009.png new file mode 100644 index 0000000..3e5a6ee Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00009.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00010.png b/tests/functional/snapshots/nanox/create_password/00010.png new file mode 100644 index 0000000..1ee07b0 Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00010.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00011.png b/tests/functional/snapshots/nanox/create_password/00011.png new file mode 100644 index 0000000..7b6645a Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00011.png differ diff --git a/tests/functional/snapshots/nanox/create_password/00012.png b/tests/functional/snapshots/nanox/create_password/00012.png new file mode 100644 index 0000000..23ec845 Binary files /dev/null and b/tests/functional/snapshots/nanox/create_password/00012.png differ diff --git a/tests/functional/snapshots/nanox/delete_all_password/00000.png b/tests/functional/snapshots/nanox/delete_all_password/00000.png new file mode 100644 index 0000000..4a3a89f Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_all_password/00000.png differ diff --git a/tests/functional/snapshots/nanox/delete_all_password/00001.png b/tests/functional/snapshots/nanox/delete_all_password/00001.png new file mode 100644 index 0000000..7b72526 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_all_password/00001.png differ diff --git a/tests/functional/snapshots/nanox/delete_all_password/00002.png b/tests/functional/snapshots/nanox/delete_all_password/00002.png new file mode 100644 index 0000000..cb9aae8 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_all_password/00002.png differ diff --git a/tests/functional/snapshots/nanox/delete_all_password/00003.png b/tests/functional/snapshots/nanox/delete_all_password/00003.png new file mode 100644 index 0000000..7b6645a Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_all_password/00003.png differ diff --git a/tests/functional/snapshots/nanox/delete_all_password/00004.png b/tests/functional/snapshots/nanox/delete_all_password/00004.png new file mode 100644 index 0000000..af0709d Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_all_password/00004.png differ diff --git a/tests/functional/snapshots/nanox/delete_all_password/00005.png b/tests/functional/snapshots/nanox/delete_all_password/00005.png new file mode 100644 index 0000000..c8ad48d Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_all_password/00005.png differ diff --git a/tests/functional/snapshots/nanox/delete_all_password/00006.png b/tests/functional/snapshots/nanox/delete_all_password/00006.png new file mode 100644 index 0000000..7b6645a Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_all_password/00006.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00000.png b/tests/functional/snapshots/nanox/delete_one_password/00000.png new file mode 100644 index 0000000..4a3a89f Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00000.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00001.png b/tests/functional/snapshots/nanox/delete_one_password/00001.png new file mode 100644 index 0000000..7b72526 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00001.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00002.png b/tests/functional/snapshots/nanox/delete_one_password/00002.png new file mode 100644 index 0000000..cb9aae8 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00002.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00003.png b/tests/functional/snapshots/nanox/delete_one_password/00003.png new file mode 100644 index 0000000..7b6645a Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00003.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00004.png b/tests/functional/snapshots/nanox/delete_one_password/00004.png new file mode 100644 index 0000000..23ec845 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00004.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00005.png b/tests/functional/snapshots/nanox/delete_one_password/00005.png new file mode 100644 index 0000000..ebad114 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00005.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00006.png b/tests/functional/snapshots/nanox/delete_one_password/00006.png new file mode 100644 index 0000000..bb4b746 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00006.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00007.png b/tests/functional/snapshots/nanox/delete_one_password/00007.png new file mode 100644 index 0000000..7b6645a Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00007.png differ diff --git a/tests/functional/snapshots/nanox/delete_one_password/00008.png b/tests/functional/snapshots/nanox/delete_one_password/00008.png new file mode 100644 index 0000000..23ec845 Binary files /dev/null and b/tests/functional/snapshots/nanox/delete_one_password/00008.png differ diff --git a/tests/functional/snapshots/nanox/settings/00000.png b/tests/functional/snapshots/nanox/settings/00000.png new file mode 100644 index 0000000..4a3a89f Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00000.png differ diff --git a/tests/functional/snapshots/nanox/settings/00001.png b/tests/functional/snapshots/nanox/settings/00001.png new file mode 100644 index 0000000..7b72526 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00001.png differ diff --git a/tests/functional/snapshots/nanox/settings/00002.png b/tests/functional/snapshots/nanox/settings/00002.png new file mode 100644 index 0000000..cb9aae8 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00002.png differ diff --git a/tests/functional/snapshots/nanox/settings/00003.png b/tests/functional/snapshots/nanox/settings/00003.png new file mode 100644 index 0000000..2ba81fe Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00003.png differ diff --git a/tests/functional/snapshots/nanox/settings/00004.png b/tests/functional/snapshots/nanox/settings/00004.png new file mode 100644 index 0000000..885d46c Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00004.png differ diff --git a/tests/functional/snapshots/nanox/settings/00005.png b/tests/functional/snapshots/nanox/settings/00005.png new file mode 100644 index 0000000..70e61be Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00005.png differ diff --git a/tests/functional/snapshots/nanox/settings/00006.png b/tests/functional/snapshots/nanox/settings/00006.png new file mode 100644 index 0000000..7b624d0 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00006.png differ diff --git a/tests/functional/snapshots/nanox/settings/00007.png b/tests/functional/snapshots/nanox/settings/00007.png new file mode 100644 index 0000000..07cf6c5 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00007.png differ diff --git a/tests/functional/snapshots/nanox/settings/00008.png b/tests/functional/snapshots/nanox/settings/00008.png new file mode 100644 index 0000000..9b5a504 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00008.png differ diff --git a/tests/functional/snapshots/nanox/settings/00009.png b/tests/functional/snapshots/nanox/settings/00009.png new file mode 100644 index 0000000..5947d6b Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00009.png differ diff --git a/tests/functional/snapshots/nanox/settings/00010.png b/tests/functional/snapshots/nanox/settings/00010.png new file mode 100644 index 0000000..7b72526 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00010.png differ diff --git a/tests/functional/snapshots/nanox/settings/00011.png b/tests/functional/snapshots/nanox/settings/00011.png new file mode 100644 index 0000000..11a2c9d Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00011.png differ diff --git a/tests/functional/snapshots/nanox/settings/00012.png b/tests/functional/snapshots/nanox/settings/00012.png new file mode 100644 index 0000000..4c31a99 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00012.png differ diff --git a/tests/functional/snapshots/nanox/settings/00013.png b/tests/functional/snapshots/nanox/settings/00013.png new file mode 100644 index 0000000..36c4a01 Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00013.png differ diff --git a/tests/functional/snapshots/nanox/settings/00014.png b/tests/functional/snapshots/nanox/settings/00014.png new file mode 100644 index 0000000..7528a4b Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00014.png differ diff --git a/tests/functional/snapshots/nanox/settings/00015.png b/tests/functional/snapshots/nanox/settings/00015.png new file mode 100644 index 0000000..5947d6b Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00015.png differ diff --git a/tests/functional/snapshots/nanox/settings/00016.png b/tests/functional/snapshots/nanox/settings/00016.png new file mode 100644 index 0000000..bdbc0ff Binary files /dev/null and b/tests/functional/snapshots/nanox/settings/00016.png differ diff --git a/tests/functional/snapshots/stax/create_password/00006.png b/tests/functional/snapshots/stax/create_password/00006.png index 3940493..3201dd4 100644 Binary files a/tests/functional/snapshots/stax/create_password/00006.png and b/tests/functional/snapshots/stax/create_password/00006.png differ diff --git a/tests/functional/snapshots/stax/create_password/00007.png b/tests/functional/snapshots/stax/create_password/00007.png index f155901..1760c57 100644 Binary files a/tests/functional/snapshots/stax/create_password/00007.png and b/tests/functional/snapshots/stax/create_password/00007.png differ diff --git a/tests/functional/snapshots/stax/create_password/00008.png b/tests/functional/snapshots/stax/create_password/00008.png index eacb79b..68f01bc 100644 Binary files a/tests/functional/snapshots/stax/create_password/00008.png and b/tests/functional/snapshots/stax/create_password/00008.png differ diff --git a/tests/functional/snapshots/stax/create_password/00009.png b/tests/functional/snapshots/stax/create_password/00009.png index 32bf1cc..bc5ae4d 100644 Binary files a/tests/functional/snapshots/stax/create_password/00009.png and b/tests/functional/snapshots/stax/create_password/00009.png differ diff --git a/tests/functional/snapshots/stax/create_password/00010.png b/tests/functional/snapshots/stax/create_password/00010.png index 468bce8..9adf9a8 100644 Binary files a/tests/functional/snapshots/stax/create_password/00010.png and b/tests/functional/snapshots/stax/create_password/00010.png differ diff --git a/tests/functional/snapshots/stax/delete_all_password/00004.png b/tests/functional/snapshots/stax/delete_all_password/00004.png index ffd4215..7e24251 100644 Binary files a/tests/functional/snapshots/stax/delete_all_password/00004.png and b/tests/functional/snapshots/stax/delete_all_password/00004.png differ diff --git a/tests/functional/snapshots/stax/delete_all_password/00005.png b/tests/functional/snapshots/stax/delete_all_password/00005.png index 55912c3..70a58d2 100644 Binary files a/tests/functional/snapshots/stax/delete_all_password/00005.png and b/tests/functional/snapshots/stax/delete_all_password/00005.png differ diff --git a/tests/functional/snapshots/stax/delete_all_password/00006.png b/tests/functional/snapshots/stax/delete_all_password/00006.png index 32b816e..599cd9b 120000 --- a/tests/functional/snapshots/stax/delete_all_password/00006.png +++ b/tests/functional/snapshots/stax/delete_all_password/00006.png @@ -1 +1 @@ -../menu_screen.png \ No newline at end of file +../short_menu_screen.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/delete_one_password/00005.png b/tests/functional/snapshots/stax/delete_one_password/00005.png index 9efdbfb..e66154b 100644 Binary files a/tests/functional/snapshots/stax/delete_one_password/00005.png and b/tests/functional/snapshots/stax/delete_one_password/00005.png differ diff --git a/tests/functional/snapshots/stax/delete_one_password/00006.png b/tests/functional/snapshots/stax/delete_one_password/00006.png index a602e35..5d06807 100644 Binary files a/tests/functional/snapshots/stax/delete_one_password/00006.png and b/tests/functional/snapshots/stax/delete_one_password/00006.png differ diff --git a/tests/functional/snapshots/stax/home_screen.png b/tests/functional/snapshots/stax/home_screen.png index e5bc237..8ae32a8 100644 Binary files a/tests/functional/snapshots/stax/home_screen.png and b/tests/functional/snapshots/stax/home_screen.png differ diff --git a/tests/functional/snapshots/stax/menu_screen.png b/tests/functional/snapshots/stax/menu_screen.png index 0247d9b..0060f02 100644 Binary files a/tests/functional/snapshots/stax/menu_screen.png and b/tests/functional/snapshots/stax/menu_screen.png differ diff --git a/tests/functional/snapshots/stax/short_menu_screen.png b/tests/functional/snapshots/stax/short_menu_screen.png new file mode 100644 index 0000000..8f4319b Binary files /dev/null and b/tests/functional/snapshots/stax/short_menu_screen.png differ diff --git a/tests/functional/snapshots/stax/startup_choose_kbl.png b/tests/functional/snapshots/stax/startup_choose_kbl.png index 5901d9e..c3514c5 100644 Binary files a/tests/functional/snapshots/stax/startup_choose_kbl.png and b/tests/functional/snapshots/stax/startup_choose_kbl.png differ diff --git a/tests/functional/test_cmd.py b/tests/functional/test_cmd.py index 2b553ae..94a41c0 100644 --- a/tests/functional/test_cmd.py +++ b/tests/functional/test_cmd.py @@ -2,7 +2,7 @@ def test_app_info(cmd: PasswordsManagerCommand): - assert cmd.get_app_info() == ("Passwords", "1.3.0") + assert cmd.get_app_info() == ("Passwords", "1.3.1") def test_app_config(cmd: PasswordsManagerCommand): diff --git a/tests/functional/touch/test_common.py b/tests/functional/test_common.py similarity index 72% rename from tests/functional/touch/test_common.py rename to tests/functional/test_common.py index 2dad141..feba07d 100644 --- a/tests/functional/touch/test_common.py +++ b/tests/functional/test_common.py @@ -1,11 +1,8 @@ -import pytest - from ragger.navigator import Navigator -from .navigator import CustomNavInsID +from touch.navigator import CustomNavInsID -@pytest.mark.use_on_device(["stax", "flex", "apex_p"]) def test_immediate_quit(navigator: Navigator): instructions = [ CustomNavInsID.DISCLAIMER_CONFIRM, diff --git a/tests/functional/touch/test_passwords.py b/tests/functional/test_passwords.py similarity index 89% rename from tests/functional/touch/test_passwords.py rename to tests/functional/test_passwords.py index b8b3070..b7f2301 100644 --- a/tests/functional/touch/test_passwords.py +++ b/tests/functional/test_passwords.py @@ -1,12 +1,10 @@ from pathlib import Path -import pytest from ragger.navigator import Navigator, NavIns, NavInsID -from .navigator import CustomNavInsID +from touch.navigator import CustomNavInsID -@pytest.mark.use_on_device(["stax", "flex", "apex_p"]) def test_delete_one_password(navigator: Navigator, default_screenshot_path: Path): instructions = [ CustomNavInsID.DISCLAIMER_CONFIRM, @@ -27,7 +25,6 @@ def test_delete_one_password(navigator: Navigator, default_screenshot_path: Path screen_change_before_first_instruction=False) -@pytest.mark.use_on_device(["stax", "flex", "apex_p"]) def test_delete_all_passwords(navigator: Navigator, default_screenshot_path: Path): instructions = [ CustomNavInsID.DISCLAIMER_CONFIRM, @@ -38,8 +35,6 @@ def test_delete_all_passwords(navigator: Navigator, default_screenshot_path: Pat # confirm CustomNavInsID.CONFIRM_YES, NavIns(NavInsID.WAIT, (2, )), - # check the password has been removed from the list - CustomNavInsID.MENU_TO_DISPLAY, ] navigator.navigate_and_compare(default_screenshot_path, "delete_all_password", @@ -47,7 +42,6 @@ def test_delete_all_passwords(navigator: Navigator, default_screenshot_path: Pat screen_change_before_first_instruction=False) -@pytest.mark.use_on_device(["stax", "flex", "apex_p"]) def test_create_password(navigator: Navigator, default_screenshot_path: Path): instructions = [ CustomNavInsID.DISCLAIMER_CONFIRM, diff --git a/tests/functional/test_settings.py b/tests/functional/test_settings.py new file mode 100644 index 0000000..bdd6bb2 --- /dev/null +++ b/tests/functional/test_settings.py @@ -0,0 +1,32 @@ +from pathlib import Path + +from ragger.navigator import Navigator + +from touch.navigator import CustomNavInsID +from ledgered.devices import Device + +def test_settings_screens(navigator: Navigator, device: Device, default_screenshot_path: Path): + # Touch: settings span 2 pages (switches, then keyboard layout choices). + # Nano (horizontal flow): 6 switches (uppercase, lowercase, numbers, bars, + # ext_symbols, no_enter) + 1 "Host keyboard" BARS_LIST entry that opens + # the layout sub-menu. + instructions = [ + CustomNavInsID.DISCLAIMER_CONFIRM, + CustomNavInsID.CHOOSE_KBL_QWERTY, + CustomNavInsID.HOME_TO_SETTINGS, + ] + if device.is_nano: + instructions += [CustomNavInsID.SETTINGS_NEXT] * 6 + instructions += [CustomNavInsID.BUTTON_APPROVE] + instructions += [CustomNavInsID.SETTINGS_NEXT] * 2 + instructions += [CustomNavInsID.BUTTON_APPROVE] + instructions += [CustomNavInsID.SETTINGS_NEXT] + instructions += [CustomNavInsID.BUTTON_APPROVE] + else: + instructions += [CustomNavInsID.SETTINGS_NEXT] * 2 + instructions += [CustomNavInsID.SETTINGS_TO_HOME] + + navigator.navigate_and_compare(default_screenshot_path, + "settings", + instructions, + screen_change_before_first_instruction=False) diff --git a/tests/functional/touch/navigator.py b/tests/functional/touch/navigator.py index 7e26b01..d94beb8 100644 --- a/tests/functional/touch/navigator.py +++ b/tests/functional/touch/navigator.py @@ -50,8 +50,9 @@ def __init__(self, backend, device, golden_run): CustomNavInsID.HOME_TO_MENU: self.screen.home.action, CustomNavInsID.SETTINGS_TO_HOME: self.screen.settings.multi_page_exit, CustomNavInsID.SETTINGS_NEXT: self.screen.settings.next, - CustomNavInsID.MENU_TO_DISPLAY: partial(self.screen.menu_choice.choose, 2), - CustomNavInsID.MENU_TO_CREATE: partial(self.screen.menu_choice.choose, 3), + CustomNavInsID.MENU_TO_CREATE: partial(self.screen.menu_choice.choose, 1), + CustomNavInsID.MENU_TO_TYPE: partial(self.screen.menu_choice.choose, 2), + CustomNavInsID.MENU_TO_DISPLAY: partial(self.screen.menu_choice.choose, 3), CustomNavInsID.MENU_TO_DELETE: partial(self.screen.menu_choice.choose, 4), CustomNavInsID.MENU_TO_DELETE_ALL: partial(self.screen.menu_choice.choose, 5), CustomNavInsID.LIST_TO_MENU: self.screen.settings.multi_page_exit, diff --git a/tests/functional/touch/test_settings.py b/tests/functional/touch/test_settings.py deleted file mode 100644 index a78f088..0000000 --- a/tests/functional/touch/test_settings.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path -import pytest - -from ragger.navigator import Navigator - -from .navigator import CustomNavInsID - - -@pytest.mark.use_on_device(["stax", "flex", "apex_p"]) -def test_settings_screens(navigator: Navigator, default_screenshot_path: Path): - instructions = [ - CustomNavInsID.DISCLAIMER_CONFIRM, - CustomNavInsID.CHOOSE_KBL_QWERTY, - CustomNavInsID.HOME_TO_SETTINGS, - CustomNavInsID.SETTINGS_NEXT, - CustomNavInsID.SETTINGS_NEXT, - CustomNavInsID.SETTINGS_TO_HOME, - ] - navigator.navigate_and_compare(default_screenshot_path, - "settings", - instructions, - screen_change_before_first_instruction=False) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 646d52a..4886d02 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -27,7 +27,6 @@ set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -pedantic -g -O0 --coverag set(GCC_COVERAGE_LINK_FLAGS "--coverage -lgcov") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") -set(SDK_DIR "$ENV{BOLOS_SDK}") # guard against in-source builds if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) @@ -48,12 +47,6 @@ include_directories( ../../src ../../include ./mocks/ - ${SDK_DIR}/target/stax/include - ${SDK_DIR}/include - ${SDK_DIR}/lib_standard_app - ${SDK_DIR}/io/include - ${SDK_DIR}/io_legacy/include - ${SDK_DIR}/lib_cxng/include ) add_executable(test_password_list test_password_list.c) @@ -64,5 +57,10 @@ add_executable(test_hid_mapping test_hid_mapping.c) add_library(hid_mapping SHARED ../../src/hid_mapping.c) target_link_libraries(test_hid_mapping PUBLIC cmocka gcov hid_mapping bsd) +add_executable(test_password test_password.c) +add_library(password SHARED ../../src/password.c ../../src/metadata.c) +target_link_libraries(test_password PUBLIC cmocka gcov password bsd) + add_test(password_list test_password_list) add_test(hid_mapping test_hid_mapping) +add_test(password test_password) diff --git a/tests/unit/README.md b/tests/unit/README.md index 01b67bb..616ee5a 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -13,7 +13,7 @@ and for code coverage generation: ## Overview -In `unit-tests` folder, compile with +In `tests/unit` folder, compile with ```sh cmake -Bbuild -H. && make -C build @@ -27,7 +27,7 @@ CTEST_OUTPUT_ON_FAILURE=1 make -C build test ## Generate code coverage -Just execute in `unit-tests` folder +Just execute in `tests/unit` folder ```sh ./gen_coverage.sh diff --git a/tests/unit/mocks/cx.h b/tests/unit/mocks/cx.h new file mode 100644 index 0000000..baa637e --- /dev/null +++ b/tests/unit/mocks/cx.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +// Pulled in transitively by ctr_drbg.h (-> password_generation.h -> +// password_typing.h -> password.c). The production header from the SDK +// declares cryptographic primitives (cx_aes_*, CX_LAST, CX_ENCRYPT, ...). +// password.c / metadata.c never reference any of them. The only piece needed +// at parse time is the cx_aes_key_t type, used as a struct field type in +// mbedtls_ctr_drbg_context. A dummy typedef satisfies the compiler without +// pulling in the rest of the SDK crypto layer. +typedef struct { + uint8_t bytes[64]; +} cx_aes_key_t; diff --git a/tests/unit/mocks/os.h b/tests/unit/mocks/os.h index 2377e68..5e703c0 100644 --- a/tests/unit/mocks/os.h +++ b/tests/unit/mocks/os.h @@ -5,11 +5,25 @@ #include #include -#define PRINTF printf -#define THROW(X) return +#define PRINTF printf + +// Declared in os_nvm.h by the SDK. metadata.c calls it; tests provide the +// implementation as a stub. Keep the signature in sync with the SDK +// (`unsigned int src_len`). +void nvm_write(void* dst_adr, void* src_adr, unsigned int src_len); +#ifndef THROW +#define THROW(X) return +#endif +#ifndef EXCEPTION #define EXCEPTION 1 +#endif +// SDK macro that turns a relative pointer into an absolute one on device. +// In host-side unit tests pointers are already absolute, so it's a no-op. +#define PIC(x) (x) -bool bolos_ux_mnemonic_check(const unsigned char* buffer, unsigned int length) { +// Declared `static inline` so each translation unit that pulls in this mock +// header gets its own copy without producing duplicate-symbol link errors. +static inline bool bolos_ux_mnemonic_check(const unsigned char* buffer, unsigned int length) { const char* expected_mnemonic = "list of random words which actually are the mnemonic"; printf("Comparing strings under size '%d'\n", length); printf(" - expected: '%s'\n", expected_mnemonic); diff --git a/tests/unit/mocks/os_io_seproxyhal.h b/tests/unit/mocks/os_io_seproxyhal.h new file mode 100644 index 0000000..c1610e5 --- /dev/null +++ b/tests/unit/mocks/os_io_seproxyhal.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +// Pulled in via globals.h. The real SDK header declares the IO buffer used +// to talk to the SE; on the host side we only need the symbol so password.c +// can be compiled and linked. The buffer itself is supplied by the test code. +extern uint8_t G_io_seproxyhal_spi_buffer[]; diff --git a/tests/unit/mocks/status_words.h b/tests/unit/mocks/status_words.h new file mode 100644 index 0000000..adb2135 --- /dev/null +++ b/tests/unit/mocks/status_words.h @@ -0,0 +1,6 @@ +#pragma once + +// Pulled in transitively by error.h (-> metadata.h, password.h). The +// production header defines SWO_* status word constants which are never +// referenced by the code we build for unit tests, so an empty mock is +// sufficient to satisfy the #include directive. diff --git a/tests/unit/test_password.c b/tests/unit/test_password.c new file mode 100644 index 0000000..cfff917 --- /dev/null +++ b/tests/unit/test_password.c @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include + +#include "metadata.h" +#include "password.h" +#include "types.h" + +// --- stubs for symbols password.c / metadata.c reference at link time but +// --- which nickname_exists does not actually exercise. + +// globals.h declares `extern const internalStorage_t N_storage_real;` for the +// on-device layout (NVM is read-only from C's perspective; writes go through +// nvm_write). The test does not include globals.h so we can define the same +// symbol without `const` and let it land in writable BSS. The linker resolves +// the two declarations by symbol name; the qualifier mismatch is harmless. +internalStorage_t N_storage_real; +uint8_t G_io_seproxyhal_spi_buffer[300]; + +void nvm_write(void *dst, void *src, unsigned int len) { + if (src == NULL) { + memset(dst, 0, len); + } else { + memcpy(dst, src, len); + } +} + +// password.c references these via type_password_at_offset / create_new_password, +// neither of which is called from nickname_exists. Stub them so the linker is +// happy. +bool type_password(uint8_t *seed, + size_t seed_size, + uint8_t *out_buffer, + uint8_t enabledSets, + const uint8_t *minSets, + size_t out_size) { + (void) seed; + (void) seed_size; + (void) out_buffer; + (void) enabledSets; + (void) minSets; + (void) out_size; + return false; +} + +uint8_t get_charset_options(void) { + return 0; +} + +// --- helpers ---------------------------------------------------------------- + +// Append one live metadata entry holding `nickname` to N_storage_real. +static void add_password(const char *nickname) { + const size_t name_len = strlen(nickname); + const uint8_t datasize = 1 + name_len; // charset byte + nickname bytes + uint32_t offset = 0; + while (N_storage_real.metadatas[offset] != 0) { + offset += N_storage_real.metadatas[offset] + 2; + } + N_storage_real.metadatas[offset] = datasize; + N_storage_real.metadatas[offset + 1] = 0; // META_NONE -> live entry + N_storage_real.metadatas[offset + 2] = 0; // charset byte + memcpy(&N_storage_real.metadatas[offset + 3], nickname, name_len); + // terminate the list so get_metadata stops here + N_storage_real.metadatas[offset + 3 + name_len] = 0; + N_storage_real.metadata_count++; +} + +static int setup(void **state __attribute__((unused))) { + memset(&N_storage_real, 0, sizeof(N_storage_real)); + return 0; +} + +// --- tests ------------------------------------------------------------------ + +static void test_nickname_exists_empty_db(void **state __attribute__((unused))) { + assert_false(nickname_exists("foo", 3)); + assert_false(nickname_exists("", 0)); +} + +static void test_nickname_exists_found(void **state __attribute__((unused))) { + add_password("alpha"); + add_password("beta"); + add_password("gamma"); + assert_true(nickname_exists("alpha", 5)); + assert_true(nickname_exists("beta", 4)); + assert_true(nickname_exists("gamma", 5)); +} + +static void test_nickname_exists_not_found(void **state __attribute__((unused))) { + add_password("alpha"); + add_password("beta"); + assert_false(nickname_exists("delta", 5)); +} + +static void test_nickname_exists_length_mismatch(void **state __attribute__((unused))) { + // A nickname stored as "alpha" must not match shorter or longer queries. + add_password("alpha"); + assert_false(nickname_exists("alp", 3)); + assert_false(nickname_exists("alphabet", 8)); +} + +static void test_nickname_exists_case_sensitive(void **state __attribute__((unused))) { + add_password("alpha"); + assert_false(nickname_exists("ALPHA", 5)); + assert_false(nickname_exists("Alpha", 5)); +} + +static void test_nickname_exists_truncation(void **state __attribute__((unused))) { + // write_metadata clips the data block at MAX_METANAME bytes (charset + + // nickname), so the longest persistable nickname is MAX_METANAME - 1 = 19 + // bytes. Anything longer is silently truncated. nickname_exists must + // detect that a longer input would land on the same stored value, otherwise + // a 20-char input could bypass the duplicate check. + const char stored[] = "AAAAAAAAAAAAAAAAAAA"; // 19 chars + assert_int_equal(strlen(stored), MAX_METANAME - 1); + add_password(stored); + + // Exact 19-char match still works. + assert_true(nickname_exists(stored, MAX_METANAME - 1)); + + // 20 chars sharing the first 19 with the stored entry: truncation aware + // duplicate detection must flag it as a conflict. + const char twenty_chars_same_prefix[] = "AAAAAAAAAAAAAAAAAAAB"; // 20 chars + assert_int_equal(strlen(twenty_chars_same_prefix), MAX_METANAME); + assert_true(nickname_exists(twenty_chars_same_prefix, MAX_METANAME)); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_nickname_exists_empty_db, setup, NULL), + cmocka_unit_test_setup_teardown(test_nickname_exists_found, setup, NULL), + cmocka_unit_test_setup_teardown(test_nickname_exists_not_found, setup, NULL), + cmocka_unit_test_setup_teardown(test_nickname_exists_length_mismatch, setup, NULL), + cmocka_unit_test_setup_teardown(test_nickname_exists_case_sensitive, setup, NULL), + cmocka_unit_test_setup_teardown(test_nickname_exists_truncation, setup, NULL), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}