Marco Ricci commited on 2025-08-11 21:50:42
Zeige 6 geänderte Dateien mit 409 Einfügungen und 156 Löschungen.
Beyond the five main groups that reflect the original five test classes for the command-line interface, introduce further classes/groupings for related tests, and trim the test names appropriately. In particular, remove the ordinal in the test name until the new order (due to the new class names and trimmed test names) sufficiently settles. Do the same for the SSH agent tests. (This could have been done for all tests, but with diminishing returns.) The grouping currently (mostly) reflects thematic similarity, but in the long run, we intend for it to reflect both thematic similarity and common test environments, with respect to setup and teardown: tests of the same group should look similar to each other too, and common code among tests of the same group should be factored out if possible. The grouping is inserted, syntactically, between the existing test functions. In particular, to keep the diff readable, we do not relocate or reorder any test functions yet in this commit. We add some TODOs to indicate where further reordering or rewriting is necessary, beyond just introducing new groups. The intended (but not yet implemented) sharing of test setup and teardown code will probably entail using fixtures, pytest's preferred mechanism for shared setup and teardown. However, as of this writing, it is still an open question how to deal with `hypothesis`-based tests in this scenario: they do not work well together with per-function test fixtures, but we also do *not* want to have to (re-)implement all setup/teardown pairs once as a fixture and once as a context manager.
... | ... |
@@ -2,6 +2,8 @@ |
2 | 2 |
# |
3 | 3 |
# SPDX-License-Identifier: Zlib |
4 | 4 |
|
5 |
+"""Tests for the `derivepassphrase vault` command-line interface.""" |
|
6 |
+ |
|
5 | 7 |
from __future__ import annotations |
6 | 8 |
|
7 | 9 |
import contextlib |
... | ... |
@@ -671,10 +673,10 @@ class Parametrize(types.SimpleNamespace): |
671 | 673 |
) |
672 | 674 |
|
673 | 675 |
|
674 |
-class TestCLI: |
|
675 |
- """Tests for the `derivepassphrase vault` command-line interface.""" |
|
676 |
+class TestHelp: |
|
677 |
+ """Tests related to help output.""" |
|
676 | 678 |
|
677 |
- def test_200_help_output( |
|
679 |
+ def test_help_output( |
|
678 | 680 |
self, |
679 | 681 |
) -> None: |
680 | 682 |
"""The `--help` option emits help text.""" |
... | ... |
@@ -702,8 +704,12 @@ class TestCLI: |
702 | 704 |
empty_stderr=True, output="Use $VISUAL or $EDITOR to configure" |
703 | 705 |
), "expected clean exit, and option group epilog in help text" |
704 | 706 |
|
707 |
+ |
|
708 |
+class TestDerivedPassphraseConstraints: |
|
709 |
+ """Tests for (derived) passphrase constraints.""" |
|
710 |
+ |
|
705 | 711 |
@Parametrize.CHARSET_NAME |
706 |
- def test_201_disable_character_set( |
|
712 |
+ def test_disable_character_set( |
|
707 | 713 |
self, |
708 | 714 |
charset_name: str, |
709 | 715 |
) -> None: |
... | ... |
@@ -739,7 +745,7 @@ class TestCLI: |
739 | 745 |
f"derived password contains forbidden character {c!r}" |
740 | 746 |
) |
741 | 747 |
|
742 |
- def test_202_disable_repetition( |
|
748 |
+ def test_disable_repetition( |
|
743 | 749 |
self, |
744 | 750 |
) -> None: |
745 | 751 |
"""Character repetition can be disabled on the command-line.""" |
... | ... |
@@ -776,8 +782,40 @@ class TestCLI: |
776 | 782 |
f"at position {i}: {result.stdout!r}" |
777 | 783 |
) |
778 | 784 |
|
785 |
+ |
|
786 |
+class TestPhraseBasic: |
|
787 |
+ """Tests for master passphrase configuration: basic.""" |
|
788 |
+ |
|
789 |
+ @pytest.mark.xfail( |
|
790 |
+ True, |
|
791 |
+ reason='not implemented yet', |
|
792 |
+ raises=NotImplementedError, |
|
793 |
+ strict=True, |
|
794 |
+ ) |
|
795 |
+ def test_phrase_from_config( |
|
796 |
+ self, |
|
797 |
+ ) -> None: |
|
798 |
+ """A stored configured master passphrase will be used.""" |
|
799 |
+ raise NotImplementedError |
|
800 |
+ |
|
801 |
+ @pytest.mark.xfail( |
|
802 |
+ True, |
|
803 |
+ reason='not implemented yet', |
|
804 |
+ raises=NotImplementedError, |
|
805 |
+ strict=True, |
|
806 |
+ ) |
|
807 |
+ def test_phrase_from_command_line( |
|
808 |
+ self, |
|
809 |
+ ) -> None: |
|
810 |
+ """A master passphrase requested on the command-line will be used.""" |
|
811 |
+ raise NotImplementedError |
|
812 |
+ |
|
813 |
+ |
|
814 |
+class TestKeyBasic: |
|
815 |
+ """Tests for SSH key configuration: basic.""" |
|
816 |
+ |
|
779 | 817 |
@Parametrize.CONFIG_WITH_KEY |
780 |
- def test_204a_key_from_config( |
|
818 |
+ def test_key_from_config( |
|
781 | 819 |
self, |
782 | 820 |
running_ssh_agent: data.RunningSSHAgentInfo, |
783 | 821 |
config: _types.VaultConfig, |
... | ... |
@@ -819,7 +857,7 @@ class TestCLI: |
819 | 857 |
result.stdout.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1 |
820 | 858 |
), "expected known output" |
821 | 859 |
|
822 |
- def test_204b_key_from_command_line( |
|
860 |
+ def test_key_from_command_line( |
|
823 | 861 |
self, |
824 | 862 |
running_ssh_agent: data.RunningSSHAgentInfo, |
825 | 863 |
) -> None: |
... | ... |
@@ -866,9 +904,13 @@ class TestCLI: |
866 | 904 |
"expected known output" |
867 | 905 |
) |
868 | 906 |
|
907 |
+ |
|
908 |
+class TestPhraseAndKeyOverriding: |
|
909 |
+ """Tests for master passphrase and SSH key configuration: overriding.""" |
|
910 |
+ |
|
869 | 911 |
@Parametrize.BASE_CONFIG_WITH_KEY_VARIATIONS |
870 | 912 |
@Parametrize.KEY_INDEX |
871 |
- def test_204c_key_override_on_command_line( |
|
913 |
+ def test_key_override_on_command_line( |
|
872 | 914 |
self, |
873 | 915 |
running_ssh_agent: data.RunningSSHAgentInfo, |
874 | 916 |
config: dict[str, Any], |
... | ... |
@@ -909,7 +951,7 @@ class TestCLI: |
909 | 951 |
"expected no error messages on stderr" |
910 | 952 |
) |
911 | 953 |
|
912 |
- def test_205_service_phrase_if_key_in_global_config( |
|
954 |
+ def test_service_phrase_if_key_in_global_config( |
|
913 | 955 |
self, |
914 | 956 |
running_ssh_agent: data.RunningSSHAgentInfo, |
915 | 957 |
) -> None: |
... | ... |
@@ -960,7 +1002,7 @@ class TestCLI: |
960 | 1002 |
) |
961 | 1003 |
|
962 | 1004 |
@Parametrize.KEY_OVERRIDING_IN_CONFIG |
963 |
- def test_206_setting_phrase_thus_overriding_key_in_config( |
|
1005 |
+ def test_setting_phrase_thus_overriding_key_in_config( |
|
964 | 1006 |
self, |
965 | 1007 |
running_ssh_agent: data.RunningSSHAgentInfo, |
966 | 1008 |
caplog: pytest.LogCaptureFixture, |
... | ... |
@@ -1013,6 +1055,11 @@ class TestCLI: |
1013 | 1055 |
map(is_harmless_config_import_warning, caplog.record_tuples) |
1014 | 1056 |
), "unexpected error output" |
1015 | 1057 |
|
1058 |
+ |
|
1059 |
+# TODO(the-13th-letter): Assimilate into its descendant. |
|
1060 |
+class TestNotesPrinting000: |
|
1061 |
+ """Tests concerning printing the service notes: group 000.""" |
|
1062 |
+ |
|
1016 | 1063 |
@hypothesis.given( |
1017 | 1064 |
notes=strategies.text( |
1018 | 1065 |
strategies.characters( |
... | ... |
@@ -1069,8 +1116,13 @@ class TestCLI: |
1069 | 1116 |
"expected known stderr contents" |
1070 | 1117 |
) |
1071 | 1118 |
|
1119 |
+ |
|
1120 |
+# TODO(the-13th-letter): Assimilate the descendants. |
|
1121 |
+class TestInvalidCommandLines: |
|
1122 |
+ """Tests concerning invalid command-lines.""" |
|
1123 |
+ |
|
1072 | 1124 |
@Parametrize.VAULT_CHARSET_OPTION |
1073 |
- def test_210_invalid_argument_range( |
|
1125 |
+ def test_invalid_argument_range( |
|
1074 | 1126 |
self, |
1075 | 1127 |
option: str, |
1076 | 1128 |
) -> None: |
... | ... |
@@ -1099,7 +1151,7 @@ class TestCLI: |
1099 | 1151 |
) |
1100 | 1152 |
|
1101 | 1153 |
@Parametrize.OPTION_COMBINATIONS_SERVICE_NEEDED |
1102 |
- def test_211_service_needed( |
|
1154 |
+ def test_service_needed( |
|
1103 | 1155 |
self, |
1104 | 1156 |
options: list[str], |
1105 | 1157 |
service: bool | None, |
... | ... |
@@ -1173,7 +1225,7 @@ class TestCLI: |
1173 | 1225 |
) |
1174 | 1226 |
assert result.clean_exit(empty_stderr=True), "expected clean exit" |
1175 | 1227 |
|
1176 |
- def test_211a_empty_service_name_causes_warning( |
|
1228 |
+ def test_empty_service_name_causes_warning( |
|
1177 | 1229 |
self, |
1178 | 1230 |
caplog: pytest.LogCaptureFixture, |
1179 | 1231 |
) -> None: |
... | ... |
@@ -1240,7 +1292,7 @@ class TestCLI: |
1240 | 1292 |
}, "requested configuration change was not applied" |
1241 | 1293 |
|
1242 | 1294 |
@Parametrize.OPTION_COMBINATIONS_INCOMPATIBLE |
1243 |
- def test_212_incompatible_options( |
|
1295 |
+ def test_incompatible_options( |
|
1244 | 1296 |
self, |
1245 | 1297 |
options: list[str], |
1246 | 1298 |
service: bool | None, |
... | ... |
@@ -1268,8 +1320,12 @@ class TestCLI: |
1268 | 1320 |
"expected error exit and known error message" |
1269 | 1321 |
) |
1270 | 1322 |
|
1323 |
+ |
|
1324 |
+class TestImportConfigValid: |
|
1325 |
+ """Tests concerning `vault` configuration imports: valid imports.""" |
|
1326 |
+ |
|
1271 | 1327 |
@Parametrize.VALID_TEST_CONFIGS |
1272 |
- def test_213_import_config_success( |
|
1328 |
+ def test_import_config( |
|
1273 | 1329 |
self, |
1274 | 1330 |
caplog: pytest.LogCaptureFixture, |
1275 | 1331 |
config: Any, |
... | ... |
@@ -1318,7 +1374,7 @@ class TestCLI: |
1318 | 1374 |
]) |
1319 | 1375 |
) |
1320 | 1376 |
) |
1321 |
- def test_213a_import_config_success( |
|
1377 |
+ def test_import_smudged_config( |
|
1322 | 1378 |
self, |
1323 | 1379 |
caplog: pytest.LogCaptureFixture, |
1324 | 1380 |
conf: data.VaultTestConfig, |
... | ... |
@@ -1363,7 +1419,11 @@ class TestCLI: |
1363 | 1419 |
), "unexpected error output" |
1364 | 1420 |
assert_vault_config_is_indented_and_line_broken(config_txt) |
1365 | 1421 |
|
1366 |
- def test_213b_import_bad_config_not_vault_config( |
|
1422 |
+ |
|
1423 |
+class TestImportConfigInvalid: |
|
1424 |
+ """Tests concerning `vault` configuration imports: invalid imports.""" |
|
1425 |
+ |
|
1426 |
+ def test_import_config_not_a_vault_config( |
|
1367 | 1427 |
self, |
1368 | 1428 |
) -> None: |
1369 | 1429 |
"""Importing an invalid config fails.""" |
... | ... |
@@ -1389,7 +1449,7 @@ class TestCLI: |
1389 | 1449 |
"expected error exit and known error message" |
1390 | 1450 |
) |
1391 | 1451 |
|
1392 |
- def test_213c_import_bad_config_not_json_data( |
|
1452 |
+ def test_import_config_not_json_data( |
|
1393 | 1453 |
self, |
1394 | 1454 |
) -> None: |
1395 | 1455 |
"""Importing an invalid config fails.""" |
... | ... |
@@ -1415,7 +1475,7 @@ class TestCLI: |
1415 | 1475 |
"expected error exit and known error message" |
1416 | 1476 |
) |
1417 | 1477 |
|
1418 |
- def test_213d_import_bad_config_not_a_file( |
|
1478 |
+ def test_import_config_not_a_file( |
|
1419 | 1479 |
self, |
1420 | 1480 |
) -> None: |
1421 | 1481 |
"""Importing an invalid config fails.""" |
... | ... |
@@ -1453,8 +1513,12 @@ class TestCLI: |
1453 | 1513 |
"expected error exit and known error message" |
1454 | 1514 |
) |
1455 | 1515 |
|
1516 |
+ |
|
1517 |
+class TestExportConfigValid: |
|
1518 |
+ """Tests concerning `vault` configuration exports: valid exports.""" |
|
1519 |
+ |
|
1456 | 1520 |
@Parametrize.VALID_TEST_CONFIGS |
1457 |
- def test_214_export_config_success( |
|
1521 |
+ def test_export_config_success( |
|
1458 | 1522 |
self, |
1459 | 1523 |
caplog: pytest.LogCaptureFixture, |
1460 | 1524 |
config: Any, |
... | ... |
@@ -1494,8 +1558,20 @@ class TestCLI: |
1494 | 1558 |
), "unexpected error output" |
1495 | 1559 |
assert_vault_config_is_indented_and_line_broken(result.stdout) |
1496 | 1560 |
|
1561 |
+ @pytest.mark.xfail( |
|
1562 |
+ True, |
|
1563 |
+ reason='not implemented yet', |
|
1564 |
+ raises=NotImplementedError, |
|
1565 |
+ strict=True, |
|
1566 |
+ ) |
|
1567 |
+ def test_export_smudged_config( |
|
1568 |
+ self, |
|
1569 |
+ ) -> None: |
|
1570 |
+ """Exporting a smudged configuration works.""" |
|
1571 |
+ raise NotImplementedError |
|
1572 |
+ |
|
1497 | 1573 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
1498 |
- def test_214a_export_settings_no_stored_settings( |
|
1574 |
+ def test_export_config_no_stored_settings( |
|
1499 | 1575 |
self, |
1500 | 1576 |
export_options: list[str], |
1501 | 1577 |
) -> None: |
... | ... |
@@ -1526,8 +1602,12 @@ class TestCLI: |
1526 | 1602 |
) |
1527 | 1603 |
assert result.clean_exit(empty_stderr=True), "expected clean exit" |
1528 | 1604 |
|
1605 |
+ |
|
1606 |
+class TestExportConfigInvalid: |
|
1607 |
+ """Tests concerning `vault` configuration exports: invalid exports.""" |
|
1608 |
+ |
|
1529 | 1609 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
1530 |
- def test_214b_export_settings_bad_stored_config( |
|
1610 |
+ def test_export_config_bad_stored_config( |
|
1531 | 1611 |
self, |
1532 | 1612 |
export_options: list[str], |
1533 | 1613 |
) -> None: |
... | ... |
@@ -1556,7 +1636,7 @@ class TestCLI: |
1556 | 1636 |
) |
1557 | 1637 |
|
1558 | 1638 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
1559 |
- def test_214c_export_settings_not_a_file( |
|
1639 |
+ def test_export_config_not_a_file( |
|
1560 | 1640 |
self, |
1561 | 1641 |
export_options: list[str], |
1562 | 1642 |
) -> None: |
... | ... |
@@ -1587,7 +1667,7 @@ class TestCLI: |
1587 | 1667 |
) |
1588 | 1668 |
|
1589 | 1669 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
1590 |
- def test_214d_export_settings_target_not_a_file( |
|
1670 |
+ def test_export_config_target_not_a_file( |
|
1591 | 1671 |
self, |
1592 | 1672 |
export_options: list[str], |
1593 | 1673 |
) -> None: |
... | ... |
@@ -1617,7 +1697,7 @@ class TestCLI: |
1617 | 1697 |
|
1618 | 1698 |
@pytest_machinery.skip_if_on_the_annoying_os |
1619 | 1699 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
1620 |
- def test_214e_export_settings_settings_directory_not_a_directory( |
|
1700 |
+ def test_export_config_settings_directory_not_a_directory( |
|
1621 | 1701 |
self, |
1622 | 1702 |
export_options: list[str], |
1623 | 1703 |
) -> None: |
... | ... |
@@ -1650,6 +1730,11 @@ class TestCLI: |
1650 | 1730 |
"expected error exit and known error message" |
1651 | 1731 |
) |
1652 | 1732 |
|
1733 |
+ |
|
1734 |
+# TODO(the-13th-letter): Assimilate the parent. |
|
1735 |
+class TestNotesPrinting001: |
|
1736 |
+ """Tests concerning printing the service notes: group 001.""" |
|
1737 |
+ |
|
1653 | 1738 |
@Parametrize.NOTES_PLACEMENT |
1654 | 1739 |
@hypothesis.given( |
1655 | 1740 |
notes=strategies.text( |
... | ... |
@@ -1660,7 +1745,7 @@ class TestCLI: |
1660 | 1745 |
max_size=512, |
1661 | 1746 |
).filter(str.strip), |
1662 | 1747 |
) |
1663 |
- def test_215_notes_placement( |
|
1748 |
+ def test_notes_placement( |
|
1664 | 1749 |
self, |
1665 | 1750 |
notes_placement: Literal["before", "after"], |
1666 | 1751 |
placement_args: list[str], |
... | ... |
@@ -1700,6 +1785,10 @@ class TestCLI: |
1700 | 1785 |
) |
1701 | 1786 |
assert result.clean_exit(output=expected), "expected clean exit" |
1702 | 1787 |
|
1788 |
+ |
|
1789 |
+class TestNotesEditingValid: |
|
1790 |
+ """Tests concerning editing service notes: valid calls.""" |
|
1791 |
+ |
|
1703 | 1792 |
@Parametrize.MODERN_EDITOR_INTERFACE |
1704 | 1793 |
@hypothesis.settings( |
1705 | 1794 |
suppress_health_check=[ |
... | ... |
@@ -1716,7 +1805,7 @@ class TestCLI: |
1716 | 1805 |
max_size=512, |
1717 | 1806 |
).filter(str.strip), |
1718 | 1807 |
) |
1719 |
- def test_220_edit_notes_successfully( |
|
1808 |
+ def test_successful_edit( |
|
1720 | 1809 |
self, |
1721 | 1810 |
caplog: pytest.LogCaptureFixture, |
1722 | 1811 |
modern_editor_interface: bool, |
... | ... |
@@ -1806,7 +1895,7 @@ class TestCLI: |
1806 | 1895 |
max_size=512, |
1807 | 1896 |
).filter(str.strip), |
1808 | 1897 |
) |
1809 |
- def test_221_edit_notes_noop( |
|
1898 |
+ def test_noop_edit( |
|
1810 | 1899 |
self, |
1811 | 1900 |
edit_func_name: Literal["empty", "space"], |
1812 | 1901 |
modern_editor_interface: bool, |
... | ... |
@@ -1895,7 +1984,7 @@ class TestCLI: |
1895 | 1984 |
max_size=512, |
1896 | 1985 |
).filter(str.strip), |
1897 | 1986 |
) |
1898 |
- def test_222_edit_notes_marker_removed( |
|
1987 |
+ def test_marker_removed( |
|
1899 | 1988 |
self, |
1900 | 1989 |
caplog: pytest.LogCaptureFixture, |
1901 | 1990 |
modern_editor_interface: bool, |
... | ... |
@@ -1971,6 +2060,10 @@ class TestCLI: |
1971 | 2060 |
"services": {"sv": {"notes": notes.strip()}}, |
1972 | 2061 |
} |
1973 | 2062 |
|
2063 |
+ |
|
2064 |
+class TestNotesEditingInvalid: |
|
2065 |
+ """Tests concerning editing service notes: invalid/error calls.""" |
|
2066 |
+ |
|
1974 | 2067 |
@hypothesis.given( |
1975 | 2068 |
notes=strategies.text( |
1976 | 2069 |
strategies.characters( |
... | ... |
@@ -1980,7 +2073,7 @@ class TestCLI: |
1980 | 2073 |
max_size=512, |
1981 | 2074 |
).filter(str.strip), |
1982 | 2075 |
) |
1983 |
- def test_223_edit_notes_abort( |
|
2076 |
+ def test_abort( |
|
1984 | 2077 |
self, |
1985 | 2078 |
notes: str, |
1986 | 2079 |
) -> None: |
... | ... |
@@ -2029,7 +2122,7 @@ class TestCLI: |
2029 | 2122 |
"services": {"sv": {"notes": notes.strip()}}, |
2030 | 2123 |
} |
2031 | 2124 |
|
2032 |
- def test_223a_edit_empty_notes_abort( |
|
2125 |
+ def test_abort_no_prior_notes( |
|
2033 | 2126 |
self, |
2034 | 2127 |
) -> None: |
2035 | 2128 |
"""Aborting editing notes works even if no notes are stored yet. |
... | ... |
@@ -2092,7 +2185,7 @@ class TestCLI: |
2092 | 2185 |
max_size=512, |
2093 | 2186 |
), |
2094 | 2187 |
) |
2095 |
- def test_223b_edit_notes_fail_config_option_missing( |
|
2188 |
+ def test_fail_on_config_option_missing( |
|
2096 | 2189 |
self, |
2097 | 2190 |
caplog: pytest.LogCaptureFixture, |
2098 | 2191 |
modern_editor_interface: bool, |
... | ... |
@@ -2172,8 +2265,13 @@ class TestCLI: |
2172 | 2265 |
config = json.load(infile) |
2173 | 2266 |
assert config == vault_config |
2174 | 2267 |
|
2268 |
+ |
|
2269 |
+# TODO(the-13th-letter): Assimilate the descendants. |
|
2270 |
+class TestStoringConfigurationSuccesses: |
|
2271 |
+ """Tests concerning storing the configuration: successes.""" |
|
2272 |
+ |
|
2175 | 2273 |
@Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG |
2176 |
- def test_224_store_config_good( |
|
2274 |
+ def test_store_good_config( |
|
2177 | 2275 |
self, |
2178 | 2276 |
command_line: list[str], |
2179 | 2277 |
input: str, |
... | ... |
@@ -2219,8 +2317,13 @@ class TestCLI: |
2219 | 2317 |
) |
2220 | 2318 |
assert_vault_config_is_indented_and_line_broken(config_txt) |
2221 | 2319 |
|
2320 |
+ |
|
2321 |
+# TODO(the-13th-letter): Assimilate the descendants. |
|
2322 |
+class TestStoringConfigurationFailures: |
|
2323 |
+ """Tests concerning storing the configuration: failures.""" |
|
2324 |
+ |
|
2222 | 2325 |
@Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES |
2223 |
- def test_225_store_config_fail( |
|
2326 |
+ def test_store_bad_config( |
|
2224 | 2327 |
self, |
2225 | 2328 |
command_line: list[str], |
2226 | 2329 |
input: str, |
... | ... |
@@ -2255,7 +2358,7 @@ class TestCLI: |
2255 | 2358 |
"expected error exit and known error message" |
2256 | 2359 |
) |
2257 | 2360 |
|
2258 |
- def test_225a_store_config_fail_manual_no_ssh_key_selection( |
|
2361 |
+ def test_fail_because_no_ssh_key_selection( |
|
2259 | 2362 |
self, |
2260 | 2363 |
running_ssh_agent: data.RunningSSHAgentInfo, |
2261 | 2364 |
) -> None: |
... | ... |
@@ -2297,7 +2400,7 @@ class TestCLI: |
2297 | 2400 |
"expected error exit and known error message" |
2298 | 2401 |
) |
2299 | 2402 |
|
2300 |
- def test_225b_store_config_fail_manual_no_ssh_agent( |
|
2403 |
+ def test_fail_because_no_ssh_agent( |
|
2301 | 2404 |
self, |
2302 | 2405 |
running_ssh_agent: data.RunningSSHAgentInfo, |
2303 | 2406 |
) -> None: |
... | ... |
@@ -2326,7 +2429,7 @@ class TestCLI: |
2326 | 2429 |
"expected error exit and known error message" |
2327 | 2430 |
) |
2328 | 2431 |
|
2329 |
- def test_225c_store_config_fail_manual_bad_ssh_agent_connection( |
|
2432 |
+ def test_fail_because_bad_ssh_agent_connection( |
|
2330 | 2433 |
self, |
2331 | 2434 |
running_ssh_agent: data.RunningSSHAgentInfo, |
2332 | 2435 |
) -> None: |
... | ... |
@@ -2357,7 +2460,7 @@ class TestCLI: |
2357 | 2460 |
) |
2358 | 2461 |
|
2359 | 2462 |
@Parametrize.TRY_RACE_FREE_IMPLEMENTATION |
2360 |
- def test_225d_store_config_fail_manual_read_only_file( |
|
2463 |
+ def test_fail_because_read_only_file( |
|
2361 | 2464 |
self, |
2362 | 2465 |
try_race_free_implementation: bool, |
2363 | 2466 |
) -> None: |
... | ... |
@@ -2388,10 +2491,10 @@ class TestCLI: |
2388 | 2491 |
"expected error exit and known error message" |
2389 | 2492 |
) |
2390 | 2493 |
|
2391 |
- def test_225e_store_config_fail_manual_custom_error( |
|
2494 |
+ def test_fail_because_of_custom_error( |
|
2392 | 2495 |
self, |
2393 | 2496 |
) -> None: |
2394 |
- """OS-erroring with `--config` fails.""" |
|
2497 |
+ """Triggering internal errors during `--config` leads to failure.""" |
|
2395 | 2498 |
runner = machinery.CliRunner(mix_stderr=False) |
2396 | 2499 |
# TODO(the-13th-letter): Rewrite using parenthesized |
2397 | 2500 |
# with-statements. |
... | ... |
@@ -2421,7 +2524,7 @@ class TestCLI: |
2421 | 2524 |
"expected error exit and known error message" |
2422 | 2525 |
) |
2423 | 2526 |
|
2424 |
- def test_225f_store_config_fail_unset_and_set_same_settings( |
|
2527 |
+ def test_fail_because_unsetting_and_setting_same_settings( |
|
2425 | 2528 |
self, |
2426 | 2529 |
) -> None: |
2427 | 2530 |
"""Issuing conflicting settings to `--config` fails.""" |
... | ... |
@@ -2453,7 +2556,7 @@ class TestCLI: |
2453 | 2556 |
error="Attempted to unset and set --length at the same time." |
2454 | 2557 |
), "expected error exit and known error message" |
2455 | 2558 |
|
2456 |
- def test_225g_store_config_fail_manual_ssh_agent_no_keys_loaded( |
|
2559 |
+ def test_fail_because_ssh_agent_has_no_keys_loaded( |
|
2457 | 2560 |
self, |
2458 | 2561 |
running_ssh_agent: data.RunningSSHAgentInfo, |
2459 | 2562 |
) -> None: |
... | ... |
@@ -2489,11 +2592,11 @@ class TestCLI: |
2489 | 2592 |
"expected error exit and known error message" |
2490 | 2593 |
) |
2491 | 2594 |
|
2492 |
- def test_225h_store_config_fail_manual_ssh_agent_runtime_error( |
|
2595 |
+ def test_store_config_fail_manual_ssh_agent_runtime_error( |
|
2493 | 2596 |
self, |
2494 | 2597 |
running_ssh_agent: data.RunningSSHAgentInfo, |
2495 | 2598 |
) -> None: |
2496 |
- """The SSH agent erroring during `--config --key` fails.""" |
|
2599 |
+ """Triggering an error in the SSH agent during `--config --key` leads to failure.""" |
|
2497 | 2600 |
del running_ssh_agent |
2498 | 2601 |
runner = machinery.CliRunner(mix_stderr=False) |
2499 | 2602 |
# TODO(the-13th-letter): Rewrite using parenthesized |
... | ... |
@@ -2522,11 +2625,11 @@ class TestCLI: |
2522 | 2625 |
error="violates the communication protocol." |
2523 | 2626 |
), "expected error exit and known error message" |
2524 | 2627 |
|
2525 |
- def test_225i_store_config_fail_manual_ssh_agent_refuses( |
|
2628 |
+ def test_store_config_fail_manual_ssh_agent_refuses( |
|
2526 | 2629 |
self, |
2527 | 2630 |
running_ssh_agent: data.RunningSSHAgentInfo, |
2528 | 2631 |
) -> None: |
2529 |
- """The SSH agent refusing during `--config --key` fails.""" |
|
2632 |
+ """The SSH agent refusing during `--config --key` leads to failure.""" |
|
2530 | 2633 |
del running_ssh_agent |
2531 | 2634 |
runner = machinery.CliRunner(mix_stderr=False) |
2532 | 2635 |
# TODO(the-13th-letter): Rewrite using parenthesized |
... | ... |
@@ -2557,7 +2660,12 @@ class TestCLI: |
2557 | 2660 |
"expected error exit and known error message" |
2558 | 2661 |
) |
2559 | 2662 |
|
2560 |
- def test_226_no_arguments(self) -> None: |
|
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: |
|
2561 | 2669 |
"""Calling `derivepassphrase vault` without any arguments fails.""" |
2562 | 2670 |
runner = machinery.CliRunner(mix_stderr=False) |
2563 | 2671 |
# TODO(the-13th-letter): Rewrite using parenthesized |
... | ... |
@@ -2578,7 +2686,7 @@ class TestCLI: |
2578 | 2686 |
error="Deriving a passphrase requires a SERVICE" |
2579 | 2687 |
), "expected error exit and known error message" |
2580 | 2688 |
|
2581 |
- def test_226a_no_passphrase_or_key( |
|
2689 |
+ def test_no_passphrase_or_key( |
|
2582 | 2690 |
self, |
2583 | 2691 |
) -> None: |
2584 | 2692 |
"""Deriving a passphrase without a passphrase or key fails.""" |
... | ... |
@@ -2603,12 +2711,20 @@ class TestCLI: |
2603 | 2711 |
"expected error exit and known error message" |
2604 | 2712 |
) |
2605 | 2713 |
|
2606 |
- def test_230_config_directory_nonexistant( |
|
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( |
|
2607 | 2720 |
self, |
2608 | 2721 |
) -> None: |
2609 | 2722 |
"""Running without an existing config directory works. |
2610 | 2723 |
|
2611 | 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. |
|
2612 | 2728 |
|
2613 | 2729 |
[issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6 |
2614 | 2730 |
|
... | ... |
@@ -2646,7 +2762,12 @@ class TestCLI: |
2646 | 2762 |
"services": {}, |
2647 | 2763 |
}, "config mismatch" |
2648 | 2764 |
|
2649 |
- def test_230a_config_directory_not_a_file( |
|
2765 |
+ |
|
2766 |
+# TODO(the-13th-letter): Assimilate into the parent. |
|
2767 |
+class TestStoringConfigurationFailures001: |
|
2768 |
+ """Tests concerning storing the configuration: failures, group 001.""" |
|
2769 |
+ |
|
2770 |
+ def test_config_directory_not_a_file( |
|
2650 | 2771 |
self, |
2651 | 2772 |
) -> None: |
2652 | 2773 |
"""Erroring without an existing config directory errors normally. |
... | ... |
@@ -2655,6 +2776,9 @@ class TestCLI: |
2655 | 2776 |
errors by itself. |
2656 | 2777 |
|
2657 | 2778 |
This is a regression test; see [issue\u00a0#6][] for context. |
2779 |
+ See also |
|
2780 |
+ [TestStoringConfigurationSuccesses001.test_config_directory_nonexistant][] |
|
2781 |
+ for a related aspect of this. |
|
2658 | 2782 |
|
2659 | 2783 |
[issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6 |
2660 | 2784 |
|
... | ... |
@@ -2694,7 +2818,10 @@ class TestCLI: |
2694 | 2818 |
"expected error exit and known error message" |
2695 | 2819 |
) |
2696 | 2820 |
|
2697 |
- def test_230b_store_config_custom_error( |
|
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( |
|
2698 | 2825 |
self, |
2699 | 2826 |
) -> None: |
2700 | 2827 |
"""Storing the configuration reacts even to weird errors.""" |
... | ... |
@@ -2727,8 +2854,12 @@ class TestCLI: |
2727 | 2854 |
"expected error exit and known error message" |
2728 | 2855 |
) |
2729 | 2856 |
|
2857 |
+ |
|
2858 |
+class TestPassphraseUnicodeNormalization: |
|
2859 |
+ """Tests concerning the Unicode normalization of passphrases.""" |
|
2860 |
+ |
|
2730 | 2861 |
@Parametrize.UNICODE_NORMALIZATION_WARNING_INPUTS |
2731 |
- def test_300_unicode_normalization_form_warning( |
|
2862 |
+ def test_warning( |
|
2732 | 2863 |
self, |
2733 | 2864 |
caplog: pytest.LogCaptureFixture, |
2734 | 2865 |
main_config: str, |
... | ... |
@@ -2767,7 +2898,7 @@ class TestCLI: |
2767 | 2898 |
), "expected known warning message in stderr" |
2768 | 2899 |
|
2769 | 2900 |
@Parametrize.UNICODE_NORMALIZATION_ERROR_INPUTS |
2770 |
- def test_301_unicode_normalization_form_error( |
|
2901 |
+ def test_error( |
|
2771 | 2902 |
self, |
2772 | 2903 |
main_config: str, |
2773 | 2904 |
command_line: list[str], |
... | ... |
@@ -2807,7 +2938,7 @@ class TestCLI: |
2807 | 2938 |
) |
2808 | 2939 |
|
2809 | 2940 |
@Parametrize.UNICODE_NORMALIZATION_COMMAND_LINES |
2810 |
- def test_301a_unicode_normalization_form_error_from_stored_config( |
|
2941 |
+ def test_error_from_stored_config( |
|
2811 | 2942 |
self, |
2812 | 2943 |
command_line: list[str], |
2813 | 2944 |
) -> None: |
... | ... |
@@ -2848,7 +2979,11 @@ class TestCLI: |
2848 | 2979 |
), |
2849 | 2980 |
), "expected error exit and known error message" |
2850 | 2981 |
|
2851 |
- def test_310_bad_user_config_file( |
|
2982 |
+ |
|
2983 |
+class TestUserConfigurationFileOther: |
|
2984 |
+ """Other tests concerning the user configuration file.""" |
|
2985 |
+ |
|
2986 |
+ def test_bad_user_config_file( |
|
2852 | 2987 |
self, |
2853 | 2988 |
) -> None: |
2854 | 2989 |
"""Loading a user configuration file in an invalid format fails.""" |
... | ... |
@@ -2876,10 +3011,10 @@ class TestCLI: |
2876 | 3011 |
"expected error exit and known error message" |
2877 | 3012 |
) |
2878 | 3013 |
|
2879 |
- def test_311_bad_user_config_is_a_directory( |
|
3014 |
+ def test_user_config_is_a_directory( |
|
2880 | 3015 |
self, |
2881 | 3016 |
) -> None: |
2882 |
- """Loading a user configuration file in an invalid format fails.""" |
|
3017 |
+ """Loading a user configuration non-file fails.""" |
|
2883 | 3018 |
runner = machinery.CliRunner(mix_stderr=False) |
2884 | 3019 |
# TODO(the-13th-letter): Rewrite using parenthesized |
2885 | 3020 |
# with-statements. |
... | ... |
@@ -2909,7 +3044,11 @@ class TestCLI: |
2909 | 3044 |
"expected error exit and known error message" |
2910 | 3045 |
) |
2911 | 3046 |
|
2912 |
- def test_400_missing_af_unix_support( |
|
3047 |
+ |
|
3048 |
+class TestSSHAgentAvailability: |
|
3049 |
+ """Tests concerning the availability of the SSH agent.""" |
|
3050 |
+ |
|
3051 |
+ def test_missing_af_unix_support( |
|
2913 | 3052 |
self, |
2914 | 3053 |
caplog: pytest.LogCaptureFixture, |
2915 | 3054 |
) -> None: |
... | ... |
@@ -349,13 +349,13 @@ def parse_version_output( # noqa: C901 |
349 | 349 |
) |
350 | 350 |
|
351 | 351 |
|
352 |
-class TestAllCLI: |
|
353 |
- """Tests uniformly for all command-line interfaces.""" |
|
352 |
+class Test001VersionOutputParser: |
|
353 |
+ """Tests for the `--version` output parser.""" |
|
354 | 354 |
|
355 | 355 |
@Parametrize.MASK_PROG_NAME |
356 | 356 |
@Parametrize.MASK_VERSION |
357 | 357 |
@Parametrize.VERSION_OUTPUT_DATA |
358 |
- def test_001_parse_version_output( |
|
358 |
+ def test_parse_version_output( |
|
359 | 359 |
self, |
360 | 360 |
version_output: str, |
361 | 361 |
prog_name: str | None, |
... | ... |
@@ -374,6 +374,10 @@ class TestAllCLI: |
374 | 374 |
== expected_parse |
375 | 375 |
) |
376 | 376 |
|
377 |
+ |
|
378 |
+class TestHelpOutput: |
|
379 |
+ """Tests for all command-line interfaces' `--help` output.""" |
|
380 |
+ |
|
377 | 381 |
# TODO(the-13th-letter): Do we actually need this? What should we |
378 | 382 |
# check for? |
379 | 383 |
def test_100_help_output(self) -> None: |
... | ... |
@@ -569,7 +573,11 @@ class TestAllCLI: |
569 | 573 |
"Expected no color, but found an ANSI control sequence" |
570 | 574 |
) |
571 | 575 |
|
572 |
- def test_202a_derivepassphrase_version_option_output( |
|
576 |
+ |
|
577 |
+class TestVersionOutput: |
|
578 |
+ """Tests for all command-line interfaces' `--version` output.""" |
|
579 |
+ |
|
580 |
+ def test_derivepassphrase_version_option_output( |
|
573 | 581 |
self, |
574 | 582 |
) -> None: |
575 | 583 |
"""The version output states supported features. |
... | ... |
@@ -614,7 +622,7 @@ class TestAllCLI: |
614 | 622 |
assert not version_data.features |
615 | 623 |
assert not version_data.extras |
616 | 624 |
|
617 |
- def test_202b_export_version_option_output( |
|
625 |
+ def test_export_version_option_output( |
|
618 | 626 |
self, |
619 | 627 |
) -> None: |
620 | 628 |
"""The version output states supported features. |
... | ... |
@@ -666,7 +674,7 @@ class TestAllCLI: |
666 | 674 |
assert not version_data.features |
667 | 675 |
assert not version_data.extras |
668 | 676 |
|
669 |
- def test_202c_export_vault_version_option_output( |
|
677 |
+ def test_export_vault_version_option_output( |
|
670 | 678 |
self, |
671 | 679 |
) -> None: |
672 | 680 |
"""The version output states supported features. |
... | ... |
@@ -726,7 +734,7 @@ class TestAllCLI: |
726 | 734 |
assert not version_data.features |
727 | 735 |
assert version_data.extras == actually_enabled_extras |
728 | 736 |
|
729 |
- def test_202d_vault_version_option_output( |
|
737 |
+ def test_vault_version_option_output( |
|
730 | 738 |
self, |
731 | 739 |
) -> None: |
732 | 740 |
"""The version output states supported features. |
... | ... |
@@ -589,8 +589,12 @@ class TestShellCompletion: |
589 | 589 |
"""Return the completion items' values, as a sequence.""" |
590 | 590 |
return tuple(c.value for c in self()) |
591 | 591 |
|
592 |
+ |
|
593 |
+class TestCompletableItems: |
|
594 |
+ """Tests for completablility of items.""" |
|
595 |
+ |
|
592 | 596 |
@Parametrize.COMPLETABLE_ITEMS |
593 |
- def test_100_is_completable_item( |
|
597 |
+ def test_is_completable_item( |
|
594 | 598 |
self, |
595 | 599 |
partial: str, |
596 | 600 |
is_completable: bool, |
... | ... |
@@ -598,8 +602,12 @@ class TestShellCompletion: |
598 | 602 |
"""Our `_is_completable_item` predicate for service names works.""" |
599 | 603 |
assert cli_helpers.is_completable_item(partial) == is_completable |
600 | 604 |
|
605 |
+ |
|
606 |
+class TestCompletableItemClasses(TestShellCompletion): |
|
607 |
+ """Tests for the different classes of completable items.""" |
|
608 |
+ |
|
601 | 609 |
@Parametrize.COMPLETABLE_OPTIONS |
602 |
- def test_200_options( |
|
610 |
+ def test_options( |
|
603 | 611 |
self, |
604 | 612 |
command_prefix: Sequence[str], |
605 | 613 |
incomplete: str, |
... | ... |
@@ -610,7 +618,7 @@ class TestShellCompletion: |
610 | 618 |
assert frozenset(comp.get_words()) == completions |
611 | 619 |
|
612 | 620 |
@Parametrize.COMPLETABLE_SUBCOMMANDS |
613 |
- def test_201_subcommands( |
|
621 |
+ def test_subcommands( |
|
614 | 622 |
self, |
615 | 623 |
command_prefix: Sequence[str], |
616 | 624 |
incomplete: str, |
... | ... |
@@ -622,7 +630,7 @@ class TestShellCompletion: |
622 | 630 |
|
623 | 631 |
@Parametrize.COMPLETABLE_PATH_ARGUMENT |
624 | 632 |
@Parametrize.INCOMPLETE |
625 |
- def test_202_paths( |
|
633 |
+ def test_paths( |
|
626 | 634 |
self, |
627 | 635 |
command_prefix: Sequence[str], |
628 | 636 |
incomplete: str, |
... | ... |
@@ -636,7 +644,7 @@ class TestShellCompletion: |
636 | 644 |
) |
637 | 645 |
|
638 | 646 |
@Parametrize.COMPLETABLE_SERVICE_NAMES |
639 |
- def test_203_service_names( |
|
647 |
+ def test_service_names( |
|
640 | 648 |
self, |
641 | 649 |
config: _types.VaultConfig, |
642 | 650 |
incomplete: str, |
... | ... |
@@ -659,9 +667,13 @@ class TestShellCompletion: |
659 | 667 |
comp = self.Completions(["vault"], incomplete) |
660 | 668 |
assert frozenset(comp.get_words()) == completions |
661 | 669 |
|
670 |
+ |
|
671 |
+class TestCompletionFormatting: |
|
672 |
+ """Tests for the formatting of completable items.""" |
|
673 |
+ |
|
662 | 674 |
@Parametrize.SHELL_FORMATTER |
663 | 675 |
@Parametrize.COMPLETION_FUNCTION_INPUTS |
664 |
- def test_300_shell_completion_formatting( |
|
676 |
+ def test_shell_completion_formatting( |
|
665 | 677 |
self, |
666 | 678 |
shell: str, |
667 | 679 |
format_func: Callable[[click.shell_completion.CompletionItem], str], |
... | ... |
@@ -724,9 +736,13 @@ class TestShellCompletion: |
724 | 736 |
assert actual_items == expected_items |
725 | 737 |
assert actual_string == expected_string |
726 | 738 |
|
739 |
+ |
|
740 |
+class TestCLICompletabilityHandling(TestShellCompletion): |
|
741 |
+ """Tests for how the command-line interface handles completability.""" |
|
742 |
+ |
|
727 | 743 |
@Parametrize.CONFIG_SETTING_MODE |
728 | 744 |
@Parametrize.SERVICE_NAME_COMPLETION_INPUTS |
729 |
- def test_400_incompletable_service_names( |
|
745 |
+ def test_cli_warns_and_completion_skips_incompletable_service_names( |
|
730 | 746 |
self, |
731 | 747 |
caplog: pytest.LogCaptureFixture, |
732 | 748 |
mode: Literal["config", "import"], |
... | ... |
@@ -774,7 +790,7 @@ class TestShellCompletion: |
774 | 790 |
comp = self.Completions(["vault"], incomplete) |
775 | 791 |
assert frozenset(comp.get_words()) == completions |
776 | 792 |
|
777 |
- def test_410a_service_name_exceptions_not_found( |
|
793 |
+ def test_handling_nonexistant_service_names( |
|
778 | 794 |
self, |
779 | 795 |
) -> None: |
780 | 796 |
"""Service name completion quietly fails on missing configuration.""" |
... | ... |
@@ -803,7 +819,7 @@ class TestShellCompletion: |
803 | 819 |
) |
804 | 820 |
|
805 | 821 |
@Parametrize.SERVICE_NAME_EXCEPTIONS |
806 |
- def test_410b_service_name_exceptions_custom_error( |
|
822 |
+ def test_handling_unexpected_exceptions( |
|
807 | 823 |
self, |
808 | 824 |
exc_type: type[Exception], |
809 | 825 |
) -> None: |
... | ... |
@@ -56,11 +56,12 @@ class Parametrize(test_000_basic.Parametrize, test_utils.Parametrize): |
56 | 56 |
) |
57 | 57 |
|
58 | 58 |
|
59 |
-class TestCLITransition: |
|
60 |
- """Transition tests for the command-line interface up to v1.0.""" |
|
59 |
+# TODO(the-13th-letter): Assimilate the descendants. |
|
60 |
+class TestConfigMigrationMachinery: |
|
61 |
+ """Tests for the configuration file migration machinery.""" |
|
61 | 62 |
|
62 | 63 |
@Parametrize.BASE_CONFIG_VARIATIONS |
63 |
- def test_110_load_config_backup( |
|
64 |
+ def test_load_config_backup( |
|
64 | 65 |
self, |
65 | 66 |
config: Any, |
66 | 67 |
) -> None: |
... | ... |
@@ -83,7 +84,7 @@ class TestCLITransition: |
83 | 84 |
assert cli_helpers.migrate_and_load_old_config()[0] == config |
84 | 85 |
|
85 | 86 |
@Parametrize.BASE_CONFIG_VARIATIONS |
86 |
- def test_111_migrate_config( |
|
87 |
+ def test_migration( |
|
87 | 88 |
self, |
88 | 89 |
config: Any, |
89 | 90 |
) -> None: |
... | ... |
@@ -106,7 +107,7 @@ class TestCLITransition: |
106 | 107 |
assert cli_helpers.migrate_and_load_old_config() == (config, None) |
107 | 108 |
|
108 | 109 |
@Parametrize.BASE_CONFIG_VARIATIONS |
109 |
- def test_112_migrate_config_error( |
|
110 |
+ def test_migration_error( |
|
110 | 111 |
self, |
111 | 112 |
config: Any, |
112 | 113 |
) -> None: |
... | ... |
@@ -136,7 +137,7 @@ class TestCLITransition: |
136 | 137 |
assert err.errno in {errno.EISDIR, errno.EEXIST} |
137 | 138 |
|
138 | 139 |
@Parametrize.BAD_CONFIGS |
139 |
- def test_113_migrate_config_error_bad_config_value( |
|
140 |
+ def test_bad_config_migration( |
|
140 | 141 |
self, |
141 | 142 |
config: Any, |
142 | 143 |
) -> None: |
... | ... |
@@ -161,7 +162,11 @@ class TestCLITransition: |
161 | 162 |
): |
162 | 163 |
cli_helpers.migrate_and_load_old_config() |
163 | 164 |
|
164 |
- def test_200_forward_export_vault_path_parameter( |
|
165 |
+ |
|
166 |
+class TestArgumentForwarding: |
|
167 |
+ """Tests for the argument forwarding up to v1.0.""" |
|
168 |
+ |
|
169 |
+ def test_forward_export_vault_path_parameter( |
|
165 | 170 |
self, |
166 | 171 |
caplog: pytest.LogCaptureFixture, |
167 | 172 |
) -> None: |
... | ... |
@@ -195,7 +200,7 @@ class TestCLITransition: |
195 | 200 |
) |
196 | 201 |
assert json.loads(result.stdout) == data.VAULT_V03_CONFIG_DATA |
197 | 202 |
|
198 |
- def test_201_forward_export_vault_empty_commandline( |
|
203 |
+ def test_forward_export_vault_empty_commandline( |
|
199 | 204 |
self, |
200 | 205 |
caplog: pytest.LogCaptureFixture, |
201 | 206 |
) -> None: |
... | ... |
@@ -228,7 +233,7 @@ class TestCLITransition: |
228 | 233 |
) |
229 | 234 |
|
230 | 235 |
@Parametrize.CHARSET_NAME |
231 |
- def test_210_forward_vault_disable_character_set( |
|
236 |
+ def test_forward_vault_disable_character_set( |
|
232 | 237 |
self, |
233 | 238 |
caplog: pytest.LogCaptureFixture, |
234 | 239 |
charset_name: str, |
... | ... |
@@ -271,7 +276,7 @@ class TestCLITransition: |
271 | 276 |
f"derived password contains forbidden character {c!r}" |
272 | 277 |
) |
273 | 278 |
|
274 |
- def test_211_forward_vault_empty_command_line( |
|
279 |
+ def test_forward_vault_empty_command_line( |
|
275 | 280 |
self, |
276 | 281 |
caplog: pytest.LogCaptureFixture, |
277 | 282 |
) -> None: |
... | ... |
@@ -304,7 +309,11 @@ class TestCLITransition: |
304 | 309 |
error="Deriving a passphrase requires a SERVICE." |
305 | 310 |
), "expected error exit and known error type" |
306 | 311 |
|
307 |
- def test_300_export_using_old_config_file( |
|
312 |
+ |
|
313 |
+class TestConfigMigration: |
|
314 |
+ """Tests for the configuration file migration up to v1.0.""" |
|
315 |
+ |
|
316 |
+ def test_export_and_automatic_migration( |
|
308 | 317 |
self, |
309 | 318 |
caplog: pytest.LogCaptureFixture, |
310 | 319 |
) -> None: |
... | ... |
@@ -345,7 +354,7 @@ class TestCLITransition: |
345 | 354 |
"Successfully migrated to ", caplog.record_tuples |
346 | 355 |
), "expected known warning message in stderr" |
347 | 356 |
|
348 |
- def test_300a_export_using_old_config_file_migration_error( |
|
357 |
+ def test_export_and_automatic_migration_with_errors( |
|
349 | 358 |
self, |
350 | 359 |
caplog: pytest.LogCaptureFixture, |
351 | 360 |
) -> None: |
... | ... |
@@ -395,7 +404,12 @@ class TestCLITransition: |
395 | 404 |
"Failed to migrate to ", caplog.record_tuples |
396 | 405 |
), "expected known warning message in stderr" |
397 | 406 |
|
398 |
- def test_400_completion_service_name_old_config_file( |
|
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( |
|
399 | 413 |
self, |
400 | 414 |
) -> None: |
401 | 415 |
"""Completing service names from the old settings file works.""" |
... | ... |
@@ -526,11 +526,11 @@ class Parametrize(types.SimpleNamespace): |
526 | 526 |
) |
527 | 527 |
|
528 | 528 |
|
529 |
-class TestCLIUtils: |
|
530 |
- """Tests for command-line utility functions.""" |
|
529 |
+class TestConfigLoadingSaving: |
|
530 |
+ """Tests for the config loading and saving utility functions.""" |
|
531 | 531 |
|
532 | 532 |
@Parametrize.BASE_CONFIG_VARIATIONS |
533 |
- def test_100_load_config( |
|
533 |
+ def test_load( |
|
534 | 534 |
self, |
535 | 535 |
config: Any, |
536 | 536 |
) -> None: |
... | ... |
@@ -553,7 +553,7 @@ class TestCLIUtils: |
553 | 553 |
assert json.load(fileobj) == config |
554 | 554 |
assert cli_helpers.load_config() == config |
555 | 555 |
|
556 |
- def test_110_save_bad_config( |
|
556 |
+ def test_save_bad_config( |
|
557 | 557 |
self, |
558 | 558 |
) -> None: |
559 | 559 |
"""[`cli_helpers.save_config`][] fails for bad configurations.""" |
... | ... |
@@ -575,7 +575,11 @@ class TestCLIUtils: |
575 | 575 |
) |
576 | 576 |
cli_helpers.save_config(None) # type: ignore[arg-type] |
577 | 577 |
|
578 |
- def test_111_prompt_for_selection_multiple(self) -> None: |
|
578 |
+ |
|
579 |
+class TestPrompting: |
|
580 |
+ """Tests for the prompting utility functions.""" |
|
581 |
+ |
|
582 |
+ def test_selection_multiple(self) -> None: |
|
579 | 583 |
"""[`cli_helpers.prompt_for_selection`][] works in the "multiple" case.""" |
580 | 584 |
|
581 | 585 |
@click.command() |
... | ... |
@@ -687,7 +691,7 @@ Your selection? (1-10, leave empty to abort):\x20 |
687 | 691 |
Your selection? (1-10, leave empty to abort): """, |
688 | 692 |
}, "expected known output" |
689 | 693 |
|
690 |
- def test_112_prompt_for_selection_single(self) -> None: |
|
694 |
+ def test_selection_single(self) -> None: |
|
691 | 695 |
"""[`cli_helpers.prompt_for_selection`][] works in the "single" case.""" |
692 | 696 |
|
693 | 697 |
@click.command() |
... | ... |
@@ -755,7 +759,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
755 | 759 |
""", |
756 | 760 |
}, "expected known output" |
757 | 761 |
|
758 |
- def test_113_prompt_for_passphrase( |
|
762 |
+ def test_passphrase( |
|
759 | 763 |
self, |
760 | 764 |
) -> None: |
761 | 765 |
"""[`cli_helpers.prompt_for_passphrase`][] works.""" |
... | ... |
@@ -775,7 +779,11 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
775 | 779 |
assert res["kwargs"].get("err"), err_msg |
776 | 780 |
assert res["kwargs"].get("hide_input"), err_msg |
777 | 781 |
|
778 |
- def test_120_standard_logging_context_manager( |
|
782 |
+ |
|
783 |
+class TestLoggingMachinery: |
|
784 |
+ """Tests for the logging utility functions.""" |
|
785 |
+ |
|
786 |
+ def test_standard_logging_context_manager( |
|
779 | 787 |
self, |
780 | 788 |
caplog: pytest.LogCaptureFixture, |
781 | 789 |
capsys: pytest.CaptureFixture[str], |
... | ... |
@@ -837,7 +845,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
837 | 845 |
(package_name, logging.WARNING, "message 3"), |
838 | 846 |
] |
839 | 847 |
|
840 |
- def test_121_standard_logging_warnings_context_manager( |
|
848 |
+ def test_warnings_context_manager( |
|
841 | 849 |
self, |
842 | 850 |
caplog: pytest.LogCaptureFixture, |
843 | 851 |
capsys: pytest.CaptureFixture[str], |
... | ... |
@@ -896,6 +904,10 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
896 | 904 |
assert f"FutureWarning: {THE_FUTURE}" in record_tuples[1][2] |
897 | 905 |
assert f"UserWarning: {JUST_TESTING}" in record_tuples[2][2] |
898 | 906 |
|
907 |
+ |
|
908 |
+class TestExportConfigAsShellScript: |
|
909 |
+ """Tests the utility functions for exporting configs in `sh` format.""" |
|
910 |
+ |
|
899 | 911 |
def export_as_sh_helper( |
900 | 912 |
self, |
901 | 913 |
config: Any, |
... | ... |
@@ -958,7 +970,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
958 | 970 |
}, |
959 | 971 |
), |
960 | 972 |
) |
961 |
- def test_130a_export_as_sh_global( |
|
973 |
+ def test_export_as_sh_global( |
|
962 | 974 |
self, |
963 | 975 |
global_config_settable: _types.VaultConfigServicesSettings, |
964 | 976 |
global_config_importable: _types.VaultConfigServicesSettings, |
... | ... |
@@ -1001,7 +1013,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1001 | 1013 |
}, |
1002 | 1014 |
), |
1003 | 1015 |
) |
1004 |
- def test_130b_export_as_sh_global_only_imports( |
|
1016 |
+ def test_export_as_sh_global_only_imports( |
|
1005 | 1017 |
self, |
1006 | 1018 |
global_config_importable: _types.VaultConfigServicesSettings, |
1007 | 1019 |
) -> None: |
... | ... |
@@ -1061,7 +1073,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1061 | 1073 |
}, |
1062 | 1074 |
), |
1063 | 1075 |
) |
1064 |
- def test_130c_export_as_sh_service( |
|
1076 |
+ def test_export_as_sh_service( |
|
1065 | 1077 |
self, |
1066 | 1078 |
service_name: str, |
1067 | 1079 |
service_config_settable: _types.VaultConfigServicesSettings, |
... | ... |
@@ -1124,7 +1136,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1124 | 1136 |
}, |
1125 | 1137 |
), |
1126 | 1138 |
) |
1127 |
- def test_130d_export_as_sh_service_only_imports( |
|
1139 |
+ def test_export_as_sh_service_only_imports( |
|
1128 | 1140 |
self, |
1129 | 1141 |
service_name: str, |
1130 | 1142 |
service_config_importable: _types.VaultConfigServicesSettings, |
... | ... |
@@ -1146,6 +1158,10 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1146 | 1158 |
assert _types.is_vault_config(config) |
1147 | 1159 |
return self.export_as_sh_helper(config) |
1148 | 1160 |
|
1161 |
+ |
|
1162 |
+class TestTempdir: |
|
1163 |
+ """Tests for the temporary directory handling utility functions.""" |
|
1164 |
+ |
|
1149 | 1165 |
# The Annoying OS appears to silently truncate spaces at the end of |
1150 | 1166 |
# filenames. |
1151 | 1167 |
@hypothesis.given( |
... | ... |
@@ -1165,7 +1181,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1165 | 1181 |
), |
1166 | 1182 |
) |
1167 | 1183 |
@hypothesis.example(env_var="", suffix=".") |
1168 |
- def test_140a_get_tempdir( |
|
1184 |
+ def test_get_tempdir( |
|
1169 | 1185 |
self, |
1170 | 1186 |
env_var: str, |
1171 | 1187 |
suffix: str, |
... | ... |
@@ -1228,7 +1244,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1228 | 1244 |
) |
1229 | 1245 |
assert not temp_path.exists(), f"temp path {temp_path} not cleaned up!" |
1230 | 1246 |
|
1231 |
- def test_140b_get_tempdir_force_default(self) -> None: |
|
1247 |
+ def test_get_tempdir_force_default(self) -> None: |
|
1232 | 1248 |
"""[`cli_helpers.get_tempdir`][] returns a temporary directory. |
1233 | 1249 |
|
1234 | 1250 |
If all candidates are mocked to fail for the standard temporary |
... | ... |
@@ -1282,8 +1298,14 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1282 | 1298 |
monkeypatch.setattr(pathlib.Path, "is_dir", is_dir_error) |
1283 | 1299 |
assert cli_helpers.get_tempdir() == config_dir |
1284 | 1300 |
|
1301 |
+ |
|
1302 |
+# TODO(the-13th-letter): Use a better class name, or consider keeping them |
|
1303 |
+# as top-level functions. |
|
1304 |
+class TestMisc: |
|
1305 |
+ """Miscellaneous tests for the command-line utility functions.""" |
|
1306 |
+ |
|
1285 | 1307 |
@Parametrize.DELETE_CONFIG_INPUT |
1286 |
- def test_203_repeated_config_deletion( |
|
1308 |
+ def test_repeated_config_deletion( |
|
1287 | 1309 |
self, |
1288 | 1310 |
command_line: list[str], |
1289 | 1311 |
config: _types.VaultConfig, |
... | ... |
@@ -1318,7 +1340,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1318 | 1340 |
config_readback = json.load(infile) |
1319 | 1341 |
assert config_readback == result_config |
1320 | 1342 |
|
1321 |
- def test_204_phrase_from_key_manually(self) -> None: |
|
1343 |
+ def test_phrase_from_key_manually(self) -> None: |
|
1322 | 1344 |
"""The dummy service, key and config settings are consistent.""" |
1323 | 1345 |
assert ( |
1324 | 1346 |
vault.Vault( |
... | ... |
@@ -1328,7 +1350,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1328 | 1350 |
) |
1329 | 1351 |
|
1330 | 1352 |
@Parametrize.VALIDATION_FUNCTION_INPUT |
1331 |
- def test_210a_validate_constraints_manually( |
|
1353 |
+ def test_validate_constraints_manually( |
|
1332 | 1354 |
self, |
1333 | 1355 |
vfunc: Callable[[click.Context, click.Parameter, Any], int | None], |
1334 | 1356 |
input: int, |
... | ... |
@@ -1339,7 +1361,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1339 | 1361 |
assert vfunc(ctx, param, input) == input |
1340 | 1362 |
|
1341 | 1363 |
@Parametrize.CONNECTION_HINTS |
1342 |
- def test_227_get_suitable_ssh_keys( |
|
1364 |
+ def test_get_suitable_ssh_keys( |
|
1343 | 1365 |
self, |
1344 | 1366 |
running_ssh_agent: data.RunningSSHAgentInfo, |
1345 | 1367 |
conn_hint: str, |
... | ... |
@@ -1384,7 +1406,7 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo. |
1384 | 1406 |
) |
1385 | 1407 |
|
1386 | 1408 |
@Parametrize.KEY_TO_PHRASE_SETTINGS |
1387 |
- def test_400_key_to_phrase( |
|
1409 |
+ def test_key_to_phrase( |
|
1388 | 1410 |
self, |
1389 | 1411 |
ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient, |
1390 | 1412 |
list_keys_action: ListKeysAction | None, |
... | ... |
@@ -507,10 +507,10 @@ class Parametrize(types.SimpleNamespace): |
507 | 507 |
) |
508 | 508 |
|
509 | 509 |
|
510 |
-class TestTestingMachineryStubbedSSHAgentSocket: |
|
511 |
- """Test the stubbed SSH agent socket for the `ssh_agent` module tests.""" |
|
510 |
+class TestStubbedSSHAgentSocketRequests: |
|
511 |
+ """Test the stubbed SSH agent socket: normal requests.""" |
|
512 | 512 |
|
513 |
- def test_100a_query_extensions_base(self) -> None: |
|
513 |
+ def test_query_extensions_base(self) -> None: |
|
514 | 514 |
"""The base agent implements no extensions.""" |
515 | 515 |
with contextlib.ExitStack() as stack: |
516 | 516 |
monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
... | ... |
@@ -539,7 +539,7 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
539 | 539 |
agent.sendall(query_request) |
540 | 540 |
assert agent.recv(1000) == query_response |
541 | 541 |
|
542 |
- def test_100b_query_extensions_extended(self) -> None: |
|
542 |
+ def test_query_extensions_extended(self) -> None: |
|
543 | 543 |
"""The extended agent implements a known list of extensions.""" |
544 | 544 |
with contextlib.ExitStack() as stack: |
545 | 545 |
monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
... | ... |
@@ -575,7 +575,7 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
575 | 575 |
agent.sendall(query_request) |
576 | 576 |
assert agent.recv(1000) == query_response |
577 | 577 |
|
578 |
- def test_101_request_identities(self) -> None: |
|
578 |
+ def test_request_identities(self) -> None: |
|
579 | 579 |
"""The agent implements a known list of identities.""" |
580 | 580 |
unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix |
581 | 581 |
with machinery.StubbedSSHAgentSocket() as agent: |
... | ... |
@@ -606,7 +606,7 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
606 | 606 |
assert not message |
607 | 607 |
|
608 | 608 |
@Parametrize.SUPPORTED_SSH_TEST_KEYS |
609 |
- def test_102_sign( |
|
609 |
+ def test_sign( |
|
610 | 610 |
self, |
611 | 611 |
ssh_test_key_type: str, |
612 | 612 |
ssh_test_key: data.SSHTestKey, |
... | ... |
@@ -636,7 +636,11 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
636 | 636 |
agent.sendall(query_request) |
637 | 637 |
assert agent.recv(1000) == query_response |
638 | 638 |
|
639 |
- def test_120_close_multiple(self) -> None: |
|
639 |
+ |
|
640 |
+class TestStubbedSSHAgentSocketProperOperations: |
|
641 |
+ """Test the stubbed SSH agent socket: proper use and misuse.""" |
|
642 |
+ |
|
643 |
+ def test_close_multiple(self) -> None: |
|
640 | 644 |
"""The agent can be closed repeatedly.""" |
641 | 645 |
with machinery.StubbedSSHAgentSocket() as agent: |
642 | 646 |
pass |
... | ... |
@@ -644,7 +648,7 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
644 | 648 |
pass |
645 | 649 |
del agent |
646 | 650 |
|
647 |
- def test_121_closed_agents_cannot_be_interacted_with(self) -> None: |
|
651 |
+ def test_closed_agents_cannot_be_interacted_with(self) -> None: |
|
648 | 652 |
"""The agent can be closed repeatedly.""" |
649 | 653 |
with machinery.StubbedSSHAgentSocket() as agent: |
650 | 654 |
pass |
... | ... |
@@ -664,7 +668,7 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
664 | 668 |
agent.sendall(query_request) |
665 | 669 |
assert agent.recv(100) == query_response |
666 | 670 |
|
667 |
- def test_122_no_recv_without_sendall(self) -> None: |
|
671 |
+ def test_no_recv_without_sendall(self) -> None: |
|
668 | 672 |
"""The agent requires a message before sending a response.""" |
669 | 673 |
with machinery.StubbedSSHAgentSocket() as agent: # noqa: SIM117 |
670 | 674 |
with pytest.raises( |
... | ... |
@@ -676,7 +680,7 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
676 | 680 |
agent.recv(100) |
677 | 681 |
|
678 | 682 |
@Parametrize.INVALID_SSH_AGENT_MESSAGES |
679 |
- def test_123_invalid_ssh_agent_messages( |
|
683 |
+ def test_invalid_ssh_agent_messages( |
|
680 | 684 |
self, |
681 | 685 |
message: Buffer, |
682 | 686 |
) -> None: |
... | ... |
@@ -691,8 +695,12 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
691 | 695 |
agent.sendall(message) |
692 | 696 |
assert agent.recv(100) == query_response |
693 | 697 |
|
698 |
+ |
|
699 |
+class TestStubbedSSHAgentSocketSupportedAndUnsupportedFeatures: |
|
700 |
+ """Test the stubbed SSH agent socket: supported/unsupported features.""" |
|
701 |
+ |
|
694 | 702 |
@Parametrize.UNSUPPORTED_SSH_AGENT_MESSAGES |
695 |
- def test_124_unsupported_ssh_agent_messages( |
|
703 |
+ def test_unsupported_ssh_agent_messages( |
|
696 | 704 |
self, |
697 | 705 |
message: Buffer, |
698 | 706 |
) -> None: |
... | ... |
@@ -708,7 +716,7 @@ class TestTestingMachineryStubbedSSHAgentSocket: |
708 | 716 |
assert agent.recv(100) == query_response |
709 | 717 |
|
710 | 718 |
@Parametrize.STUBBED_AGENT_ADDRESSES |
711 |
- def test_125_addresses( |
|
719 |
+ def test_addresses( |
|
712 | 720 |
self, |
713 | 721 |
address: str | None, |
714 | 722 |
exception: type[Exception] | None, |
... | ... |
@@ -776,8 +784,12 @@ class TestStaticFunctionality: |
776 | 784 |
assert not trailer |
777 | 785 |
return ssh_agent.SSHAgentClient.string(unstringed) |
778 | 786 |
|
779 |
- # TODO(the-13th-letter): Re-evaluate if this check is worth keeping. |
|
780 |
- # It cannot provide true tamper-resistence, but probably appears to. |
|
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. |
|
781 | 793 |
@Parametrize.PUBLIC_KEY_DATA |
782 | 794 |
def test_100_key_decoding( |
783 | 795 |
self, |
... | ... |
@@ -791,8 +803,12 @@ class TestStaticFunctionality: |
791 | 803 |
"recorded public key data doesn't match" |
792 | 804 |
) |
793 | 805 |
|
806 |
+ |
|
807 |
+class TestShellExportScriptParsing: |
|
808 |
+ """Test the shell export script parsing utility function.""" |
|
809 |
+ |
|
794 | 810 |
@Parametrize.SH_EXPORT_LINES |
795 |
- def test_190_sh_export_line_parsing( |
|
811 |
+ def test_sh_export_line_parsing( |
|
796 | 812 |
self, line: str, env_name: str, value: str | None |
797 | 813 |
) -> None: |
798 | 814 |
"""[`tests.parse_sh_export_line`][] works.""" |
... | ... |
@@ -805,6 +821,10 @@ class TestStaticFunctionality: |
805 | 821 |
with pytest.raises(ValueError, match="Cannot parse sh line:"): |
806 | 822 |
callables.parse_sh_export_line(line, env_name=env_name) |
807 | 823 |
|
824 |
+ |
|
825 |
+class TestGracefulFailureForSSHAuthSock: |
|
826 |
+ """Test the `posix` socket provider if `SSH_AUTH_SOCK` is unset.""" |
|
827 |
+ |
|
808 | 828 |
def test_200_constructor_posix_no_ssh_auth_sock( |
809 | 829 |
self, |
810 | 830 |
skip_if_no_af_unix_support: None, |
... | ... |
@@ -819,15 +839,19 @@ class TestStaticFunctionality: |
819 | 839 |
): |
820 | 840 |
posix_handler() |
821 | 841 |
|
842 |
+ |
|
843 |
+class TestSSHProtocolDatatypes(TestStaticFunctionality): |
|
844 |
+ """Tests for the utility functions for the SSH protocol datatypes.""" |
|
845 |
+ |
|
822 | 846 |
@Parametrize.UINT32_INPUT |
823 |
- def test_210_uint32(self, input: int, expected: bytes | bytearray) -> None: |
|
847 |
+ def test_uint32(self, input: int, expected: bytes | bytearray) -> None: |
|
824 | 848 |
"""`uint32` encoding works.""" |
825 | 849 |
uint32 = ssh_agent.SSHAgentClient.uint32 |
826 | 850 |
assert uint32(input) == expected |
827 | 851 |
|
828 | 852 |
@hypothesis.given(strategies.integers(min_value=0, max_value=0xFFFFFFFF)) |
829 | 853 |
@hypothesis.example(0xDEADBEEF).via("manual, pre-hypothesis example") |
830 |
- def test_210a_uint32_from_number(self, num: int) -> None: |
|
854 |
+ def test_uint32_from_number(self, num: int) -> None: |
|
831 | 855 |
"""`uint32` encoding works, starting from numbers.""" |
832 | 856 |
uint32 = ssh_agent.SSHAgentClient.uint32 |
833 | 857 |
assert int.from_bytes(uint32(num), "big", signed=False) == num |
... | ... |
@@ -836,7 +860,7 @@ class TestStaticFunctionality: |
836 | 860 |
@hypothesis.example(b"\xde\xad\xbe\xef").via( |
837 | 861 |
"manual, pre-hypothesis example" |
838 | 862 |
) |
839 |
- def test_210b_uint32_from_bytestring(self, bytestring: bytes) -> None: |
|
863 |
+ def test_uint32_from_bytestring(self, bytestring: bytes) -> None: |
|
840 | 864 |
"""`uint32` encoding works, starting from length four byte strings.""" |
841 | 865 |
uint32 = ssh_agent.SSHAgentClient.uint32 |
842 | 866 |
assert ( |
... | ... |
@@ -845,7 +869,7 @@ class TestStaticFunctionality: |
845 | 869 |
) |
846 | 870 |
|
847 | 871 |
@Parametrize.SSH_STRING_INPUT |
848 |
- def test_211_string( |
|
872 |
+ def test_string( |
|
849 | 873 |
self, input: bytes | bytearray, expected: bytes | bytearray |
850 | 874 |
) -> None: |
851 | 875 |
"""SSH string encoding works.""" |
... | ... |
@@ -856,7 +880,7 @@ class TestStaticFunctionality: |
856 | 880 |
@hypothesis.example(b"DEADBEEF" * 10000).via( |
857 | 881 |
"manual, pre-hypothesis example with highest order bit set" |
858 | 882 |
) |
859 |
- def test_211a_string_from_bytestring(self, bytestring: bytes) -> None: |
|
883 |
+ def test_string_from_bytestring(self, bytestring: bytes) -> None: |
|
860 | 884 |
"""SSH string encoding works, starting from a byte string.""" |
861 | 885 |
res = ssh_agent.SSHAgentClient.string(bytestring) |
862 | 886 |
assert res.startswith((b"\x00\x00", b"\x00\x01")) |
... | ... |
@@ -864,7 +888,7 @@ class TestStaticFunctionality: |
864 | 888 |
assert res[4:] == bytestring |
865 | 889 |
|
866 | 890 |
@Parametrize.SSH_UNSTRING_INPUT |
867 |
- def test_212_unstring( |
|
891 |
+ def test_unstring( |
|
868 | 892 |
self, input: bytes | bytearray, expected: bytes | bytearray |
869 | 893 |
) -> None: |
870 | 894 |
"""SSH string decoding works.""" |
... | ... |
@@ -883,7 +907,7 @@ class TestStaticFunctionality: |
883 | 907 |
@hypothesis.example(b"\x00\x00\x00\x01").via( |
884 | 908 |
"detect no-op encoding via ill-formed SSH string" |
885 | 909 |
) |
886 |
- def test_212a_unstring_of_string_of_data(self, bytestring: bytes) -> None: |
|
910 |
+ def test_unstring_of_string_of_data(self, bytestring: bytes) -> None: |
|
887 | 911 |
"""SSH string decoding of encoded SSH strings works. |
888 | 912 |
|
889 | 913 |
References: |
... | ... |
@@ -911,7 +935,7 @@ class TestStaticFunctionality: |
911 | 935 |
lambda x: TestStaticFunctionality.as_ssh_string(x) # noqa: PLW0108 |
912 | 936 |
), |
913 | 937 |
) |
914 |
- def test_212b_string_of_unstring_of_data(self, encoded: bytes) -> None: |
|
938 |
+ def test_string_of_unstring_of_data(self, encoded: bytes) -> None: |
|
915 | 939 |
"""SSH string decoding of encoded SSH strings works. |
916 | 940 |
|
917 | 941 |
References: |
... | ... |
@@ -928,7 +952,11 @@ class TestStaticFunctionality: |
928 | 952 |
assert canon1(encoded) == canon2(encoded) |
929 | 953 |
assert canon1(canon2(encoded)) == canon1(encoded) |
930 | 954 |
|
931 |
- def test_220_registry_resolve( |
|
955 |
+ |
|
956 |
+class TestSSHAgentSocketProviderRegistry: |
|
957 |
+ """Tests for the SSH agent socket provider registry.""" |
|
958 |
+ |
|
959 |
+ def test_resolve( |
|
932 | 960 |
self, |
933 | 961 |
) -> None: |
934 | 962 |
"""Resolving entries in the socket provider registry works.""" |
... | ... |
@@ -944,7 +972,7 @@ class TestStaticFunctionality: |
944 | 972 |
resolve("stub_agent") |
945 | 973 |
|
946 | 974 |
@Parametrize.RESOLVE_CHAINS |
947 |
- def test_221_registry_resolve_chains( |
|
975 |
+ def test_resolve_chains( |
|
948 | 976 |
self, |
949 | 977 |
terminal: Literal["unimplemented", "alias", "callable"], |
950 | 978 |
chain: list[str], |
... | ... |
@@ -1003,7 +1031,7 @@ class TestStaticFunctionality: |
1003 | 1031 |
unique=True, |
1004 | 1032 |
), |
1005 | 1033 |
) |
1006 |
- def test_221a_registry_resolve_chains( |
|
1034 |
+ def test_resolve_chains2( |
|
1007 | 1035 |
self, |
1008 | 1036 |
terminal: Literal["unimplemented", "alias", "callable"], |
1009 | 1037 |
chain: list[str], |
... | ... |
@@ -1041,7 +1069,7 @@ class TestStaticFunctionality: |
1041 | 1069 |
assert resolve(link) == implementation |
1042 | 1070 |
|
1043 | 1071 |
@Parametrize.GOOD_ENTRY_POINTS |
1044 |
- def test_230_find_all_socket_providers( |
|
1072 |
+ def test_find_all_socket_providers( |
|
1045 | 1073 |
self, |
1046 | 1074 |
additional_entry_points: list[importlib.metadata.EntryPoint], |
1047 | 1075 |
) -> None: |
... | ... |
@@ -1060,7 +1088,7 @@ class TestStaticFunctionality: |
1060 | 1088 |
} |
1061 | 1089 |
|
1062 | 1090 |
@Parametrize.BAD_ENTRY_POINTS |
1063 |
- def test_231_find_all_socket_providers_errors( |
|
1091 |
+ def test_find_all_socket_providers_errors( |
|
1064 | 1092 |
self, |
1065 | 1093 |
additional_entry_points: list[importlib.metadata.EntryPoint], |
1066 | 1094 |
) -> None: |
... | ... |
@@ -1074,8 +1102,13 @@ class TestStaticFunctionality: |
1074 | 1102 |
stack.enter_context(pytest.raises(AssertionError)) |
1075 | 1103 |
socketprovider.SocketProvider._find_all_ssh_agent_socket_providers() |
1076 | 1104 |
|
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 |
+ |
|
1077 | 1110 |
@Parametrize.UINT32_EXCEPTIONS |
1078 |
- def test_310_uint32_exceptions( |
|
1111 |
+ def test_uint32_exceptions( |
|
1079 | 1112 |
self, input: int, exc_type: type[Exception], exc_pattern: str |
1080 | 1113 |
) -> None: |
1081 | 1114 |
"""`uint32` encoding fails for out-of-bound values.""" |
... | ... |
@@ -1084,7 +1117,7 @@ class TestStaticFunctionality: |
1084 | 1117 |
uint32(input) |
1085 | 1118 |
|
1086 | 1119 |
@Parametrize.SSH_STRING_EXCEPTIONS |
1087 |
- def test_311_string_exceptions( |
|
1120 |
+ def test_string_exceptions( |
|
1088 | 1121 |
self, input: Any, exc_type: type[Exception], exc_pattern: str |
1089 | 1122 |
) -> None: |
1090 | 1123 |
"""SSH string encoding fails for non-strings.""" |
... | ... |
@@ -1093,7 +1126,7 @@ class TestStaticFunctionality: |
1093 | 1126 |
string(input) |
1094 | 1127 |
|
1095 | 1128 |
@Parametrize.SSH_UNSTRING_EXCEPTIONS |
1096 |
- def test_312_unstring_exceptions( |
|
1129 |
+ def test_unstring_exceptions( |
|
1097 | 1130 |
self, |
1098 | 1131 |
input: bytes | bytearray, |
1099 | 1132 |
exc_type: type[Exception], |
... | ... |
@@ -1112,7 +1145,12 @@ class TestStaticFunctionality: |
1112 | 1145 |
with pytest.raises(exc_type, match=exc_pattern): |
1113 | 1146 |
unstring_prefix(input) |
1114 | 1147 |
|
1115 |
- def test_320_registry_already_registered( |
|
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 |
+ def test_already_registered( |
|
1116 | 1154 |
self, |
1117 | 1155 |
) -> None: |
1118 | 1156 |
"""The registry forbids overwriting entries.""" |
... | ... |
@@ -1136,7 +1174,7 @@ class TestStaticFunctionality: |
1136 | 1174 |
with pytest.raises(ValueError, match="already registered"): |
1137 | 1175 |
register("the_annoying_os", "unix_domain")(the_annoying_os) |
1138 | 1176 |
|
1139 |
- def test_321_registry_resolve_non_existant_entries( |
|
1177 |
+ def test_resolve_non_existant_entries( |
|
1140 | 1178 |
self, |
1141 | 1179 |
) -> None: |
1142 | 1180 |
"""Resolving a non-existant entry fails.""" |
... | ... |
@@ -1153,7 +1191,7 @@ class TestStaticFunctionality: |
1153 | 1191 |
with pytest.raises(socketprovider.NoSuchProviderError): |
1154 | 1192 |
socketprovider.SocketProvider.resolve("native") |
1155 | 1193 |
|
1156 |
- def test_322_registry_register_new_entry( |
|
1194 |
+ def test_register_new_entry( |
|
1157 | 1195 |
self, |
1158 | 1196 |
) -> None: |
1159 | 1197 |
"""Registering new entries works.""" |
... | ... |
@@ -1188,7 +1226,7 @@ class TestStaticFunctionality: |
1188 | 1226 |
]) |
1189 | 1227 |
|
1190 | 1228 |
@Parametrize.EXISTING_REGISTRY_ENTRIES |
1191 |
- def test_323_registry_register_old_entry( |
|
1229 |
+ def test_register_old_entry( |
|
1192 | 1230 |
self, |
1193 | 1231 |
existing: str, |
1194 | 1232 |
) -> None: |
... | ... |
@@ -1230,11 +1268,11 @@ class TestStaticFunctionality: |
1230 | 1268 |
]) |
1231 | 1269 |
|
1232 | 1270 |
|
1233 |
-class TestAgentInteraction: |
|
1234 |
- """Test actually talking to the SSH agent.""" |
|
1271 |
+class TestAgentSigning: |
|
1272 |
+ """Test actually talking to the SSH agent: signing data.""" |
|
1235 | 1273 |
|
1236 | 1274 |
@Parametrize.SUPPORTED_SSH_TEST_KEYS |
1237 |
- def test_200_sign_data_via_agent( |
|
1275 |
+ def test_sign_data_via_agent( |
|
1238 | 1276 |
self, |
1239 | 1277 |
ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient, |
1240 | 1278 |
ssh_test_key_type: str, |
... | ... |
@@ -1279,7 +1317,7 @@ class TestAgentInteraction: |
1279 | 1317 |
), f"SSH signature mismatch ({ssh_test_key_type})" |
1280 | 1318 |
|
1281 | 1319 |
@Parametrize.UNSUITABLE_SSH_TEST_KEYS |
1282 |
- def test_201_sign_data_via_agent_unsupported( |
|
1320 |
+ def test_sign_data_via_agent_unsupported( |
|
1283 | 1321 |
self, |
1284 | 1322 |
ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient, |
1285 | 1323 |
ssh_test_key_type: str, |
... | ... |
@@ -1309,8 +1347,12 @@ class TestAgentInteraction: |
1309 | 1347 |
with pytest.raises(ValueError, match="unsuitable SSH key"): |
1310 | 1348 |
vault.Vault.phrase_from_key(public_key_data, conn=client) |
1311 | 1349 |
|
1350 |
+ |
|
1351 |
+class TestSuitableKeys: |
|
1352 |
+ """Test actually talking to the SSH agent: determining suitable keys.""" |
|
1353 |
+ |
|
1312 | 1354 |
@Parametrize.SSH_KEY_SELECTION |
1313 |
- def test_210_ssh_key_selector( |
|
1355 |
+ def test_ssh_key_selector( |
|
1314 | 1356 |
self, |
1315 | 1357 |
monkeypatch: pytest.MonkeyPatch, |
1316 | 1358 |
ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient, |
... | ... |
@@ -1389,7 +1431,11 @@ class TestAgentInteraction: |
1389 | 1431 |
for snippet in ("Suitable SSH keys:\n", text, f"\n{b64_key}\n"): |
1390 | 1432 |
assert result.clean_exit(output=snippet), "expected clean exit" |
1391 | 1433 |
|
1392 |
- def test_300_constructor_bad_running_agent( |
|
1434 |
+ |
|
1435 |
+class TestConstructorFailures: |
|
1436 |
+ """Test actually talking to the SSH agent: constructor failures.""" |
|
1437 |
+ |
|
1438 |
+ def test_constructor_bad_running_agent( |
|
1393 | 1439 |
self, |
1394 | 1440 |
running_ssh_agent: data.RunningSSHAgentInfo, |
1395 | 1441 |
) -> None: |
... | ... |
@@ -1404,7 +1450,7 @@ class TestAgentInteraction: |
1404 | 1450 |
with pytest.raises(OSError): # noqa: PT011 |
1405 | 1451 |
ssh_agent.SSHAgentClient() |
1406 | 1452 |
|
1407 |
- def test_301_constructor_no_af_unix_support(self) -> None: |
|
1453 |
+ def test_constructor_no_af_unix_support(self) -> None: |
|
1408 | 1454 |
"""Fail without [`socket.AF_UNIX`][] support.""" |
1409 | 1455 |
assert "posix" in socketprovider.SocketProvider.registry |
1410 | 1456 |
with pytest.MonkeyPatch.context() as monkeypatch: |
... | ... |
@@ -1416,7 +1462,7 @@ class TestAgentInteraction: |
1416 | 1462 |
): |
1417 | 1463 |
ssh_agent.SSHAgentClient(socket="posix") |
1418 | 1464 |
|
1419 |
- def test_302_no_ssh_agent_socket_provider_available( |
|
1465 |
+ def test_no_ssh_agent_socket_provider_available( |
|
1420 | 1466 |
self, |
1421 | 1467 |
) -> None: |
1422 | 1468 |
"""Fail if no SSH agent socket provider is available.""" |
... | ... |
@@ -1433,15 +1479,23 @@ class TestAgentInteraction: |
1433 | 1479 |
for e in excinfo.value.exceptions |
1434 | 1480 |
]) |
1435 | 1481 |
|
1436 |
- def test_303_explicit_socket( |
|
1482 |
+ |
|
1483 |
+class TestOtherConstructorFeatures: |
|
1484 |
+ """Test actually talking to the SSH agent: other constructor features.""" |
|
1485 |
+ |
|
1486 |
+ def test_explicit_socket( |
|
1437 | 1487 |
self, |
1438 | 1488 |
spawn_ssh_agent: data.SpawnedSSHAgentInfo, |
1439 | 1489 |
) -> None: |
1440 | 1490 |
conn = spawn_ssh_agent.client._connection |
1441 | 1491 |
ssh_agent.SSHAgentClient(socket=conn) |
1442 | 1492 |
|
1493 |
+ |
|
1494 |
+class TestAgentErrorResponses: |
|
1495 |
+ """Test actually talking to the SSH agent: errors from the SSH agent.""" |
|
1496 |
+ |
|
1443 | 1497 |
@Parametrize.TRUNCATED_AGENT_RESPONSES |
1444 |
- def test_310_truncated_server_response( |
|
1498 |
+ def test_truncated_server_response( |
|
1445 | 1499 |
self, |
1446 | 1500 |
running_ssh_agent: data.RunningSSHAgentInfo, |
1447 | 1501 |
response: bytes, |
... | ... |
@@ -1465,7 +1519,7 @@ class TestAgentInteraction: |
1465 | 1519 |
client.request(255, b"") |
1466 | 1520 |
|
1467 | 1521 |
@Parametrize.LIST_KEYS_ERROR_RESPONSES |
1468 |
- def test_320_list_keys_error_responses( |
|
1522 |
+ def test_list_keys_error_responses( |
|
1469 | 1523 |
self, |
1470 | 1524 |
running_ssh_agent: data.RunningSSHAgentInfo, |
1471 | 1525 |
response_code: _types.SSH_AGENT, |
... | ... |
@@ -1525,7 +1579,7 @@ class TestAgentInteraction: |
1525 | 1579 |
client.list_keys() |
1526 | 1580 |
|
1527 | 1581 |
@Parametrize.SIGN_ERROR_RESPONSES |
1528 |
- def test_330_sign_error_responses( |
|
1582 |
+ def test_sign_error_responses( |
|
1529 | 1583 |
self, |
1530 | 1584 |
running_ssh_agent: data.RunningSSHAgentInfo, |
1531 | 1585 |
key: bytes | bytearray, |
... | ... |
@@ -1594,7 +1648,7 @@ class TestAgentInteraction: |
1594 | 1648 |
client.sign(key, b"abc", check_if_key_loaded=check) |
1595 | 1649 |
|
1596 | 1650 |
@Parametrize.REQUEST_ERROR_RESPONSES |
1597 |
- def test_340_request_error_responses( |
|
1651 |
+ def test_request_error_responses( |
|
1598 | 1652 |
self, |
1599 | 1653 |
running_ssh_agent: data.RunningSSHAgentInfo, |
1600 | 1654 |
request_code: _types.SSH_AGENTC, |
... | ... |
@@ -1622,7 +1676,7 @@ class TestAgentInteraction: |
1622 | 1676 |
client.request(request_code, b"", response_code=response_code) |
1623 | 1677 |
|
1624 | 1678 |
@Parametrize.QUERY_EXTENSIONS_MALFORMED_RESPONSES |
1625 |
- def test_350_query_extensions_malformed_responses( |
|
1679 |
+ def test_query_extensions_malformed_responses( |
|
1626 | 1680 |
self, |
1627 | 1681 |
monkeypatch: pytest.MonkeyPatch, |
1628 | 1682 |
running_ssh_agent: data.RunningSSHAgentInfo, |
1629 | 1683 |