From b7042a70db3a79640fb57d6f2c016d6416a9f812 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:20:58 -0400 Subject: [PATCH 1/2] Add CLI integration test --- src/tests/test_cli_integration.py | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/tests/test_cli_integration.py diff --git a/src/tests/test_cli_integration.py b/src/tests/test_cli_integration.py new file mode 100644 index 0000000..84cb2e4 --- /dev/null +++ b/src/tests/test_cli_integration.py @@ -0,0 +1,87 @@ +import importlib +import shutil +from pathlib import Path +from types import SimpleNamespace + +from typer.testing import CliRunner + +from tests.helpers import TEST_SEED, TEST_PASSWORD + +import constants +import seedpass.core.manager as manager_module +import seedpass.cli as cli_module +import utils.password_prompt as pwd_prompt +import colorama + + +def test_cli_integration(monkeypatch, tmp_path): + # Redirect home directory so profiles are created under tmp_path + monkeypatch.setattr(Path, "home", lambda: tmp_path) + importlib.reload(constants) + importlib.reload(manager_module) + # Avoid colorama wrapping stdout which breaks CliRunner + colorama.deinit() + monkeypatch.setattr(pwd_prompt, "colorama_init", lambda: None) + importlib.reload(pwd_prompt) + importlib.reload(cli_module) + + runner = CliRunner() + + # Provide non-interactive responses + monkeypatch.setattr(manager_module, "prompt_seed_words", lambda *a, **k: TEST_SEED) + monkeypatch.setattr(manager_module, "prompt_new_password", lambda: TEST_PASSWORD) + monkeypatch.setattr(manager_module, "prompt_for_password", lambda: TEST_PASSWORD) + monkeypatch.setattr( + manager_module, "prompt_existing_password", lambda *a, **k: TEST_PASSWORD + ) + monkeypatch.setattr(manager_module, "confirm_action", lambda *a, **k: True) + monkeypatch.setattr(manager_module, "masked_input", lambda *_: TEST_SEED) + monkeypatch.setattr( + manager_module.PasswordManager, "start_background_sync", lambda *a, **k: None + ) + monkeypatch.setattr( + manager_module.PasswordManager, + "start_background_vault_sync", + lambda *a, **k: None, + ) + monkeypatch.setattr( + manager_module.PasswordManager, + "start_background_relay_check", + lambda *a, **k: None, + ) + monkeypatch.setattr( + manager_module, "NostrClient", lambda *a, **k: SimpleNamespace() + ) + + def auto_add(self): + return self.setup_existing_seed( + method="paste", seed=TEST_SEED, password=TEST_PASSWORD + ) + + monkeypatch.setattr(manager_module.PasswordManager, "add_new_fingerprint", auto_add) + + # Any unexpected input requests will receive "1" to avoid blocking + monkeypatch.setattr("builtins.input", lambda *a, **k: "1") + + # Create a profile + result = runner.invoke(cli_module.app, ["fingerprint", "add"]) + assert result.exit_code == 0 + + # Add a password entry + result = runner.invoke(cli_module.app, ["entry", "add", "Example", "--length", "8"]) + assert result.exit_code == 0 + index = int(result.stdout.strip()) + + # Retrieve the entry via search + result = runner.invoke(cli_module.app, ["entry", "get", "Example"]) + assert result.exit_code == 0 + assert len(result.stdout.strip()) == 8 + + # Ensure the index file was created + fm = manager_module.FingerprintManager(constants.APP_DIR) + fp = fm.current_fingerprint + assert fp is not None + assert (constants.APP_DIR / fp / "seedpass_entries_db.json.enc").exists() + + # Cleanup created data + shutil.rmtree(constants.APP_DIR, ignore_errors=True) From 98f8bfa679614a96b3d0f56516f9b9d384ca10fb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:40:34 -0400 Subject: [PATCH 2/2] Fix CLI integration test --- src/tests/test_cli_integration.py | 66 +++++++++++++++++-------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/src/tests/test_cli_integration.py b/src/tests/test_cli_integration.py index 84cb2e4..a2978bb 100644 --- a/src/tests/test_cli_integration.py +++ b/src/tests/test_cli_integration.py @@ -1,33 +1,30 @@ import importlib import shutil +from contextlib import redirect_stdout +from io import StringIO from pathlib import Path from types import SimpleNamespace -from typer.testing import CliRunner +from tests.helpers import TEST_PASSWORD, TEST_SEED -from tests.helpers import TEST_SEED, TEST_PASSWORD - -import constants -import seedpass.core.manager as manager_module -import seedpass.cli as cli_module -import utils.password_prompt as pwd_prompt import colorama +import constants +import seedpass.cli as cli_module +import seedpass.core.manager as manager_module +import utils.password_prompt as pwd_prompt def test_cli_integration(monkeypatch, tmp_path): - # Redirect home directory so profiles are created under tmp_path + """Exercise basic CLI flows without interactive prompts.""" monkeypatch.setattr(Path, "home", lambda: tmp_path) + monkeypatch.setattr(colorama, "init", lambda *a, **k: None) + monkeypatch.setattr(pwd_prompt, "colorama_init", lambda: None) importlib.reload(constants) importlib.reload(manager_module) - # Avoid colorama wrapping stdout which breaks CliRunner - colorama.deinit() - monkeypatch.setattr(pwd_prompt, "colorama_init", lambda: None) importlib.reload(pwd_prompt) importlib.reload(cli_module) - runner = CliRunner() - - # Provide non-interactive responses + # Bypass user prompts and background threads monkeypatch.setattr(manager_module, "prompt_seed_words", lambda *a, **k: TEST_SEED) monkeypatch.setattr(manager_module, "prompt_new_password", lambda: TEST_PASSWORD) monkeypatch.setattr(manager_module, "prompt_for_password", lambda: TEST_PASSWORD) @@ -59,29 +56,38 @@ def test_cli_integration(monkeypatch, tmp_path): ) monkeypatch.setattr(manager_module.PasswordManager, "add_new_fingerprint", auto_add) - - # Any unexpected input requests will receive "1" to avoid blocking monkeypatch.setattr("builtins.input", lambda *a, **k: "1") - # Create a profile - result = runner.invoke(cli_module.app, ["fingerprint", "add"]) - assert result.exit_code == 0 + buf = StringIO() + with redirect_stdout(buf): + try: + cli_module.app(["fingerprint", "add"]) + except SystemExit as e: + assert e.code == 0 + buf.truncate(0) + buf.seek(0) - # Add a password entry - result = runner.invoke(cli_module.app, ["entry", "add", "Example", "--length", "8"]) - assert result.exit_code == 0 - index = int(result.stdout.strip()) + with redirect_stdout(buf): + try: + cli_module.app(["entry", "add", "Example", "--length", "8"]) + except SystemExit as e: + assert e.code == 0 + buf.truncate(0) + buf.seek(0) - # Retrieve the entry via search - result = runner.invoke(cli_module.app, ["entry", "get", "Example"]) - assert result.exit_code == 0 - assert len(result.stdout.strip()) == 8 + with redirect_stdout(buf): + try: + cli_module.app(["entry", "get", "Example"]) + except SystemExit as e: + assert e.code == 0 + lines = [line for line in buf.getvalue().splitlines() if line.strip()] + password = lines[-1] + assert len(password.strip()) >= 8 - # Ensure the index file was created fm = manager_module.FingerprintManager(constants.APP_DIR) fp = fm.current_fingerprint assert fp is not None - assert (constants.APP_DIR / fp / "seedpass_entries_db.json.enc").exists() + index_file = constants.APP_DIR / fp / "seedpass_entries_db.json.enc" + assert index_file.exists() - # Cleanup created data shutil.rmtree(constants.APP_DIR, ignore_errors=True)