git.schokokeks.org
Repositories
Help
Report an Issue
derivepassphrase.git
Code
Commits
Branches
Tags
Suche
Strukturansicht:
9ad57b1
Branches
Tags
documentation-tree
master
unstable/modularize-and-refactor-test-machinery
unstable/ssh-agent-socket-providers
wishlist
0.1.0
0.1.1
0.1.2
0.1.3
0.2.0
0.3.0
0.3.1
0.3.2
0.3.3
0.4.0
0.5.1
0.5.2
derivepassphrase.git
tests
test_derivepassphrase_cli
test_transition.py
Split the CLI tests into one file per class/group
Marco Ricci
commited
9ad57b1
at 2025-08-09 16:22:42
test_transition.py
Blame
History
Raw
# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info> # # SPDX-License-Identifier: Zlib # TODO(the-13th-letter): Remove this module in v1.0. # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#upgrading-to-v1.0 from __future__ import annotations import contextlib import errno import json import logging import os import pathlib import click.testing import pytest from typing_extensions import Any from derivepassphrase import cli, vault from derivepassphrase._internals import ( cli_helpers, ) from tests import data, machinery, test_derivepassphrase_cli from tests.data import callables from tests.machinery import pytest as pytest_machinery from tests.test_derivepassphrase_cli import test_utils DUMMY_SERVICE = data.DUMMY_SERVICE DUMMY_PASSPHRASE = data.DUMMY_PASSPHRASE DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS class Parametrize( test_derivepassphrase_cli.Parametrize, test_utils.Parametrize ): """Common test parametrizations.""" BAD_CONFIGS = pytest.mark.parametrize( "config", [ {"global": "", "services": {}}, {"global": 0, "services": {}}, { "global": {"phrase": "abc"}, "services": False, }, { "global": {"phrase": "abc"}, "services": True, }, { "global": {"phrase": "abc"}, "services": None, }, ], ) class TestCLITransition: """Transition tests for the command-line interface up to v1.0.""" @Parametrize.BASE_CONFIG_VARIATIONS def test_110_load_config_backup( self, config: Any, ) -> None: """Loading the old settings file works.""" runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) cli_helpers.config_filename( subsystem="old settings.json" ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8") assert cli_helpers.migrate_and_load_old_config()[0] == config @Parametrize.BASE_CONFIG_VARIATIONS def test_111_migrate_config( self, config: Any, ) -> None: """Migrating the old settings file works.""" runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) cli_helpers.config_filename( subsystem="old settings.json" ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8") assert cli_helpers.migrate_and_load_old_config() == (config, None) @Parametrize.BASE_CONFIG_VARIATIONS def test_112_migrate_config_error( self, config: Any, ) -> None: """Migrating the old settings file atop a directory fails.""" runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) cli_helpers.config_filename( subsystem="old settings.json" ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8") cli_helpers.config_filename(subsystem="vault").mkdir( parents=True, exist_ok=True ) config2, err = cli_helpers.migrate_and_load_old_config() assert config2 == config assert isinstance(err, OSError) # The Annoying OS uses EEXIST, other OSes use EISDIR. assert err.errno in {errno.EISDIR, errno.EEXIST} @Parametrize.BAD_CONFIGS def test_113_migrate_config_error_bad_config_value( self, config: Any, ) -> None: """Migrating an invalid old settings file fails.""" runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) cli_helpers.config_filename( subsystem="old settings.json" ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8") with pytest.raises( ValueError, match=cli_helpers.INVALID_VAULT_CONFIG ): cli_helpers.migrate_and_load_old_config() def test_200_forward_export_vault_path_parameter( self, caplog: pytest.LogCaptureFixture, ) -> None: """Forwarding arguments from "export" to "export vault" works.""" pytest.importorskip("cryptography", minversion="38.0") runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=data.VAULT_V03_CONFIG, vault_key=data.VAULT_MASTER_KEY, ) ) monkeypatch.setenv("VAULT_KEY", data.VAULT_MASTER_KEY) result = runner.invoke( cli.derivepassphrase, ["export", "VAULT_PATH"], ) assert result.clean_exit(empty_stderr=False), "expected clean exit" assert machinery.deprecation_warning_emitted( "A subcommand will be required here in v1.0", caplog.record_tuples ) assert machinery.deprecation_warning_emitted( 'Defaulting to subcommand "vault"', caplog.record_tuples ) assert json.loads(result.stdout) == data.VAULT_V03_CONFIG_DATA def test_201_forward_export_vault_empty_commandline( self, caplog: pytest.LogCaptureFixture, ) -> None: """Deferring from "export" to "export vault" works.""" pytest.importorskip("cryptography", minversion="38.0") runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) result = runner.invoke( cli.derivepassphrase, ["export"], ) assert machinery.deprecation_warning_emitted( "A subcommand will be required here in v1.0", caplog.record_tuples ) assert machinery.deprecation_warning_emitted( 'Defaulting to subcommand "vault"', caplog.record_tuples ) assert result.error_exit(error="Missing argument 'PATH'"), ( "expected error exit and known error type" ) @Parametrize.CHARSET_NAME def test_210_forward_vault_disable_character_set( self, caplog: pytest.LogCaptureFixture, charset_name: str, ) -> None: """Forwarding arguments from top-level to "vault" works.""" option = f"--{charset_name}" charset = vault.Vault.CHARSETS[charset_name].decode("ascii") runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) monkeypatch.setattr( cli_helpers, "prompt_for_passphrase", callables.auto_prompt, ) result = runner.invoke( cli.derivepassphrase, [option, "0", "-p", "--", DUMMY_SERVICE], input=DUMMY_PASSPHRASE, catch_exceptions=False, ) assert result.clean_exit(empty_stderr=False), "expected clean exit" assert machinery.deprecation_warning_emitted( "A subcommand will be required here in v1.0", caplog.record_tuples ) assert machinery.deprecation_warning_emitted( 'Defaulting to subcommand "vault"', caplog.record_tuples ) for c in charset: assert c not in result.stdout, ( f"derived password contains forbidden character {c!r}" ) def test_211_forward_vault_empty_command_line( self, caplog: pytest.LogCaptureFixture, ) -> None: """Deferring from top-level to "vault" works.""" runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) result = runner.invoke( cli.derivepassphrase, [], input=DUMMY_PASSPHRASE, catch_exceptions=False, ) assert machinery.deprecation_warning_emitted( "A subcommand will be required here in v1.0", caplog.record_tuples ) assert machinery.deprecation_warning_emitted( 'Defaulting to subcommand "vault"', caplog.record_tuples ) assert result.error_exit( error="Deriving a passphrase requires a SERVICE." ), "expected error exit and known error type" def test_300_export_using_old_config_file( self, caplog: pytest.LogCaptureFixture, ) -> None: """Exporting from (and migrating) the old settings file works.""" caplog.set_level(logging.INFO) runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) cli_helpers.config_filename( subsystem="old settings.json" ).write_text( json.dumps( {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, indent=2, ) + "\n", encoding="UTF-8", ) result = runner.invoke( cli.derivepassphrase_vault, ["--export", "-"], catch_exceptions=False, ) assert result.clean_exit(), "expected clean exit" assert machinery.deprecation_warning_emitted( "v0.1-style config file", caplog.record_tuples ), "expected known warning message in stderr" assert machinery.deprecation_info_emitted( "Successfully migrated to ", caplog.record_tuples ), "expected known warning message in stderr" def test_300a_export_using_old_config_file_migration_error( self, caplog: pytest.LogCaptureFixture, ) -> None: """Exporting from (and not migrating) the old settings file fails.""" runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_config( monkeypatch=monkeypatch, runner=runner, ) ) cli_helpers.config_filename( subsystem="old settings.json" ).write_text( json.dumps( {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, indent=2, ) + "\n", encoding="UTF-8", ) def raiser(*_args: Any, **_kwargs: Any) -> None: raise OSError( errno.EACCES, os.strerror(errno.EACCES), cli_helpers.config_filename(subsystem="vault"), ) monkeypatch.setattr(os, "replace", raiser) monkeypatch.setattr(pathlib.Path, "rename", raiser) result = runner.invoke( cli.derivepassphrase_vault, ["--export", "-"], catch_exceptions=False, ) assert result.clean_exit(), "expected clean exit" assert machinery.deprecation_warning_emitted( "v0.1-style config file", caplog.record_tuples ), "expected known warning message in stderr" assert machinery.warning_emitted( "Failed to migrate to ", caplog.record_tuples ), "expected known warning message in stderr" def test_400_completion_service_name_old_config_file( self, ) -> None: """Completing service names from the old settings file works.""" config = {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}} runner = machinery.CliRunner(mix_stderr=False) # TODO(the-13th-letter): Rewrite using parenthesized # with-statements. # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 with contextlib.ExitStack() as stack: monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) stack.enter_context( pytest_machinery.isolated_vault_config( monkeypatch=monkeypatch, runner=runner, vault_config=config, ) ) old_name = cli_helpers.config_filename( subsystem="old settings.json" ) new_name = cli_helpers.config_filename(subsystem="vault") old_name.unlink(missing_ok=True) new_name.rename(old_name) assert cli_helpers.shell_complete_service( click.Context(cli.derivepassphrase), click.Argument(["some_parameter"]), "", ) == [DUMMY_SERVICE]