Marco Ricci commited on 2025-08-15 06:51:10
Zeige 1 geänderte Dateien mit 163 Einfügungen und 21 Löschungen.
Implement alluded to, but missing, tests for `derivepassphrase vault`: passphrase usage based on stored configuration, passphrase usage based on the command-line, and exporting configurations that were originally smudged upon import.
| ... | ... |
@@ -273,6 +273,9 @@ def assert_vault_config_is_indented_and_line_broken( |
| 273 | 273 |
class Parametrize(types.SimpleNamespace): |
| 274 | 274 |
"""Common test parametrizations.""" |
| 275 | 275 |
|
| 276 |
+ AUTO_PROMPT = pytest.mark.parametrize( |
|
| 277 |
+ "auto_prompt", [False, True], ids=["normal_prompt", "auto_prompt"] |
|
| 278 |
+ ) |
|
| 276 | 279 |
CHARSET_NAME = pytest.mark.parametrize( |
| 277 | 280 |
"charset_name", ["lower", "upper", "number", "space", "dash", "symbol"] |
| 278 | 281 |
) |
| ... | ... |
@@ -428,6 +431,34 @@ class Parametrize(types.SimpleNamespace): |
| 428 | 431 |
), |
| 429 | 432 |
], |
| 430 | 433 |
) |
| 434 |
+ CONFIG_WITH_PHRASE = pytest.mark.parametrize( |
|
| 435 |
+ "config", |
|
| 436 |
+ [ |
|
| 437 |
+ pytest.param( |
|
| 438 |
+ {
|
|
| 439 |
+ "global": {"phrase": DUMMY_PASSPHRASE.rstrip("\n")},
|
|
| 440 |
+ "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS},
|
|
| 441 |
+ }, |
|
| 442 |
+ id="global", |
|
| 443 |
+ ), |
|
| 444 |
+ pytest.param( |
|
| 445 |
+ {
|
|
| 446 |
+ "global": {
|
|
| 447 |
+ "phrase": DUMMY_PASSPHRASE.rstrip("\n")
|
|
| 448 |
+ + "XXX" |
|
| 449 |
+ + DUMMY_PASSPHRASE.rstrip("\n")
|
|
| 450 |
+ }, |
|
| 451 |
+ "services": {
|
|
| 452 |
+ DUMMY_SERVICE: {
|
|
| 453 |
+ "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
|
|
| 454 |
+ **DUMMY_CONFIG_SETTINGS, |
|
| 455 |
+ } |
|
| 456 |
+ }, |
|
| 457 |
+ }, |
|
| 458 |
+ id="service", |
|
| 459 |
+ ), |
|
| 460 |
+ ], |
|
| 461 |
+ ) |
|
| 431 | 462 |
VALID_TEST_CONFIGS = pytest.mark.parametrize( |
| 432 | 463 |
"config", |
| 433 | 464 |
[conf.config for conf in TEST_CONFIGS if conf.is_valid()], |
| ... | ... |
@@ -786,29 +817,85 @@ class TestDerivedPassphraseConstraints: |
| 786 | 817 |
class TestPhraseBasic: |
| 787 | 818 |
"""Tests for master passphrase configuration: basic.""" |
| 788 | 819 |
|
| 789 |
- @pytest.mark.xfail( |
|
| 790 |
- True, |
|
| 791 |
- reason="not implemented yet", |
|
| 792 |
- raises=NotImplementedError, |
|
| 793 |
- strict=True, |
|
| 794 |
- ) |
|
| 820 |
+ @Parametrize.CONFIG_WITH_PHRASE |
|
| 795 | 821 |
def test_phrase_from_config( |
| 796 | 822 |
self, |
| 823 |
+ config: _types.VaultConfig, |
|
| 797 | 824 |
) -> None: |
| 798 | 825 |
"""A stored configured master passphrase will be used.""" |
| 799 |
- raise NotImplementedError |
|
| 826 |
+ runner = machinery.CliRunner(mix_stderr=False) |
|
| 827 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 828 |
+ # with-statements. |
|
| 829 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 830 |
+ with contextlib.ExitStack() as stack: |
|
| 831 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 832 |
+ stack.enter_context( |
|
| 833 |
+ pytest_machinery.isolated_vault_config( |
|
| 834 |
+ monkeypatch=monkeypatch, |
|
| 835 |
+ runner=runner, |
|
| 836 |
+ vault_config=config, |
|
| 837 |
+ ) |
|
| 838 |
+ ) |
|
| 800 | 839 |
|
| 801 |
- @pytest.mark.xfail( |
|
| 802 |
- True, |
|
| 803 |
- reason="not implemented yet", |
|
| 804 |
- raises=NotImplementedError, |
|
| 805 |
- strict=True, |
|
| 840 |
+ def phrase_from_key(*_args: Any, **_kwargs: Any) -> NoReturn: |
|
| 841 |
+ pytest.fail("Attempted to use a key in a phrase-based test!")
|
|
| 842 |
+ |
|
| 843 |
+ monkeypatch.setattr( |
|
| 844 |
+ vault.Vault, "phrase_from_key", phrase_from_key |
|
| 845 |
+ ) |
|
| 846 |
+ result = runner.invoke( |
|
| 847 |
+ cli.derivepassphrase_vault, |
|
| 848 |
+ ["--", DUMMY_SERVICE], |
|
| 849 |
+ catch_exceptions=False, |
|
| 806 | 850 |
) |
| 851 |
+ assert result.clean_exit(empty_stderr=True), ( |
|
| 852 |
+ "expected clean exit and empty stderr" |
|
| 853 |
+ ) |
|
| 854 |
+ assert result.stdout |
|
| 855 |
+ assert ( |
|
| 856 |
+ result.stdout.rstrip("\n").encode("UTF-8")
|
|
| 857 |
+ == DUMMY_RESULT_PASSPHRASE |
|
| 858 |
+ ), "expected known output" |
|
| 859 |
+ |
|
| 860 |
+ @Parametrize.AUTO_PROMPT |
|
| 807 | 861 |
def test_phrase_from_command_line( |
| 808 | 862 |
self, |
| 863 |
+ auto_prompt: bool, |
|
| 809 | 864 |
) -> None: |
| 810 | 865 |
"""A master passphrase requested on the command-line will be used.""" |
| 811 |
- raise NotImplementedError |
|
| 866 |
+ runner = machinery.CliRunner(mix_stderr=False) |
|
| 867 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 868 |
+ # with-statements. |
|
| 869 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 870 |
+ with contextlib.ExitStack() as stack: |
|
| 871 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 872 |
+ stack.enter_context( |
|
| 873 |
+ pytest_machinery.isolated_vault_config( |
|
| 874 |
+ monkeypatch=monkeypatch, |
|
| 875 |
+ runner=runner, |
|
| 876 |
+ vault_config={
|
|
| 877 |
+ "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
|
|
| 878 |
+ }, |
|
| 879 |
+ ) |
|
| 880 |
+ ) |
|
| 881 |
+ if auto_prompt: |
|
| 882 |
+ monkeypatch.setattr( |
|
| 883 |
+ cli_helpers, |
|
| 884 |
+ "prompt_for_passphrase", |
|
| 885 |
+ callables.auto_prompt, |
|
| 886 |
+ ) |
|
| 887 |
+ result = runner.invoke( |
|
| 888 |
+ cli.derivepassphrase_vault, |
|
| 889 |
+ ["-p", "--", DUMMY_SERVICE], |
|
| 890 |
+ input=None if auto_prompt else DUMMY_PASSPHRASE, |
|
| 891 |
+ catch_exceptions=False, |
|
| 892 |
+ ) |
|
| 893 |
+ assert result.clean_exit(), "expected clean exit" |
|
| 894 |
+ assert result.stdout, "expected program output" |
|
| 895 |
+ last_line = result.stdout.splitlines(True)[-1] |
|
| 896 |
+ assert ( |
|
| 897 |
+ last_line.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_PASSPHRASE
|
|
| 898 |
+ ), "expected known output" |
|
| 812 | 899 |
|
| 813 | 900 |
|
| 814 | 901 |
class TestKeyBasic: |
| ... | ... |
@@ -1542,17 +1629,72 @@ class TestExportConfigValid: |
| 1542 | 1629 |
), "unexpected error output" |
| 1543 | 1630 |
assert_vault_config_is_indented_and_line_broken(result.stdout) |
| 1544 | 1631 |
|
| 1545 |
- @pytest.mark.xfail( |
|
| 1546 |
- True, |
|
| 1547 |
- reason="not implemented yet", |
|
| 1548 |
- raises=NotImplementedError, |
|
| 1549 |
- strict=True, |
|
| 1632 |
+ @hypothesis.settings( |
|
| 1633 |
+ suppress_health_check=[ |
|
| 1634 |
+ *hypothesis.settings().suppress_health_check, |
|
| 1635 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
| 1636 |
+ ], |
|
| 1550 | 1637 |
) |
| 1551 |
- def test_export_smudged_config( |
|
| 1638 |
+ @hypothesis.given( |
|
| 1639 |
+ conf=hypothesis_machinery.smudged_vault_test_config( |
|
| 1640 |
+ strategies.sampled_from([ |
|
| 1641 |
+ conf for conf in data.TEST_CONFIGS if conf.is_valid() |
|
| 1642 |
+ ]) |
|
| 1643 |
+ ) |
|
| 1644 |
+ ) |
|
| 1645 |
+ def test_reexport_smudged_config( |
|
| 1552 | 1646 |
self, |
| 1647 |
+ caplog: pytest.LogCaptureFixture, |
|
| 1648 |
+ conf: data.VaultTestConfig, |
|
| 1553 | 1649 |
) -> None: |
| 1554 |
- """Exporting a smudged configuration works.""" |
|
| 1555 |
- raise NotImplementedError |
|
| 1650 |
+ """Re-exporting a smudged configuration works. |
|
| 1651 |
+ |
|
| 1652 |
+ Tested via hypothesis. |
|
| 1653 |
+ |
|
| 1654 |
+ """ |
|
| 1655 |
+ config = conf.config |
|
| 1656 |
+ config2 = copy.deepcopy(config) |
|
| 1657 |
+ _types.clean_up_falsy_vault_config_values(config2) |
|
| 1658 |
+ # Reset caplog between hypothesis runs. |
|
| 1659 |
+ caplog.clear() |
|
| 1660 |
+ runner = machinery.CliRunner(mix_stderr=False) |
|
| 1661 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1662 |
+ # with-statements. |
|
| 1663 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1664 |
+ with contextlib.ExitStack() as stack: |
|
| 1665 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1666 |
+ stack.enter_context( |
|
| 1667 |
+ pytest_machinery.isolated_vault_config( |
|
| 1668 |
+ monkeypatch=monkeypatch, |
|
| 1669 |
+ runner=runner, |
|
| 1670 |
+ vault_config={"services": {}},
|
|
| 1671 |
+ ) |
|
| 1672 |
+ ) |
|
| 1673 |
+ result1 = runner.invoke( |
|
| 1674 |
+ cli.derivepassphrase_vault, |
|
| 1675 |
+ ["--import", "-"], |
|
| 1676 |
+ input=json.dumps(config), |
|
| 1677 |
+ catch_exceptions=False, |
|
| 1678 |
+ ) |
|
| 1679 |
+ assert result1.clean_exit(empty_stderr=False), ( |
|
| 1680 |
+ "expected clean exit" |
|
| 1681 |
+ ) |
|
| 1682 |
+ assert not result1.stderr or all( |
|
| 1683 |
+ map(is_harmless_config_import_warning, caplog.record_tuples) |
|
| 1684 |
+ ), "unexpected error output" |
|
| 1685 |
+ result2 = runner.invoke( |
|
| 1686 |
+ cli.derivepassphrase_vault, |
|
| 1687 |
+ ["--export", "-"], |
|
| 1688 |
+ catch_exceptions=False, |
|
| 1689 |
+ ) |
|
| 1690 |
+ assert result2.clean_exit(empty_stderr=False), ( |
|
| 1691 |
+ "expected clean exit" |
|
| 1692 |
+ ) |
|
| 1693 |
+ assert not result2.stderr or all( |
|
| 1694 |
+ map(is_harmless_config_import_warning, caplog.record_tuples) |
|
| 1695 |
+ ), "unexpected error output" |
|
| 1696 |
+ config3 = json.loads(result2.stdout) |
|
| 1697 |
+ assert config3 == config2, "config not exported correctly" |
|
| 1556 | 1698 |
|
| 1557 | 1699 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
| 1558 | 1700 |
def test_export_config_no_stored_settings( |
| 1559 | 1701 |