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 |