Implement the TODO tests for the `vault` command-line interface
Marco Ricci

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