Rearrange tests as per the new groupings
Marco Ricci

Marco Ricci commited on 2025-08-14 20:58:10
Zeige 4 geänderte Dateien mit 415 Einfügungen und 548 Löschungen.


Also resolve some TODOs, usually by removing duplicate or
quasi-duplicate tests.
... ...
@@ -1056,68 +1056,6 @@ class TestPhraseAndKeyOverriding:
1056 1056
         ), "unexpected error output"
1057 1057
 
1058 1058
 
1059
-# TODO(the-13th-letter): Assimilate into its descendant.
1060
-class TestNotesPrinting000:
1061
-    """Tests concerning printing the service notes: group 000."""
1062
-
1063
-    @hypothesis.given(
1064
-        notes=strategies.text(
1065
-            strategies.characters(
1066
-                min_codepoint=32,
1067
-                max_codepoint=126,
1068
-                include_characters="\n",
1069
-            ),
1070
-            max_size=256,
1071
-        ),
1072
-    )
1073
-    def test_207_service_with_notes_actually_prints_notes(
1074
-        self,
1075
-        notes: str,
1076
-    ) -> None:
1077
-        """Service notes are printed, if they exist."""
1078
-        hypothesis.assume("Error:" not in notes)
1079
-        runner = machinery.CliRunner(mix_stderr=False)
1080
-        # TODO(the-13th-letter): Rewrite using parenthesized
1081
-        # with-statements.
1082
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1083
-        with contextlib.ExitStack() as stack:
1084
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1085
-            stack.enter_context(
1086
-                pytest_machinery.isolated_vault_config(
1087
-                    monkeypatch=monkeypatch,
1088
-                    runner=runner,
1089
-                    vault_config={
1090
-                        "global": {
1091
-                            "phrase": DUMMY_PASSPHRASE,
1092
-                        },
1093
-                        "services": {
1094
-                            DUMMY_SERVICE: {
1095
-                                "notes": notes,
1096
-                                **DUMMY_CONFIG_SETTINGS,
1097
-                            },
1098
-                        },
1099
-                    },
1100
-                )
1101
-            )
1102
-            result = runner.invoke(
1103
-                cli.derivepassphrase_vault,
1104
-                ["--", DUMMY_SERVICE],
1105
-            )
1106
-        assert result.clean_exit(), "expected clean exit"
1107
-        assert result.stdout, "expected program output"
1108
-        assert result.stdout.strip() == DUMMY_RESULT_PASSPHRASE.decode(
1109
-            "ascii"
1110
-        ), "expected known program output"
1111
-        assert result.stderr or not notes.strip(), "expected stderr"
1112
-        assert "Error:" not in result.stderr, (
1113
-            "expected no error messages on stderr"
1114
-        )
1115
-        assert result.stderr.strip() == notes.strip(), (
1116
-            "expected known stderr contents"
1117
-        )
1118
-
1119
-
1120
-# TODO(the-13th-letter): Assimilate the descendants.
1121 1059
 class TestInvalidCommandLines:
1122 1060
     """Tests concerning invalid command-lines."""
1123 1061
 
... ...
@@ -1320,6 +1258,52 @@ class TestInvalidCommandLines:
1320 1258
             "expected error exit and known error message"
1321 1259
         )
1322 1260
 
1261
+    def test_no_arguments(self) -> None:
1262
+        """Calling `derivepassphrase vault` without any arguments fails."""
1263
+        runner = machinery.CliRunner(mix_stderr=False)
1264
+        # TODO(the-13th-letter): Rewrite using parenthesized
1265
+        # with-statements.
1266
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1267
+        with contextlib.ExitStack() as stack:
1268
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1269
+            stack.enter_context(
1270
+                pytest_machinery.isolated_config(
1271
+                    monkeypatch=monkeypatch,
1272
+                    runner=runner,
1273
+                )
1274
+            )
1275
+            result = runner.invoke(
1276
+                cli.derivepassphrase_vault, [], catch_exceptions=False
1277
+            )
1278
+        assert result.error_exit(
1279
+            error="Deriving a passphrase requires a SERVICE"
1280
+        ), "expected error exit and known error message"
1281
+
1282
+    def test_no_passphrase_or_key(
1283
+        self,
1284
+    ) -> None:
1285
+        """Deriving a passphrase without a passphrase or key fails."""
1286
+        runner = machinery.CliRunner(mix_stderr=False)
1287
+        # TODO(the-13th-letter): Rewrite using parenthesized
1288
+        # with-statements.
1289
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1290
+        with contextlib.ExitStack() as stack:
1291
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1292
+            stack.enter_context(
1293
+                pytest_machinery.isolated_config(
1294
+                    monkeypatch=monkeypatch,
1295
+                    runner=runner,
1296
+                )
1297
+            )
1298
+            result = runner.invoke(
1299
+                cli.derivepassphrase_vault,
1300
+                ["--", DUMMY_SERVICE],
1301
+                catch_exceptions=False,
1302
+            )
1303
+        assert result.error_exit(error="No passphrase or key was given"), (
1304
+            "expected error exit and known error message"
1305
+        )
1306
+
1323 1307
 
1324 1308
 class TestImportConfigValid:
1325 1309
     """Tests concerning `vault` configuration imports: valid imports."""
... ...
@@ -1731,9 +1715,64 @@ class TestExportConfigInvalid:
1731 1715
         )
1732 1716
 
1733 1717
 
1734
-# TODO(the-13th-letter): Assimilate the parent.
1735
-class TestNotesPrinting001:
1736
-    """Tests concerning printing the service notes: group 001."""
1718
+class TestNotesPrinting:
1719
+    """Tests concerning printing the service notes."""
1720
+
1721
+    @hypothesis.given(
1722
+        notes=strategies.text(
1723
+            strategies.characters(
1724
+                min_codepoint=32,
1725
+                max_codepoint=126,
1726
+                include_characters="\n",
1727
+            ),
1728
+            max_size=256,
1729
+        ),
1730
+    )
1731
+    def test_service_with_notes_actually_prints_notes(
1732
+        self,
1733
+        notes: str,
1734
+    ) -> None:
1735
+        """Service notes are printed, if they exist."""
1736
+        hypothesis.assume("Error:" not in notes)
1737
+        runner = machinery.CliRunner(mix_stderr=False)
1738
+        # TODO(the-13th-letter): Rewrite using parenthesized
1739
+        # with-statements.
1740
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1741
+        with contextlib.ExitStack() as stack:
1742
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1743
+            stack.enter_context(
1744
+                pytest_machinery.isolated_vault_config(
1745
+                    monkeypatch=monkeypatch,
1746
+                    runner=runner,
1747
+                    vault_config={
1748
+                        "global": {
1749
+                            "phrase": DUMMY_PASSPHRASE,
1750
+                        },
1751
+                        "services": {
1752
+                            DUMMY_SERVICE: {
1753
+                                "notes": notes,
1754
+                                **DUMMY_CONFIG_SETTINGS,
1755
+                            },
1756
+                        },
1757
+                    },
1758
+                )
1759
+            )
1760
+            result = runner.invoke(
1761
+                cli.derivepassphrase_vault,
1762
+                ["--", DUMMY_SERVICE],
1763
+            )
1764
+        assert result.clean_exit(), "expected clean exit"
1765
+        assert result.stdout, "expected program output"
1766
+        assert result.stdout.strip() == DUMMY_RESULT_PASSPHRASE.decode(
1767
+            "ascii"
1768
+        ), "expected known program output"
1769
+        assert result.stderr or not notes.strip(), "expected stderr"
1770
+        assert "Error:" not in result.stderr, (
1771
+            "expected no error messages on stderr"
1772
+        )
1773
+        assert result.stderr.strip() == notes.strip(), (
1774
+            "expected known stderr contents"
1775
+        )
1737 1776
 
1738 1777
     @Parametrize.NOTES_PLACEMENT
1739 1778
     @hypothesis.given(
... ...
@@ -2266,7 +2305,6 @@ class TestNotesEditingInvalid:
2266 2305
             assert config == vault_config
2267 2306
 
2268 2307
 
2269
-# TODO(the-13th-letter): Assimilate the descendants.
2270 2308
 class TestStoringConfigurationSuccesses:
2271 2309
     """Tests concerning storing the configuration: successes."""
2272 2310
 
... ...
@@ -2317,8 +2355,53 @@ class TestStoringConfigurationSuccesses:
2317 2355
             )
2318 2356
             assert_vault_config_is_indented_and_line_broken(config_txt)
2319 2357
 
2358
+    def test_config_directory_nonexistant(
2359
+        self,
2360
+    ) -> None:
2361
+        """Running without an existing config directory works.
2362
+
2363
+        This is a regression test; see [issue\u00a0#6][] for context.
2364
+        See also
2365
+        [TestStoringConfigurationFailures001.test_config_directory_not_a_file][]
2366
+        for a related aspect of this.
2367
+
2368
+        [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6
2369
+
2370
+        """
2371
+        runner = machinery.CliRunner(mix_stderr=False)
2372
+        # TODO(the-13th-letter): Rewrite using parenthesized
2373
+        # with-statements.
2374
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2375
+        with contextlib.ExitStack() as stack:
2376
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2377
+            stack.enter_context(
2378
+                pytest_machinery.isolated_config(
2379
+                    monkeypatch=monkeypatch,
2380
+                    runner=runner,
2381
+                )
2382
+            )
2383
+            with contextlib.suppress(FileNotFoundError):
2384
+                shutil.rmtree(cli_helpers.config_filename(subsystem=None))
2385
+            result = runner.invoke(
2386
+                cli.derivepassphrase_vault,
2387
+                ["--config", "-p"],
2388
+                catch_exceptions=False,
2389
+                input="abc\n",
2390
+            )
2391
+            assert result.clean_exit(), "expected clean exit"
2392
+            assert result.stderr == "Passphrase:", (
2393
+                "program unexpectedly failed?!"
2394
+            )
2395
+            with cli_helpers.config_filename(subsystem="vault").open(
2396
+                encoding="UTF-8"
2397
+            ) as infile:
2398
+                config_readback = json.load(infile)
2399
+            assert config_readback == {
2400
+                "global": {"phrase": "abc"},
2401
+                "services": {},
2402
+            }, "config mismatch"
2403
+
2320 2404
 
2321
-# TODO(the-13th-letter): Assimilate the descendants.
2322 2405
 class TestStoringConfigurationFailures:
2323 2406
     """Tests concerning storing the configuration: failures."""
2324 2407
 
... ...
@@ -2660,113 +2743,6 @@ class TestStoringConfigurationFailures:
2660 2743
             "expected error exit and known error message"
2661 2744
         )
2662 2745
 
2663
-
2664
-# TODO(the-13th-letter): Assimilate the parent.
2665
-class TestInvalidCommandLines001(TestInvalidCommandLines):
2666
-    """Tests concerning invalid command-lines: group 001."""
2667
-
2668
-    def test_no_arguments(self) -> None:
2669
-        """Calling `derivepassphrase vault` without any arguments fails."""
2670
-        runner = machinery.CliRunner(mix_stderr=False)
2671
-        # TODO(the-13th-letter): Rewrite using parenthesized
2672
-        # with-statements.
2673
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2674
-        with contextlib.ExitStack() as stack:
2675
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2676
-            stack.enter_context(
2677
-                pytest_machinery.isolated_config(
2678
-                    monkeypatch=monkeypatch,
2679
-                    runner=runner,
2680
-                )
2681
-            )
2682
-            result = runner.invoke(
2683
-                cli.derivepassphrase_vault, [], catch_exceptions=False
2684
-            )
2685
-        assert result.error_exit(
2686
-            error="Deriving a passphrase requires a SERVICE"
2687
-        ), "expected error exit and known error message"
2688
-
2689
-    def test_no_passphrase_or_key(
2690
-        self,
2691
-    ) -> None:
2692
-        """Deriving a passphrase without a passphrase or key fails."""
2693
-        runner = machinery.CliRunner(mix_stderr=False)
2694
-        # TODO(the-13th-letter): Rewrite using parenthesized
2695
-        # with-statements.
2696
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2697
-        with contextlib.ExitStack() as stack:
2698
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2699
-            stack.enter_context(
2700
-                pytest_machinery.isolated_config(
2701
-                    monkeypatch=monkeypatch,
2702
-                    runner=runner,
2703
-                )
2704
-            )
2705
-            result = runner.invoke(
2706
-                cli.derivepassphrase_vault,
2707
-                ["--", DUMMY_SERVICE],
2708
-                catch_exceptions=False,
2709
-            )
2710
-        assert result.error_exit(error="No passphrase or key was given"), (
2711
-            "expected error exit and known error message"
2712
-        )
2713
-
2714
-
2715
-# TODO(the-13th-letter): Assimilate into the parent.
2716
-class TestStoringConfigurationSuccesses001:
2717
-    """Tests concerning storing the configuration: successes, group 001."""
2718
-
2719
-    def test_config_directory_nonexistant(
2720
-        self,
2721
-    ) -> None:
2722
-        """Running without an existing config directory works.
2723
-
2724
-        This is a regression test; see [issue\u00a0#6][] for context.
2725
-        See also
2726
-        [TestStoringConfigurationFailures001.test_config_directory_not_a_file][]
2727
-        for a related aspect of this.
2728
-
2729
-        [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6
2730
-
2731
-        """
2732
-        runner = machinery.CliRunner(mix_stderr=False)
2733
-        # TODO(the-13th-letter): Rewrite using parenthesized
2734
-        # with-statements.
2735
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2736
-        with contextlib.ExitStack() as stack:
2737
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2738
-            stack.enter_context(
2739
-                pytest_machinery.isolated_config(
2740
-                    monkeypatch=monkeypatch,
2741
-                    runner=runner,
2742
-                )
2743
-            )
2744
-            with contextlib.suppress(FileNotFoundError):
2745
-                shutil.rmtree(cli_helpers.config_filename(subsystem=None))
2746
-            result = runner.invoke(
2747
-                cli.derivepassphrase_vault,
2748
-                ["--config", "-p"],
2749
-                catch_exceptions=False,
2750
-                input="abc\n",
2751
-            )
2752
-            assert result.clean_exit(), "expected clean exit"
2753
-            assert result.stderr == "Passphrase:", (
2754
-                "program unexpectedly failed?!"
2755
-            )
2756
-            with cli_helpers.config_filename(subsystem="vault").open(
2757
-                encoding="UTF-8"
2758
-            ) as infile:
2759
-                config_readback = json.load(infile)
2760
-            assert config_readback == {
2761
-                "global": {"phrase": "abc"},
2762
-                "services": {},
2763
-            }, "config mismatch"
2764
-
2765
-
2766
-# TODO(the-13th-letter): Assimilate into the parent.
2767
-class TestStoringConfigurationFailures001:
2768
-    """Tests concerning storing the configuration: failures, group 001."""
2769
-
2770 2746
     def test_config_directory_not_a_file(
2771 2747
         self,
2772 2748
     ) -> None:
... ...
@@ -2818,42 +2794,6 @@ class TestStoringConfigurationFailures001:
2818 2794
                 "expected error exit and known error message"
2819 2795
             )
2820 2796
 
2821
-    # TODO(the-13th-letter): Remove this test, because it is basically
2822
-    # the same as
2823
-    # TestStoringConfigurationFailures.test_fail_because_of_custom_error.
2824
-    def test_store_config_custom_error(
2825
-        self,
2826
-    ) -> None:
2827
-        """Storing the configuration reacts even to weird errors."""
2828
-        runner = machinery.CliRunner(mix_stderr=False)
2829
-        # TODO(the-13th-letter): Rewrite using parenthesized
2830
-        # with-statements.
2831
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2832
-        with contextlib.ExitStack() as stack:
2833
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2834
-            stack.enter_context(
2835
-                pytest_machinery.isolated_config(
2836
-                    monkeypatch=monkeypatch,
2837
-                    runner=runner,
2838
-                )
2839
-            )
2840
-            custom_error = "custom error message"
2841
-
2842
-            def raiser(config: Any) -> None:
2843
-                del config
2844
-                raise RuntimeError(custom_error)
2845
-
2846
-            monkeypatch.setattr(cli_helpers, "save_config", raiser)
2847
-            result = runner.invoke(
2848
-                cli.derivepassphrase_vault,
2849
-                ["--config", "-p"],
2850
-                catch_exceptions=False,
2851
-                input="abc\n",
2852
-            )
2853
-            assert result.error_exit(error=custom_error), (
2854
-                "expected error exit and known error message"
2855
-            )
2856
-
2857 2797
 
2858 2798
 class TestPassphraseUnicodeNormalization:
2859 2799
     """Tests concerning the Unicode normalization of passphrases."""
... ...
@@ -56,7 +56,6 @@ class Parametrize(test_000_basic.Parametrize, test_utils.Parametrize):
56 56
     )
57 57
 
58 58
 
59
-# TODO(the-13th-letter): Assimilate the descendants.
60 59
 class TestConfigMigrationMachinery:
61 60
     """Tests for the configuration file migration machinery."""
62 61
 
... ...
@@ -162,6 +161,36 @@ class TestConfigMigrationMachinery:
162 161
             ):
163 162
                 cli_helpers.migrate_and_load_old_config()
164 163
 
164
+    def test_completion_supports_old_config_file(
165
+        self,
166
+    ) -> None:
167
+        """Completing service names from the old settings file works."""
168
+        config = {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}
169
+        runner = machinery.CliRunner(mix_stderr=False)
170
+        # TODO(the-13th-letter): Rewrite using parenthesized
171
+        # with-statements.
172
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
173
+        with contextlib.ExitStack() as stack:
174
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
175
+            stack.enter_context(
176
+                pytest_machinery.isolated_vault_config(
177
+                    monkeypatch=monkeypatch,
178
+                    runner=runner,
179
+                    vault_config=config,
180
+                )
181
+            )
182
+            old_name = cli_helpers.config_filename(
183
+                subsystem="old settings.json"
184
+            )
185
+            new_name = cli_helpers.config_filename(subsystem="vault")
186
+            old_name.unlink(missing_ok=True)
187
+            new_name.rename(old_name)
188
+            assert cli_helpers.shell_complete_service(
189
+                click.Context(cli.derivepassphrase),
190
+                click.Argument(["some_parameter"]),
191
+                "",
192
+            ) == [DUMMY_SERVICE]
193
+
165 194
 
166 195
 class TestArgumentForwarding:
167 196
     """Tests for the argument forwarding up to v1.0."""
... ...
@@ -403,38 +432,3 @@ class TestConfigMigration:
403 432
         assert machinery.warning_emitted(
404 433
             "Failed to migrate to ", caplog.record_tuples
405 434
         ), "expected known warning message in stderr"
406
-
407
-
408
-# TODO(the-13th-letter): Assimilate into the parent.
409
-class TestConfigMigrationMachinery001(TestConfigMigrationMachinery):
410
-    """Tests for the configuration file migration machinery, group 001."""
411
-
412
-    def test_completion_supports_old_config_file(
413
-        self,
414
-    ) -> None:
415
-        """Completing service names from the old settings file works."""
416
-        config = {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}
417
-        runner = machinery.CliRunner(mix_stderr=False)
418
-        # TODO(the-13th-letter): Rewrite using parenthesized
419
-        # with-statements.
420
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
421
-        with contextlib.ExitStack() as stack:
422
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
423
-            stack.enter_context(
424
-                pytest_machinery.isolated_vault_config(
425
-                    monkeypatch=monkeypatch,
426
-                    runner=runner,
427
-                    vault_config=config,
428
-                )
429
-            )
430
-            old_name = cli_helpers.config_filename(
431
-                subsystem="old settings.json"
432
-            )
433
-            new_name = cli_helpers.config_filename(subsystem="vault")
434
-            old_name.unlink(missing_ok=True)
435
-            new_name.rename(old_name)
436
-            assert cli_helpers.shell_complete_service(
437
-                click.Context(cli.derivepassphrase),
438
-                click.Argument(["some_parameter"]),
439
-                "",
440
-            ) == [DUMMY_SERVICE]
... ...
@@ -785,25 +785,6 @@ class TestStaticFunctionality:
785 785
         return ssh_agent.SSHAgentClient.string(unstringed)
786 786
 
787 787
 
788
-class TestObsolete001:
789
-    """Obsolete tests: group 001."""
790
-
791
-    # TODO(the-13th-letter): Remove this test. The test key data has its
792
-    # own set of tests now.
793
-    @Parametrize.PUBLIC_KEY_DATA
794
-    def test_100_key_decoding(
795
-        self,
796
-        public_key_struct: data.SSHTestKey,
797
-    ) -> None:
798
-        """The [`tests.ALL_KEYS`][] public key data looks sane."""
799
-        keydata = base64.b64decode(
800
-            public_key_struct.public_key.split(None, 2)[1]
801
-        )
802
-        assert keydata == public_key_struct.public_key_data, (
803
-            "recorded public key data doesn't match"
804
-        )
805
-
806
-
807 788
 class TestShellExportScriptParsing:
808 789
     """Test the shell export script parsing utility function."""
809 790
 
... ...
@@ -952,6 +933,44 @@ class TestSSHProtocolDatatypes(TestStaticFunctionality):
952 933
                 assert canon1(encoded) == canon2(encoded)
953 934
                 assert canon1(canon2(encoded)) == canon1(encoded)
954 935
 
936
+    @Parametrize.UINT32_EXCEPTIONS
937
+    def test_uint32_exceptions(
938
+        self, input: int, exc_type: type[Exception], exc_pattern: str
939
+    ) -> None:
940
+        """`uint32` encoding fails for out-of-bound values."""
941
+        uint32 = ssh_agent.SSHAgentClient.uint32
942
+        with pytest.raises(exc_type, match=exc_pattern):
943
+            uint32(input)
944
+
945
+    @Parametrize.SSH_STRING_EXCEPTIONS
946
+    def test_string_exceptions(
947
+        self, input: Any, exc_type: type[Exception], exc_pattern: str
948
+    ) -> None:
949
+        """SSH string encoding fails for non-strings."""
950
+        string = ssh_agent.SSHAgentClient.string
951
+        with pytest.raises(exc_type, match=exc_pattern):
952
+            string(input)
953
+
954
+    @Parametrize.SSH_UNSTRING_EXCEPTIONS
955
+    def test_unstring_exceptions(
956
+        self,
957
+        input: bytes | bytearray,
958
+        exc_type: type[Exception],
959
+        exc_pattern: str,
960
+        has_trailer: bool,
961
+        parts: tuple[bytes | bytearray, bytes | bytearray] | None,
962
+    ) -> None:
963
+        """SSH string decoding fails for invalid values."""
964
+        unstring = ssh_agent.SSHAgentClient.unstring
965
+        unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
966
+        with pytest.raises(exc_type, match=exc_pattern):
967
+            unstring(input)
968
+        if has_trailer:
969
+            assert tuple(bytes(x) for x in unstring_prefix(input)) == parts
970
+        else:
971
+            with pytest.raises(exc_type, match=exc_pattern):
972
+                unstring_prefix(input)
973
+
955 974
 
956 975
 class TestSSHAgentSocketProviderRegistry:
957 976
     """Tests for the SSH agent socket provider registry."""
... ...
@@ -1102,54 +1121,6 @@ class TestSSHAgentSocketProviderRegistry:
1102 1121
             stack.enter_context(pytest.raises(AssertionError))
1103 1122
             socketprovider.SocketProvider._find_all_ssh_agent_socket_providers()
1104 1123
 
1105
-
1106
-# TODO(the-13th-letter): Assimilate into base class.
1107
-class TestSSHProtocolDatatypes001(TestSSHProtocolDatatypes):
1108
-    """More tests for the utility functions for the SSH protocol datatypes."""
1109
-
1110
-    @Parametrize.UINT32_EXCEPTIONS
1111
-    def test_uint32_exceptions(
1112
-        self, input: int, exc_type: type[Exception], exc_pattern: str
1113
-    ) -> None:
1114
-        """`uint32` encoding fails for out-of-bound values."""
1115
-        uint32 = ssh_agent.SSHAgentClient.uint32
1116
-        with pytest.raises(exc_type, match=exc_pattern):
1117
-            uint32(input)
1118
-
1119
-    @Parametrize.SSH_STRING_EXCEPTIONS
1120
-    def test_string_exceptions(
1121
-        self, input: Any, exc_type: type[Exception], exc_pattern: str
1122
-    ) -> None:
1123
-        """SSH string encoding fails for non-strings."""
1124
-        string = ssh_agent.SSHAgentClient.string
1125
-        with pytest.raises(exc_type, match=exc_pattern):
1126
-            string(input)
1127
-
1128
-    @Parametrize.SSH_UNSTRING_EXCEPTIONS
1129
-    def test_unstring_exceptions(
1130
-        self,
1131
-        input: bytes | bytearray,
1132
-        exc_type: type[Exception],
1133
-        exc_pattern: str,
1134
-        has_trailer: bool,
1135
-        parts: tuple[bytes | bytearray, bytes | bytearray] | None,
1136
-    ) -> None:
1137
-        """SSH string decoding fails for invalid values."""
1138
-        unstring = ssh_agent.SSHAgentClient.unstring
1139
-        unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
1140
-        with pytest.raises(exc_type, match=exc_pattern):
1141
-            unstring(input)
1142
-        if has_trailer:
1143
-            assert tuple(bytes(x) for x in unstring_prefix(input)) == parts
1144
-        else:
1145
-            with pytest.raises(exc_type, match=exc_pattern):
1146
-                unstring_prefix(input)
1147
-
1148
-
1149
-# TODO(the-13th-letter): Assimilate into base class.
1150
-class TestSSHAgentSocketProviderRegistry001(TestSSHAgentSocketProviderRegistry):
1151
-    """More tests for the SSH agent socket provider registry."""
1152
-
1153 1124
     def test_already_registered(
1154 1125
         self,
1155 1126
     ) -> None:
... ...
@@ -138,6 +138,10 @@ class TestPhraseDependence:
138 138
             max_size=BLOCK_SIZE // 2,
139 139
         ),
140 140
     )
141
+    @hypothesis.example(phrases=[b"\x00", b"\x00\x00"], service="0").xfail(
142
+        reason="phrases are interchangable",
143
+        raises=AssertionError,
144
+    )
141 145
     def test_small(
142 146
         self,
143 147
         phrases: list[bytes],
... ...
@@ -227,6 +231,23 @@ class TestPhraseDependence:
227 231
             max_size=BLOCK_SIZE // 2,
228 232
         ),
229 233
     )
234
+    @hypothesis.example(
235
+        phrases=[
236
+            (
237
+                b"plnlrtfpijpuhqylxbgqiiyipieyxvfs"
238
+                b"avzgxbbcfusqkozwpngsyejqlmjsytrmd"
239
+            ),
240
+            b"eBkXQTfuBqp'cTcar&g*",
241
+        ],
242
+        service="any service name here",
243
+    ).xfail(
244
+        reason=(
245
+            "phrases are interchangable (Wikipedia example:"
246
+            "https://en.wikipedia.org/w/index.php?title=PBKDF2&oldid=1264881215#HMAC_collisions"
247
+            ")"
248
+        ),
249
+        raises=AssertionError,
250
+    )
230 251
     def test_mixed(
231 252
         self,
232 253
         phrases: list[bytes],
... ...
@@ -241,6 +262,46 @@ class TestPhraseDependence:
241 262
             phrase=phrases[0], service=service
242 263
         ) != vault.Vault.create_hash(phrase=phrases[1], service=service)
243 264
 
265
+    @hypothesis.given(
266
+        phrases=strategies.lists(
267
+            strategies.text(
268
+                strategies.characters(min_codepoint=32, max_codepoint=126),
269
+                min_size=1,
270
+                max_size=32,
271
+            ),
272
+            min_size=2,
273
+            max_size=2,
274
+            unique=True,
275
+        ),
276
+        config=hypothesis_machinery.vault_full_service_config(),
277
+        service=strategies.text(
278
+            strategies.characters(min_codepoint=32, max_codepoint=126),
279
+            min_size=1,
280
+            max_size=32,
281
+        ),
282
+    )
283
+    def test_phrase_dependence_with_config(
284
+        self,
285
+        phrases: list[str],
286
+        config: dict[str, int],
287
+        service: bytes,
288
+    ) -> None:
289
+        """The derived passphrase is dependent on the master passphrase."""
290
+        try:
291
+            assert vault.Vault(phrase=phrases[0], **config).generate(
292
+                service
293
+            ) != vault.Vault(phrase=phrases[1], **config).generate(service)
294
+        except ValueError as exc:  # pragma: no cover
295
+            # The service configuration strategy attempts to only
296
+            # generate satisfiable configurations.  It is possible,
297
+            # though rare, that this fails, and that unsatisfiability is
298
+            # only recognized when actually deriving a passphrase.  In
299
+            # that case, reject the generated configuration.
300
+            hypothesis.assume("no allowed characters left" not in exc.args)
301
+            # Otherwise it's a genuine bug in the test case or the
302
+            # implementation, and should be raised.
303
+            raise
304
+
244 305
 
245 306
 class TestServiceNameDependence:
246 307
     """Test the dependence of the internal hash on the service name."""
... ...
@@ -268,6 +329,46 @@ class TestServiceNameDependence:
268 329
             phrase=phrase, service=services[0]
269 330
         ) != vault.Vault.create_hash(phrase=phrase, service=services[1])
270 331
 
332
+    @hypothesis.given(
333
+        phrase=strategies.text(
334
+            strategies.characters(min_codepoint=32, max_codepoint=126),
335
+            min_size=1,
336
+            max_size=32,
337
+        ),
338
+        config=hypothesis_machinery.vault_full_service_config(),
339
+        services=strategies.lists(
340
+            strategies.text(
341
+                strategies.characters(min_codepoint=32, max_codepoint=126),
342
+                min_size=1,
343
+                max_size=32,
344
+            ),
345
+            min_size=2,
346
+            max_size=2,
347
+            unique=True,
348
+        ),
349
+    )
350
+    def test_service_name_dependence_with_config(
351
+        self,
352
+        phrase: str,
353
+        config: dict[str, int],
354
+        services: list[bytes],
355
+    ) -> None:
356
+        """The derived passphrase is dependent on the service name."""
357
+        try:
358
+            assert vault.Vault(phrase=phrase, **config).generate(
359
+                services[0]
360
+            ) != vault.Vault(phrase=phrase, **config).generate(services[1])
361
+        except ValueError as exc:  # pragma: no cover
362
+            # The service configuration strategy attempts to only
363
+            # generate satisfiable configurations.  It is possible,
364
+            # though rare, that this fails, and that unsatisfiability is
365
+            # only recognized when actually deriving a passphrase.  In
366
+            # that case, reject the generated configuration.
367
+            hypothesis.assume("no allowed characters left" not in exc.args)
368
+            # Otherwise it's a genuine bug in the test case or the
369
+            # implementation, and should be raised.
370
+            raise
371
+
271 372
 
272 373
 class TestInterchangablePhrases:
273 374
     """Test the interchangability of certain master passphrases."""
... ...
@@ -348,61 +449,6 @@ class TestBasicFunctionalityFromUpstream(TestVault):
348 449
             == b"n+oIz6sL>K*lTEWYRO%7"
349 450
         )
350 451
 
351
-    # TODO(the-13th-letter): Retire this test in favor of
352
-    # TestPhraseDependence.  The first example is a "short" example, the
353
-    # second is a "mixed" one.
354
-    @hypothesis.given(
355
-        phrases=strategies.lists(
356
-            strategies.binary(min_size=1, max_size=32),
357
-            min_size=2,
358
-            max_size=2,
359
-            unique=True,
360
-        ).filter(lambda tup: not phrases_are_interchangable(*tup)),
361
-        service=strategies.text(
362
-            strategies.characters(min_codepoint=32, max_codepoint=126),
363
-            min_size=1,
364
-            max_size=32,
365
-        ),
366
-    )
367
-    @hypothesis.example(phrases=[b"\x00", b"\x00\x00"], service="0").xfail(
368
-        reason="phrases are interchangable",
369
-        raises=AssertionError,
370
-    )
371
-    @hypothesis.example(
372
-        phrases=[
373
-            (
374
-                b"plnlrtfpijpuhqylxbgqiiyipieyxvfs"
375
-                b"avzgxbbcfusqkozwpngsyejqlmjsytrmd"
376
-            ),
377
-            b"eBkXQTfuBqp'cTcar&g*",
378
-        ],
379
-        service="any service name here",
380
-    ).xfail(
381
-        reason=(
382
-            "phrases are interchangable (Wikipedia example:"
383
-            "https://en.wikipedia.org/w/index.php?title=PBKDF2&oldid=1264881215#HMAC_collisions"
384
-            ")"
385
-        ),
386
-        raises=AssertionError,
387
-    )
388
-    def test_xxx_phrase_dependence(
389
-        self,
390
-        phrases: list[bytes],
391
-        service: str,
392
-    ) -> None:
393
-        """The derived passphrase is dependent on the master passphrase.
394
-
395
-        Certain pairs of master passphrases are known to be
396
-        interchangable; see [`vault.Vault.phrases_are_interchangable`][].
397
-        These are excluded from consideration by the hypothesis
398
-        strategy.
399
-
400
-        """
401
-        # See test_100_create_hash_phrase_dependence for context.
402
-        assert vault.Vault(phrase=phrases[0]).generate(service) != vault.Vault(
403
-            phrase=phrases[1]
404
-        ).generate(service)
405
-
406 452
 
407 453
 class TestStringAndBinaryExchangability(TestVault):
408 454
     """Test the exchangability of text and byte strings in the "vault" scheme.
... ...
@@ -502,76 +548,8 @@ class TestStringAndBinaryExchangability(TestVault):
502 548
                 "service name generate different passphrases"
503 549
             )
504 550
 
505
-    # TODO(the-13th-letter): Retire this test in favor of
506
-    # TestServiceNameDependence.
507
-    @hypothesis.given(
508
-        phrase=strategies.text(
509
-            strategies.characters(min_codepoint=32, max_codepoint=126),
510
-            min_size=1,
511
-            max_size=32,
512
-        ),
513
-        services=strategies.lists(
514
-            strategies.binary(min_size=1, max_size=32),
515
-            min_size=2,
516
-            max_size=2,
517
-            unique=True,
518
-        ),
519
-    )
520
-    def test_xxx_service_name_dependence(
521
-        self,
522
-        phrase: str,
523
-        services: list[bytes],
524
-    ) -> None:
525
-        """The derived passphrase is dependent on the service name."""
526
-        assert vault.Vault(phrase=phrase).generate(services[0]) != vault.Vault(
527
-            phrase=phrase
528
-        ).generate(services[1])
529
-
530
-    # TODO(the-13th-letter): Move this test into TestServiceNameDependence
531
-    # and write a counterpart for TestPhraseDependence. Or, generalize
532
-    # this test into a class TestConfigDependence.
533
-    @hypothesis.given(
534
-        phrase=strategies.text(
535
-            strategies.characters(min_codepoint=32, max_codepoint=126),
536
-            min_size=1,
537
-            max_size=32,
538
-        ),
539
-        config=hypothesis_machinery.vault_full_service_config(),
540
-        services=strategies.lists(
541
-            strategies.binary(min_size=1, max_size=32),
542
-            min_size=2,
543
-            max_size=2,
544
-            unique=True,
545
-        ),
546
-    )
547
-    def test_service_name_dependence_with_config(
548
-        self,
549
-        phrase: str,
550
-        config: dict[str, int],
551
-        services: list[bytes],
552
-    ) -> None:
553
-        """The derived passphrase is dependent on the service name."""
554
-        try:
555
-            assert vault.Vault(phrase=phrase, **config).generate(
556
-                services[0]
557
-            ) != vault.Vault(phrase=phrase, **config).generate(services[1])
558
-        except ValueError as exc:  # pragma: no cover
559
-            # The service configuration strategy attempts to only
560
-            # generate satisfiable configurations.  It is possible,
561
-            # though rare, that this fails, and that unsatisfiability is
562
-            # only recognized when actually deriving a passphrase.  In
563
-            # that case, reject the generated configuration.
564
-            hypothesis.assume("no allowed characters left" not in exc.args)
565
-            # Otherwise it's a genuine bug in the test case or the
566
-            # implementation, and should be raised.
567
-            raise
568
-
569
-
570
-# TODO(the-13th-letter): State machine, for master passphrase, service
571
-# name and config dependence?
572
-
573 551
 
574
-class TestConstraintSatisfactionFromUpstream001(TestVault):
552
+class TestConstraintSatisfactionFromUpstream(TestVault):
575 553
     """Test passphrase derivation with the "vault" scheme: upstream tests."""
576 554
 
577 555
     def test_nonstandard_length(self) -> None:
... ...
@@ -581,36 +559,6 @@ class TestConstraintSatisfactionFromUpstream001(TestVault):
581 559
             == b"xDFu"
582 560
         )
583 561
 
584
-
585
-class TestConstraintSatisfactionThoroughness001(TestVault):
586
-    """Test passphrase derivation with the "vault" scheme: constraint satisfaction."""
587
-
588
-    @hypothesis.given(
589
-        phrase=strategies.one_of(
590
-            strategies.binary(min_size=1, max_size=100),
591
-            strategies.text(
592
-                min_size=1,
593
-                max_size=100,
594
-                alphabet=strategies.characters(max_codepoint=255),
595
-            ),
596
-        ),
597
-        length=strategies.integers(min_value=1, max_value=200),
598
-        service=strategies.text(min_size=1, max_size=100),
599
-    )
600
-    def test_password_with_length(
601
-        self,
602
-        phrase: str | bytes,
603
-        length: int,
604
-        service: str,
605
-    ) -> None:
606
-        """Derived passphrases have the requested length."""
607
-        password = vault.Vault(phrase=phrase, length=length).generate(service)
608
-        assert len(password) == length
609
-
610
-
611
-class TestConstraintSatisfactionFromUpstream002(TestVault):
612
-    """Test passphrase derivation with the "vault" scheme: upstream tests."""
613
-
614 562
     def test_repetition_limit(self) -> None:
615 563
         """Deriving a passphrase adheres to imposed repetition limits."""
616 564
         assert (
... ...
@@ -674,8 +622,102 @@ class TestConstraintSatisfactionFromUpstream002(TestVault):
674 622
             == b": : fv_wqt>a-4w1S  R"
675 623
         )
676 624
 
625
+    def test_only_numbers_and_very_high_repetition_limit(self) -> None:
626
+        """Deriving a passphrase adheres to imposed repetition limits.
627
+
628
+        This example is checked explicitly against forbidden substrings.
629
+
630
+        """
631
+        generated = vault.Vault(
632
+            phrase=b"",
633
+            length=40,
634
+            lower=0,
635
+            upper=0,
636
+            space=0,
637
+            dash=0,
638
+            symbol=0,
639
+            repeat=4,
640
+        ).generate("abcdef")
641
+        forbidden_substrings = {
642
+            b"0000",
643
+            b"1111",
644
+            b"2222",
645
+            b"3333",
646
+            b"4444",
647
+            b"5555",
648
+            b"6666",
649
+            b"7777",
650
+            b"8888",
651
+            b"9999",
652
+        }
653
+        for substring in forbidden_substrings:
654
+            assert substring not in generated
655
+
656
+    def test_very_limited_character_set(self) -> None:
657
+        """Deriving a passphrase works even with limited character sets."""
658
+        generated = vault.Vault(
659
+            phrase=b"", length=24, lower=0, upper=0, space=0, symbol=0
660
+        ).generate("testing")
661
+        assert generated == b"763252593304946694588866"
662
+
663
+
664
+class TestConstraintSatisfactionThoroughness(TestVault):
665
+    """Test passphrase derivation with the "vault" scheme: constraint satisfaction."""
666
+
667
+    @hypothesis.given(
668
+        phrase=strategies.one_of(
669
+            strategies.binary(min_size=1, max_size=100),
670
+            strategies.text(
671
+                min_size=1,
672
+                max_size=100,
673
+                alphabet=strategies.characters(max_codepoint=255),
674
+            ),
675
+        ),
676
+        length=strategies.integers(min_value=1, max_value=200),
677
+        service=strategies.text(min_size=1, max_size=100),
678
+    )
679
+    def test_password_with_length(
680
+        self,
681
+        phrase: str | bytes,
682
+        length: int,
683
+        service: str,
684
+    ) -> None:
685
+        """Derived passphrases have the requested length."""
686
+        password = vault.Vault(phrase=phrase, length=length).generate(service)
687
+        assert len(password) == length
688
+
689
+    # This test has time complexity `O(length * repeat)`, both of which
690
+    # are chosen by hypothesis and thus outside our control.
691
+    @hypothesis.settings(deadline=None)
692
+    @hypothesis.given(
693
+        phrase=strategies.one_of(
694
+            strategies.binary(min_size=1, max_size=100),
695
+            strategies.text(
696
+                min_size=1,
697
+                max_size=100,
698
+                alphabet=strategies.characters(max_codepoint=255),
699
+            ),
700
+        ),
701
+        length=strategies.integers(min_value=2, max_value=200),
702
+        repeat=strategies.integers(min_value=1, max_value=200),
703
+        service=strategies.text(min_size=1, max_size=1000),
704
+    )
705
+    def test_arbitrary_repetition_limit(
706
+        self,
707
+        phrase: str | bytes,
708
+        length: int,
709
+        repeat: int,
710
+        service: str,
711
+    ) -> None:
712
+        """Derived passphrases obey the given occurrence constraint."""
713
+        password = vault.Vault(
714
+            phrase=phrase, length=length, repeat=repeat
715
+        ).generate(service)
716
+        for i in range((length + 1) - (repeat + 1)):
717
+            assert len(set(password[i : i + repeat + 1])) > 1
718
+
677 719
 
678
-class TestConstraintSatisfactionHeavyDuty001(TestVault):
720
+class TestConstraintSatisfactionHeavyDuty(TestVault):
679 721
     """Test passphrase derivation with the "vault" scheme: constraint satisfaction."""
680 722
 
681 723
     @hypothesis.given(
... ...
@@ -780,86 +822,6 @@ class TestConstraintSatisfactionHeavyDuty001(TestVault):
780 822
                 )
781 823
 
782 824
 
783
-class TestConstraintSatisfactionFromUpstream003(TestVault):
784
-    """Test passphrase derivation with the "vault" scheme: upstream tests."""
785
-
786
-    def test_only_numbers_and_very_high_repetition_limit(self) -> None:
787
-        """Deriving a passphrase adheres to imposed repetition limits.
788
-
789
-        This example is checked explicitly against forbidden substrings.
790
-
791
-        """
792
-        generated = vault.Vault(
793
-            phrase=b"",
794
-            length=40,
795
-            lower=0,
796
-            upper=0,
797
-            space=0,
798
-            dash=0,
799
-            symbol=0,
800
-            repeat=4,
801
-        ).generate("abcdef")
802
-        forbidden_substrings = {
803
-            b"0000",
804
-            b"1111",
805
-            b"2222",
806
-            b"3333",
807
-            b"4444",
808
-            b"5555",
809
-            b"6666",
810
-            b"7777",
811
-            b"8888",
812
-            b"9999",
813
-        }
814
-        for substring in forbidden_substrings:
815
-            assert substring not in generated
816
-
817
-
818
-class TestConstraintSatisfactionThoroughness002(TestVault):
819
-    """Test passphrase derivation with the "vault" scheme: constraint satisfaction."""
820
-
821
-    # This test has time complexity `O(length * repeat)`, both of which
822
-    # are chosen by hypothesis and thus outside our control.
823
-    @hypothesis.settings(deadline=None)
824
-    @hypothesis.given(
825
-        phrase=strategies.one_of(
826
-            strategies.binary(min_size=1, max_size=100),
827
-            strategies.text(
828
-                min_size=1,
829
-                max_size=100,
830
-                alphabet=strategies.characters(max_codepoint=255),
831
-            ),
832
-        ),
833
-        length=strategies.integers(min_value=2, max_value=200),
834
-        repeat=strategies.integers(min_value=1, max_value=200),
835
-        service=strategies.text(min_size=1, max_size=1000),
836
-    )
837
-    def test_218a_arbitrary_repetition_limit(
838
-        self,
839
-        phrase: str | bytes,
840
-        length: int,
841
-        repeat: int,
842
-        service: str,
843
-    ) -> None:
844
-        """Derived passphrases obey the given occurrence constraint."""
845
-        password = vault.Vault(
846
-            phrase=phrase, length=length, repeat=repeat
847
-        ).generate(service)
848
-        for i in range((length + 1) - (repeat + 1)):
849
-            assert len(set(password[i : i + repeat + 1])) > 1
850
-
851
-
852
-class TestConstraintSatisfactionFromUpstream004(TestVault):
853
-    """Test passphrase derivation with the "vault" scheme: upstream tests."""
854
-
855
-    def test_very_limited_character_set(self) -> None:
856
-        """Deriving a passphrase works even with limited character sets."""
857
-        generated = vault.Vault(
858
-            phrase=b"", length=24, lower=0, upper=0, space=0, symbol=0
859
-        ).generate("testing")
860
-        assert generated == b"763252593304946694588866"
861
-
862
-
863 825
 class TestUtilities(TestVault):
864 826
     """Test passphrase derivation with the "vault" scheme: utility tests."""
865 827
 
866 828