git.schokokeks.org
Repositories
Help
Report an Issue
derivepassphrase.git
Code
Commits
Branches
Tags
Suche
Strukturansicht:
e981744
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_export_vault.py
Change the code style to use double quotes for strings
Marco Ricci
commited
e981744
at 2025-08-05 21:11:03
test_derivepassphrase_cli_export_vault.py
Blame
History
Raw
# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info> # # SPDX-License-Identifier: Zlib from __future__ import annotations import base64 import contextlib import json import pathlib import types from typing import TYPE_CHECKING import hypothesis import pytest from hypothesis import strategies import tests from derivepassphrase import _types, cli, exporter from derivepassphrase.exporter import storeroom, vault_native cryptography = pytest.importorskip("cryptography", minversion="38.0") from cryptography.hazmat.primitives import ( # noqa: E402 ciphers, hashes, hmac, padding, ) from cryptography.hazmat.primitives.ciphers import ( # noqa: E402 algorithms, modes, ) if TYPE_CHECKING: from collections.abc import Callable from typing import Any from typing_extensions import Buffer, Literal class Parametrize(types.SimpleNamespace): BAD_CONFIG = pytest.mark.parametrize( "config", ["xxx", "null", '{"version": 255}'] ) VAULT_NATIVE_CONFIG_DATA = pytest.mark.parametrize( ["config", "format", "config_data"], [ pytest.param( tests.VAULT_V02_CONFIG, "v0.2", tests.VAULT_V02_CONFIG_DATA, id="V02_CONFIG-v0.2", ), pytest.param( tests.VAULT_V02_CONFIG, "v0.3", exporter.NotAVaultConfigError, id="V02_CONFIG-v0.3", ), pytest.param( tests.VAULT_V03_CONFIG, "v0.2", exporter.NotAVaultConfigError, id="V03_CONFIG-v0.2", ), pytest.param( tests.VAULT_V03_CONFIG, "v0.3", tests.VAULT_V03_CONFIG_DATA, id="V03_CONFIG-v0.3", ), ], ) VAULT_NATIVE_PARSER_CLASS_DATA = pytest.mark.parametrize( ["config", "parser_class", "config_data"], [ pytest.param( tests.VAULT_V02_CONFIG, vault_native.VaultNativeV02ConfigParser, tests.VAULT_V02_CONFIG_DATA, id="0.2", ), pytest.param( tests.VAULT_V03_CONFIG, vault_native.VaultNativeV03ConfigParser, tests.VAULT_V03_CONFIG_DATA, id="0.3", ), ], ) BAD_MASTER_KEYS_DATA = pytest.mark.parametrize( ["data", "err_msg"], [ pytest.param( '{"version": 255}', "bad or unsupported keys version header", id="v255", ), pytest.param( '{"version": 1}\nAAAA\nAAAA', "trailing data; cannot make sense", id="trailing-data", ), pytest.param( '{"version": 1}\nAAAA', "cannot handle version 0 encrypted keys", id="v0-keys", ), ], ) STOREROOM_HANDLER = pytest.mark.parametrize( "handler", [ pytest.param(storeroom.export_storeroom_data, id="handler"), pytest.param(exporter.export_vault_config_data, id="dispatcher"), ], ) VAULT_NATIVE_HANDLER = pytest.mark.parametrize( "handler", [ pytest.param(vault_native.export_vault_native_data, id="handler"), pytest.param(exporter.export_vault_config_data, id="dispatcher"), ], ) VAULT_NATIVE_PBKDF2_RESULT = pytest.mark.parametrize( ["iterations", "result"], [ pytest.param(100, b"6ede361e81e9c061efcdd68aeb768b80", id="100"), pytest.param(200, b"bcc7d01e075b9ffb69e702bf701187c1", id="200"), ], ) KEY_FORMATS = pytest.mark.parametrize( "key", [ None, pytest.param(tests.VAULT_MASTER_KEY, id="str"), pytest.param(tests.VAULT_MASTER_KEY.encode("ascii"), id="bytes"), pytest.param( bytearray(tests.VAULT_MASTER_KEY.encode("ascii")), id="bytearray", ), pytest.param( memoryview(tests.VAULT_MASTER_KEY.encode("ascii")), id="memoryview", ), ], ) PATH = pytest.mark.parametrize("path", [".vault", None]) BAD_STOREROOM_CONFIG_DATA = pytest.mark.parametrize( ["zipped_config", "error_text"], [ pytest.param( tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED, "Object key mismatch", id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED", ), pytest.param( tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2, "Directory index is not actually an index", id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2", ), pytest.param( tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3, "Directory index is not actually an index", id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3", ), pytest.param( tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4, "Object key mismatch", id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4", ), ], ) class TestCLI: """Test the command-line interface for `derivepassphrase export vault`.""" def test_200_path_parameter(self) -> None: """The path `VAULT_PATH` is supported. Using `VAULT_PATH` as the path looks up the actual path in the `VAULT_PATH` environment variable. See [`exporter.get_vault_path`][] for details. """ runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_V03_CONFIG, vault_key=tests.VAULT_MASTER_KEY, ) ) monkeypatch.setenv("VAULT_KEY", tests.VAULT_MASTER_KEY) result = runner.invoke( cli.derivepassphrase_export_vault, ["VAULT_PATH"], ) assert result.clean_exit(empty_stderr=True), "expected clean exit" assert json.loads(result.stdout) == tests.VAULT_V03_CONFIG_DATA def test_201_key_parameter(self) -> None: """The `--key` option is supported.""" runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_V03_CONFIG, ) ) result = runner.invoke( cli.derivepassphrase_export_vault, ["-k", tests.VAULT_MASTER_KEY, ".vault"], ) assert result.clean_exit(empty_stderr=True), "expected clean exit" assert json.loads(result.stdout) == tests.VAULT_V03_CONFIG_DATA @tests.Parametrize.VAULT_CONFIG_FORMATS_DATA def test_210_load_vault_v02_v03_storeroom( self, config: str | bytes, format: str, config_data: dict[str, Any], ) -> None: """Passing a specific format works. Passing a specific format name causes `derivepassphrase export vault` to only attempt decoding in that named format. """ runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=config, ) ) result = runner.invoke( cli.derivepassphrase_export_vault, ["-f", format, "-k", tests.VAULT_MASTER_KEY, "VAULT_PATH"], ) assert result.clean_exit(empty_stderr=True), "expected clean exit" assert json.loads(result.stdout) == config_data # test_300_invalid_format is found in # tests.test_derivepassphrase_export::Test002CLI def test_301_vault_config_not_found( self, caplog: pytest.LogCaptureFixture, ) -> None: """Fail when trying to decode non-existant files/directories.""" runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_V03_CONFIG, vault_key=tests.VAULT_MASTER_KEY, ) ) result = runner.invoke( cli.derivepassphrase_export_vault, ["does-not-exist.txt"], ) assert result.error_exit( error=( "Cannot parse 'does-not-exist.txt' " "as a valid vault-native config" ), record_tuples=caplog.record_tuples, ), "expected error exit and known error message" assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr def test_302_vault_config_invalid( self, caplog: pytest.LogCaptureFixture, ) -> None: """Fail to parse invalid vault configurations (files).""" runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config="", vault_key=tests.VAULT_MASTER_KEY, ) ) result = runner.invoke( cli.derivepassphrase_export_vault, [".vault"], ) assert result.error_exit( error="Cannot parse '.vault' as a valid vault-native config", record_tuples=caplog.record_tuples, ), "expected error exit and known error message" assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr def test_302a_vault_config_invalid_just_a_directory( self, caplog: pytest.LogCaptureFixture, ) -> None: """Fail to parse invalid vault configurations (directories).""" runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config="", vault_key=tests.VAULT_MASTER_KEY, ) ) p = pathlib.Path(".vault") p.unlink() p.mkdir() result = runner.invoke( cli.derivepassphrase_export_vault, [str(p)], ) assert result.error_exit( error="Cannot parse '.vault' as a valid vault-native config", record_tuples=caplog.record_tuples, ), "expected error exit and known error message" assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr def test_403_invalid_vault_config_bad_signature( self, caplog: pytest.LogCaptureFixture, ) -> None: """Fail to parse vault configurations with invalid integrity checks.""" runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_V02_CONFIG, vault_key=tests.VAULT_MASTER_KEY, ) ) result = runner.invoke( cli.derivepassphrase_export_vault, ["-f", "v0.3", ".vault"], ) assert result.error_exit( error="Cannot parse '.vault' as a valid vault-native config", record_tuples=caplog.record_tuples, ), "expected error exit and known error message" assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr def test_500_vault_config_invalid_internal( self, caplog: pytest.LogCaptureFixture, ) -> None: """The decoded vault configuration data is valid.""" runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_V03_CONFIG, vault_key=tests.VAULT_MASTER_KEY, ) ) def export_vault_config_data(*_args: Any, **_kwargs: Any) -> None: return None monkeypatch.setattr( exporter, "export_vault_config_data", export_vault_config_data, ) result = runner.invoke( cli.derivepassphrase_export_vault, [".vault"], ) assert result.error_exit( error="Invalid vault config: ", record_tuples=caplog.record_tuples, ), "expected error exit and known error message" assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr class TestStoreroom: """Test the "storeroom" handler and handler machinery.""" @Parametrize.PATH @Parametrize.KEY_FORMATS @Parametrize.STOREROOM_HANDLER def test_200_export_data_path_and_keys_type( self, path: str | None, key: str | Buffer | None, handler: exporter.ExportVaultConfigDataFunction, ) -> None: """Support different argument types. The [`exporter.export_vault_config_data`][] dispatcher supports them as well. """ runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED, vault_key=tests.VAULT_MASTER_KEY, ) ) assert ( handler(path, key, format="storeroom") == tests.VAULT_STOREROOM_CONFIG_DATA ) def test_400_decrypt_bucket_item_unknown_version(self) -> None: """Fail on unknown versions of the master keys file.""" bucket_item = ( b"\xff" + bytes(storeroom.ENCRYPTED_KEYPAIR_SIZE) + bytes(3) ) master_keys = _types.StoreroomMasterKeys( encryption_key=bytes(storeroom.KEY_SIZE), signing_key=bytes(storeroom.KEY_SIZE), hashing_key=bytes(storeroom.KEY_SIZE), ) with pytest.raises(ValueError, match="Cannot handle version 255"): storeroom._decrypt_bucket_item(bucket_item, master_keys) @Parametrize.BAD_CONFIG def test_401_decrypt_bucket_file_bad_json_or_version( self, config: str, ) -> None: """Fail on bad or unsupported bucket file contents. These include unknown versions, invalid JSON, or JSON of the wrong shape. """ runner = tests.CliRunner(mix_stderr=False) master_keys = _types.StoreroomMasterKeys( encryption_key=bytes(storeroom.KEY_SIZE), signing_key=bytes(storeroom.KEY_SIZE), hashing_key=bytes(storeroom.KEY_SIZE), ) # 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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED, ) ) p = pathlib.Path(".vault", "20") with p.open("w", encoding="UTF-8") as outfile: print(config, file=outfile) with pytest.raises(ValueError, match="Invalid bucket file: "): list(storeroom._decrypt_bucket_file(p, master_keys)) @Parametrize.BAD_MASTER_KEYS_DATA @Parametrize.STOREROOM_HANDLER def test_402_export_storeroom_data_bad_master_keys_file( self, data: str, err_msg: str, handler: exporter.ExportVaultConfigDataFunction, ) -> None: """Fail on bad or unsupported master keys file contents. These include unknown versions, and data of the wrong shape. """ runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED, vault_key=tests.VAULT_MASTER_KEY, ) ) p = pathlib.Path(".vault", ".keys") with p.open("w", encoding="UTF-8") as outfile: print(data, file=outfile) with pytest.raises(RuntimeError, match=err_msg): handler(format="storeroom") @Parametrize.BAD_STOREROOM_CONFIG_DATA @Parametrize.STOREROOM_HANDLER def test_403_export_storeroom_data_bad_directory_listing( self, zipped_config: bytes, error_text: str, handler: exporter.ExportVaultConfigDataFunction, ) -> None: """Fail on bad decoded directory structures. If the decoded configuration contains directories whose structures are inconsistent, it detects this and fails: - The key indicates a directory, but the contents don't. - The directory indicates children with invalid path names. - The directory indicates children that are missing from the configuration entirely. - The configuration contains nested subdirectories, but the higher-level directories don't indicate their subdirectories. """ runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=zipped_config, vault_key=tests.VAULT_MASTER_KEY, ) ) stack.enter_context(pytest.raises(RuntimeError, match=error_text)) handler(format="storeroom") def test_404_decrypt_keys_wrong_data_length(self) -> None: """Fail on internal structural data of the wrong size. Specifically, fail on internal structural data such as master keys or session keys that is correctly encrypted according to its MAC, but is of the wrong shape. (Since the data usually are keys and thus are opaque, the only detectable shape violation is the wrong size of the data.) """ payload = ( b"Any text here, as long as it isn't exactly 64 or 96 bytes long." ) assert len(payload) not in frozenset({ 2 * storeroom.KEY_SIZE, 3 * storeroom.KEY_SIZE, }) key = b"DEADBEEFdeadbeefDeAdBeEfdEaDbEeF" padder = padding.PKCS7(storeroom.IV_SIZE * 8).padder() plaintext = bytearray(padder.update(payload)) plaintext.extend(padder.finalize()) iv = b"deadbeefDEADBEEF" assert len(iv) == storeroom.IV_SIZE encryptor = ciphers.Cipher( algorithms.AES256(key), modes.CBC(iv) ).encryptor() ciphertext = bytearray(encryptor.update(plaintext)) ciphertext.extend(encryptor.finalize()) mac_obj = hmac.HMAC(key, hashes.SHA256()) mac_obj.update(iv) mac_obj.update(ciphertext) data = iv + bytes(ciphertext) + mac_obj.finalize() with pytest.raises( ValueError, match=r"Invalid encrypted master keys payload", ): storeroom._decrypt_master_keys_data( data, _types.StoreroomKeyPair(encryption_key=key, signing_key=key), ) with pytest.raises( ValueError, match=r"Invalid encrypted session keys payload", ): storeroom._decrypt_session_keys( data, _types.StoreroomMasterKeys( hashing_key=key, encryption_key=key, signing_key=key ), ) @hypothesis.given( data=strategies.binary( min_size=storeroom.MAC_SIZE, max_size=storeroom.MAC_SIZE ), ) def test_405_decrypt_keys_invalid_signature(self, data: bytes) -> None: """Fail on bad MAC values.""" key = b"DEADBEEFdeadbeefDeAdBeEfdEaDbEeF" # Guessing a correct payload plus MAC would be a pre-image # attack on the underlying hash function (SHA-256), i.e. is # computationally infeasible, and the chance of finding one by # such random sampling is astronomically tiny. with pytest.raises(cryptography.exceptions.InvalidSignature): storeroom._decrypt_master_keys_data( data, _types.StoreroomKeyPair(encryption_key=key, signing_key=key), ) with pytest.raises(cryptography.exceptions.InvalidSignature): storeroom._decrypt_session_keys( data, _types.StoreroomMasterKeys( hashing_key=key, encryption_key=key, signing_key=key ), ) class TestVaultNativeConfig: """Test the vault-native handler and handler machinery.""" @Parametrize.VAULT_NATIVE_PBKDF2_RESULT def test_200_pbkdf2_manually(self, iterations: int, result: bytes) -> None: """The PBKDF2 helper function works.""" assert ( vault_native.VaultNativeConfigParser._pbkdf2( tests.VAULT_MASTER_KEY.encode("utf-8"), 32, iterations ) == result ) @Parametrize.VAULT_NATIVE_CONFIG_DATA @Parametrize.VAULT_NATIVE_HANDLER def test_201_export_vault_native_data_explicit_version( self, config: str, format: Literal["v0.2", "v0.3"], config_data: _types.VaultConfig | type[Exception], handler: exporter.ExportVaultConfigDataFunction, ) -> None: """Accept data only of the correct version. Note: Historic behavior `derivepassphrase` versions prior to 0.5 automatically tried to parse vault-native configurations as v0.3-type, then v0.2-type. Since `derivepassphrase` 0.5, the command-line interface still tries multi-version parsing, but the API no longer does. """ runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=config, vault_key=tests.VAULT_MASTER_KEY, ) ) if isinstance(config_data, type): with pytest.raises(config_data): handler(None, format=format) else: parsed_config = handler(None, format=format) assert parsed_config == config_data @Parametrize.PATH @Parametrize.KEY_FORMATS @Parametrize.VAULT_NATIVE_HANDLER def test_202_export_data_path_and_keys_type( self, path: str | None, key: str | Buffer | None, handler: exporter.ExportVaultConfigDataFunction, ) -> None: """The handler supports different argument types. The [`exporter.export_vault_config_data`][] dispatcher supports them as well. """ runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=tests.VAULT_V03_CONFIG, vault_key=tests.VAULT_MASTER_KEY, ) ) assert ( handler(path, key, format="v0.3") == tests.VAULT_V03_CONFIG_DATA ) @Parametrize.VAULT_NATIVE_PARSER_CLASS_DATA def test_300_result_caching( self, config: str, parser_class: type[vault_native.VaultNativeConfigParser], config_data: dict[str, Any], ) -> None: """Cache the results of decrypting/decoding a configuration.""" def null_func(name: str) -> Callable[..., None]: def func(*_args: Any, **_kwargs: Any) -> None: # pragma: no cover msg = f"disallowed and stubbed out function {name} called" raise AssertionError(msg) return func runner = tests.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( tests.isolated_vault_exporter_config( monkeypatch=monkeypatch, runner=runner, vault_config=config, ) ) parser = parser_class( base64.b64decode(config), tests.VAULT_MASTER_KEY ) assert parser() == config_data # Now stub out all functions used to calculate the above result. monkeypatch.setattr( parser, "_parse_contents", null_func("_parse_contents") ) monkeypatch.setattr( parser, "_derive_keys", null_func("_derive_keys") ) monkeypatch.setattr( parser, "_check_signature", null_func("_check_signature") ) monkeypatch.setattr( parser, "_decrypt_payload", null_func("_decrypt_payload") ) assert parser() == config_data super_call = vault_native.VaultNativeConfigParser.__call__ assert super_call(parser) == config_data def test_400_no_password(self) -> None: """Fail on empty master keys/master passphrases.""" with pytest.raises(ValueError, match="Password must not be empty"): vault_native.VaultNativeV03ConfigParser(b"", b"")