git.schokokeks.org
Repositories
Help
Report an Issue
derivepassphrase.git
Code
Commits
Branches
Tags
Suche
Strukturansicht:
34d0f5f
Branches
Tags
documentation-tree
master
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_000_basic.py
Split the basic command-line tests, again
Marco Ricci
commited
34d0f5f
at 2025-11-26 20:46:42
test_000_basic.py
Blame
History
Raw
# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info> # # SPDX-License-Identifier: Zlib """Tests for the `derivepassphrase` command-line interface: common subsystems. (Currently, these tests are performed on the `derivepassphrase vault` subcommand, because that is the main command using all these subsystems.) """ from __future__ import annotations import contextlib import json import socket import textwrap import types from typing import TYPE_CHECKING, ClassVar import pytest from derivepassphrase import _types, cli, ssh_agent from derivepassphrase._internals import ( cli_helpers, ) from tests import data, machinery from tests.machinery import pytest as pytest_machinery if TYPE_CHECKING: from collections.abc import Iterator DUMMY_SERVICE = data.DUMMY_SERVICE DUMMY_PASSPHRASE = data.DUMMY_PASSPHRASE DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS class Parametrize(types.SimpleNamespace): """Common test parametrizations.""" UNICODE_NORMALIZATION_ERROR_INPUTS = pytest.mark.parametrize( ["main_config", "command_line", "input", "error_message"], [ pytest.param( textwrap.dedent(r""" [vault] default-unicode-normalization-form = 'XXX' """), ["--import", "-"], json.dumps({ "services": { DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(), "with_normalization": {"phrase": "D\u00fcsseldorf"}, }, }), ( "Invalid value 'XXX' for config key " "vault.default-unicode-normalization-form" ), id="global", ), pytest.param( textwrap.dedent(r""" [vault.unicode-normalization-form] with_normalization = 'XXX' """), ["--import", "-"], json.dumps({ "services": { DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(), "with_normalization": {"phrase": "D\u00fcsseldorf"}, }, }), ( "Invalid value 'XXX' for config key " "vault.with_normalization.unicode-normalization-form" ), id="service", ), pytest.param( textwrap.dedent(r""" [vault] default-unicode-normalization-form = 'XXX' """), ["--config", "--phrase"], DUMMY_PASSPHRASE, ( "Invalid value 'XXX' for config key " "vault.default-unicode-normalization-form" ), id="configure global passphrase", ), pytest.param( textwrap.dedent(r""" [vault] default-unicode-normalization-form = 'XXX' """), ["--config", "--phrase", "--", DUMMY_SERVICE], DUMMY_PASSPHRASE, ( "Invalid value 'XXX' for config key " "vault.default-unicode-normalization-form" ), id="configure service passphrase", ), pytest.param( textwrap.dedent(r""" [vault] default-unicode-normalization-form = 'XXX' """), ["--phrase", "--", DUMMY_SERVICE], DUMMY_PASSPHRASE, ( "Invalid value 'XXX' for config key " "vault.default-unicode-normalization-form" ), id="interactive passphrase", ), ], ) UNICODE_NORMALIZATION_WARNING_INPUTS = pytest.mark.parametrize( ["main_config", "command_line", "input", "warning_message"], [ pytest.param( "", ["--import", "-"], json.dumps({ "global": {"phrase": "Du\u0308sseldorf"}, "services": {}, }), "The $.global passphrase is not NFC-normalized", id="global-NFC", ), pytest.param( "", ["--import", "-"], json.dumps({ "services": { DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(), "weird entry name": {"phrase": "Du\u0308sseldorf"}, } }), ( 'The $.services["weird entry name"] passphrase ' "is not NFC-normalized" ), id="service-weird-name-NFC", ), pytest.param( "", ["--config", "-p", "--", DUMMY_SERVICE], "Du\u0308sseldorf", ( f"The $.services.{DUMMY_SERVICE} passphrase " f"is not NFC-normalized" ), id="config-NFC", ), pytest.param( "", ["-p", "--", DUMMY_SERVICE], "Du\u0308sseldorf", "The interactive input passphrase is not NFC-normalized", id="direct-input-NFC", ), pytest.param( textwrap.dedent(r""" [vault] default-unicode-normalization-form = 'NFD' """), ["--import", "-"], json.dumps({ "global": { "phrase": "D\u00fcsseldorf", }, "services": {}, }), "The $.global passphrase is not NFD-normalized", id="global-NFD", ), pytest.param( textwrap.dedent(r""" [vault] default-unicode-normalization-form = 'NFD' """), ["--import", "-"], json.dumps({ "services": { DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(), "weird entry name": {"phrase": "D\u00fcsseldorf"}, }, }), ( 'The $.services["weird entry name"] passphrase ' "is not NFD-normalized" ), id="service-weird-name-NFD", ), pytest.param( textwrap.dedent(r""" [vault.unicode-normalization-form] 'weird entry name 2' = 'NFKD' """), ["--import", "-"], json.dumps({ "services": { DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(), "weird entry name 1": {"phrase": "D\u00fcsseldorf"}, "weird entry name 2": {"phrase": "D\u00fcsseldorf"}, }, }), ( 'The $.services["weird entry name 2"] passphrase ' "is not NFKD-normalized" ), id="service-weird-name-2-NFKD", ), ], ) class TestPassphraseUnicodeNormalization: """Tests concerning the Unicode normalization of passphrases.""" DEFAULT_VAULT_CONFIG: ClassVar[_types.VaultConfig] = { "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()} } def _test( self, command_line: list[str], /, *, main_config: str, message: str, caplog: pytest.LogCaptureFixture | None = None, input: str | None = None, ) -> None: 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=self.DEFAULT_VAULT_CONFIG, main_config_str=main_config, ) ) result = runner.invoke( cli.derivepassphrase_vault, ["--debug", *command_line] if caplog is not None else command_line, catch_exceptions=False, input=input, ) if caplog is not None: assert result.clean_exit(), "expected clean exit" assert machinery.warning_emitted(message, caplog.record_tuples), ( "expected known warning message in stderr" ) else: assert result.error_exit( error="The user configuration file is invalid." ), "expected error exit and known error message" assert result.error_exit(error=message), ( "expected error exit and known error message" ) @Parametrize.UNICODE_NORMALIZATION_WARNING_INPUTS def test_warning( self, caplog: pytest.LogCaptureFixture, main_config: str, command_line: list[str], input: str | None, warning_message: str, ) -> None: """Using unnormalized Unicode passphrases warns.""" self._test( command_line, main_config=main_config, message=warning_message, caplog=caplog, input=input, ) @Parametrize.UNICODE_NORMALIZATION_ERROR_INPUTS def test_error( self, main_config: str, command_line: list[str], input: str | None, error_message: str, ) -> None: """Using unknown Unicode normalization forms fails.""" self._test( command_line, main_config=main_config, message=error_message, input=input, ) class TestUserConfigurationFileOther: """Other tests concerning the user configuration file.""" @contextlib.contextmanager def _test( self, *, main_config: str = "", ) -> Iterator[pytest.MonkeyPatch]: 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={"services": {}}, main_config_str=main_config, ) ) yield monkeypatch result = runner.invoke( cli.derivepassphrase_vault, ["--phrase", "--", DUMMY_SERVICE], input=DUMMY_PASSPHRASE, catch_exceptions=False, ) assert result.error_exit(error="Cannot load user config:"), ( "expected error exit and known error message" ) def test_bad_user_config_file( self, ) -> None: """Loading a user configuration file in an invalid format fails.""" with self._test(main_config="This file is not valid TOML.\n"): pass def test_user_config_is_a_directory( self, ) -> None: """Loading a user configuration non-file fails.""" with self._test(): user_config = cli_helpers.config_filename( subsystem="user configuration" ) user_config.unlink() user_config.mkdir(parents=True, exist_ok=True) class TestSSHAgentAvailability: """Tests concerning the availability of the SSH agent.""" def test_missing_af_unix_support( self, caplog: pytest.LogCaptureFixture, ) -> None: """Querying the SSH agent without `AF_UNIX` support 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_vault_config( monkeypatch=monkeypatch, runner=runner, vault_config={"global": {"phrase": "abc"}, "services": {}}, ) ) monkeypatch.setenv( "SSH_AUTH_SOCK", "the value doesn't even matter" ) monkeypatch.setattr( ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", ["posix"] ) monkeypatch.delattr(socket, "AF_UNIX", raising=False) result = runner.invoke( cli.derivepassphrase_vault, ["--key", "--config"], catch_exceptions=False, ) assert result.error_exit( error="does not support communicating with it" ), "expected error exit and known error message" assert machinery.warning_emitted( "Cannot connect to an SSH agent via UNIX domain sockets", caplog.record_tuples, ), "expected known warning message in stderr"