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 |