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 |