Marco Ricci commited on 2025-08-29 19:45:20
Zeige 1 geänderte Dateien mit 994 Einfügungen und 1389 Löschungen.
(This is part 8 of a series of refactorings for the test suite.) In the basic tests, factor out the common test operation or the common environment setup for each group of related tests, whichever is more feasible. For some groups of related tests, if there were similar tests that differed only in details or in parametrization data, combine them under a new parameter set, if necessary. Both the test file and the commit should be further split up, but I am not yet sure how.
| ... | ... |
@@ -16,13 +16,13 @@ import shutil |
| 16 | 16 |
import socket |
| 17 | 17 |
import textwrap |
| 18 | 18 |
import types |
| 19 |
-from typing import TYPE_CHECKING |
|
| 19 |
+from typing import TYPE_CHECKING, ClassVar |
|
| 20 | 20 |
|
| 21 | 21 |
import click.testing |
| 22 | 22 |
import hypothesis |
| 23 | 23 |
import pytest |
| 24 | 24 |
from hypothesis import strategies |
| 25 |
-from typing_extensions import Any, NamedTuple |
|
| 25 |
+from typing_extensions import Any, NamedTuple, TypedDict |
|
| 26 | 26 |
|
| 27 | 27 |
from derivepassphrase import _types, cli, ssh_agent, vault |
| 28 | 28 |
from derivepassphrase._internals import ( |
| ... | ... |
@@ -35,9 +35,10 @@ from tests.machinery import hypothesis as hypothesis_machinery |
| 35 | 35 |
from tests.machinery import pytest as pytest_machinery |
| 36 | 36 |
|
| 37 | 37 |
if TYPE_CHECKING: |
| 38 |
+ from collections.abc import Iterator |
|
| 38 | 39 |
from typing import NoReturn |
| 39 | 40 |
|
| 40 |
- from typing_extensions import Literal |
|
| 41 |
+ from typing_extensions import Literal, NotRequired |
|
| 41 | 42 |
|
| 42 | 43 |
DUMMY_SERVICE = data.DUMMY_SERVICE |
| 43 | 44 |
DUMMY_PASSPHRASE = data.DUMMY_PASSPHRASE |
| ... | ... |
@@ -262,6 +263,18 @@ def assert_vault_config_is_indented_and_line_broken( |
| 262 | 263 |
]) |
| 263 | 264 |
|
| 264 | 265 |
|
| 266 |
+class Strategies: |
|
| 267 |
+ @staticmethod |
|
| 268 |
+ def notes(*, max_size: int = 512) -> strategies.SearchStrategy[str]: |
|
| 269 |
+ return strategies.text( |
|
| 270 |
+ strategies.characters( |
|
| 271 |
+ min_codepoint=32, max_codepoint=126, include_characters="\n" |
|
| 272 |
+ ), |
|
| 273 |
+ min_size=1, |
|
| 274 |
+ max_size=max_size, |
|
| 275 |
+ ) |
|
| 276 |
+ |
|
| 277 |
+ |
|
| 265 | 278 |
class Parametrize(types.SimpleNamespace): |
| 266 | 279 |
"""Common test parametrizations.""" |
| 267 | 280 |
|
| ... | ... |
@@ -271,23 +284,6 @@ class Parametrize(types.SimpleNamespace): |
| 271 | 284 |
CHARSET_NAME = pytest.mark.parametrize( |
| 272 | 285 |
"charset_name", ["lower", "upper", "number", "space", "dash", "symbol"] |
| 273 | 286 |
) |
| 274 |
- UNICODE_NORMALIZATION_COMMAND_LINES = pytest.mark.parametrize( |
|
| 275 |
- "command_line", |
|
| 276 |
- [ |
|
| 277 |
- pytest.param( |
|
| 278 |
- ["--config", "--phrase"], |
|
| 279 |
- id="configure global passphrase", |
|
| 280 |
- ), |
|
| 281 |
- pytest.param( |
|
| 282 |
- ["--config", "--phrase", "--", "DUMMY_SERVICE"], |
|
| 283 |
- id="configure service passphrase", |
|
| 284 |
- ), |
|
| 285 |
- pytest.param( |
|
| 286 |
- ["--phrase", "--", DUMMY_SERVICE], |
|
| 287 |
- id="interactive passphrase", |
|
| 288 |
- ), |
|
| 289 |
- ], |
|
| 290 |
- ) |
|
| 291 | 287 |
CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES = pytest.mark.parametrize( |
| 292 | 288 |
["command_line", "input", "err_text"], |
| 293 | 289 |
[ |
| ... | ... |
@@ -330,17 +326,19 @@ class Parametrize(types.SimpleNamespace): |
| 330 | 326 |
], |
| 331 | 327 |
) |
| 332 | 328 |
CONFIG_EDITING_VIA_CONFIG_FLAG = pytest.mark.parametrize( |
| 333 |
- ["command_line", "input", "result_config"], |
|
| 329 |
+ ["command_line", "input", "starting_config", "result_config"], |
|
| 334 | 330 |
[ |
| 335 | 331 |
pytest.param( |
| 336 | 332 |
["--phrase"], |
| 337 | 333 |
"my passphrase\n", |
| 334 |
+ {"global": {"phrase": "abc"}, "services": {}},
|
|
| 338 | 335 |
{"global": {"phrase": "my passphrase"}, "services": {}},
|
| 339 | 336 |
id="phrase", |
| 340 | 337 |
), |
| 341 | 338 |
pytest.param( |
| 342 | 339 |
["--key"], |
| 343 | 340 |
"1\n", |
| 341 |
+ {"global": {"phrase": "abc"}, "services": {}},
|
|
| 344 | 342 |
{
|
| 345 | 343 |
"global": {"key": DUMMY_KEY1_B64, "phrase": "abc"},
|
| 346 | 344 |
"services": {},
|
| ... | ... |
@@ -350,6 +348,7 @@ class Parametrize(types.SimpleNamespace): |
| 350 | 348 |
pytest.param( |
| 351 | 349 |
["--phrase", "--", "sv"], |
| 352 | 350 |
"my passphrase\n", |
| 351 |
+ {"global": {"phrase": "abc"}, "services": {}},
|
|
| 353 | 352 |
{
|
| 354 | 353 |
"global": {"phrase": "abc"},
|
| 355 | 354 |
"services": {"sv": {"phrase": "my passphrase"}},
|
| ... | ... |
@@ -359,6 +358,7 @@ class Parametrize(types.SimpleNamespace): |
| 359 | 358 |
pytest.param( |
| 360 | 359 |
["--key", "--", "sv"], |
| 361 | 360 |
"1\n", |
| 361 |
+ {"global": {"phrase": "abc"}, "services": {}},
|
|
| 362 | 362 |
{
|
| 363 | 363 |
"global": {"phrase": "abc"},
|
| 364 | 364 |
"services": {"sv": {"key": DUMMY_KEY1_B64}},
|
| ... | ... |
@@ -368,6 +368,7 @@ class Parametrize(types.SimpleNamespace): |
| 368 | 368 |
pytest.param( |
| 369 | 369 |
["--key", "--length", "15", "--", "sv"], |
| 370 | 370 |
"1\n", |
| 371 |
+ {"global": {"phrase": "abc"}, "services": {}},
|
|
| 371 | 372 |
{
|
| 372 | 373 |
"global": {"phrase": "abc"},
|
| 373 | 374 |
"services": {"sv": {"key": DUMMY_KEY1_B64, "length": 15}},
|
| ... | ... |
@@ -488,20 +489,13 @@ class Parametrize(types.SimpleNamespace): |
| 488 | 489 |
), |
| 489 | 490 |
], |
| 490 | 491 |
) |
| 491 |
- NOOP_EDIT_FUNCS = pytest.mark.parametrize( |
|
| 492 |
- ["edit_func_name", "modern_editor_interface"], |
|
| 493 |
- [ |
|
| 494 |
- pytest.param("empty", True, id="empty"),
|
|
| 495 |
- pytest.param("space", False, id="space-legacy"),
|
|
| 496 |
- pytest.param("space", True, id="space-modern"),
|
|
| 497 |
- ], |
|
| 498 |
- ) |
|
| 499 | 492 |
EXPORT_FORMAT_OPTIONS = pytest.mark.parametrize( |
| 500 | 493 |
"export_options", |
| 501 | 494 |
[ |
| 502 | 495 |
[], |
| 503 | 496 |
["--export-as=sh"], |
| 504 | 497 |
], |
| 498 |
+ ids=["json-format", "sh-format"], |
|
| 505 | 499 |
) |
| 506 | 500 |
KEY_INDEX = pytest.mark.parametrize( |
| 507 | 501 |
"key_index", [1, 2, 3], ids=lambda i: f"index{i}"
|
| ... | ... |
@@ -545,6 +539,45 @@ class Parametrize(types.SimpleNamespace): |
| 545 | 539 |
), |
| 546 | 540 |
id="service", |
| 547 | 541 |
), |
| 542 |
+ pytest.param( |
|
| 543 |
+ textwrap.dedent(r""" |
|
| 544 |
+ [vault] |
|
| 545 |
+ default-unicode-normalization-form = 'XXX' |
|
| 546 |
+ """), |
|
| 547 |
+ ["--config", "--phrase"], |
|
| 548 |
+ DUMMY_PASSPHRASE, |
|
| 549 |
+ ( |
|
| 550 |
+ "Invalid value 'XXX' for config key " |
|
| 551 |
+ "vault.default-unicode-normalization-form" |
|
| 552 |
+ ), |
|
| 553 |
+ id="configure global passphrase", |
|
| 554 |
+ ), |
|
| 555 |
+ pytest.param( |
|
| 556 |
+ textwrap.dedent(r""" |
|
| 557 |
+ [vault] |
|
| 558 |
+ default-unicode-normalization-form = 'XXX' |
|
| 559 |
+ """), |
|
| 560 |
+ ["--config", "--phrase", "--", DUMMY_SERVICE], |
|
| 561 |
+ DUMMY_PASSPHRASE, |
|
| 562 |
+ ( |
|
| 563 |
+ "Invalid value 'XXX' for config key " |
|
| 564 |
+ "vault.default-unicode-normalization-form" |
|
| 565 |
+ ), |
|
| 566 |
+ id="configure service passphrase", |
|
| 567 |
+ ), |
|
| 568 |
+ pytest.param( |
|
| 569 |
+ textwrap.dedent(r""" |
|
| 570 |
+ [vault] |
|
| 571 |
+ default-unicode-normalization-form = 'XXX' |
|
| 572 |
+ """), |
|
| 573 |
+ ["--phrase", "--", DUMMY_SERVICE], |
|
| 574 |
+ DUMMY_PASSPHRASE, |
|
| 575 |
+ ( |
|
| 576 |
+ "Invalid value 'XXX' for config key " |
|
| 577 |
+ "vault.default-unicode-normalization-form" |
|
| 578 |
+ ), |
|
| 579 |
+ id="interactive passphrase", |
|
| 580 |
+ ), |
|
| 548 | 581 |
], |
| 549 | 582 |
) |
| 550 | 583 |
UNICODE_NORMALIZATION_WARNING_INPUTS = pytest.mark.parametrize( |
| ... | ... |
@@ -733,14 +766,7 @@ class TestHelp: |
| 733 | 766 |
class TestDerivedPassphraseConstraints: |
| 734 | 767 |
"""Tests for (derived) passphrase constraints.""" |
| 735 | 768 |
|
| 736 |
- @Parametrize.CHARSET_NAME |
|
| 737 |
- def test_disable_character_set( |
|
| 738 |
- self, |
|
| 739 |
- charset_name: str, |
|
| 740 |
- ) -> None: |
|
| 741 |
- """Named character classes can be disabled on the command-line.""" |
|
| 742 |
- option = f"--{charset_name}"
|
|
| 743 |
- charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
|
|
| 769 |
+ def _test(self, command_line: list[str]) -> machinery.ReadableResult: |
|
| 744 | 770 |
runner = machinery.CliRunner(mix_stderr=False) |
| 745 | 771 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 746 | 772 |
# with-statements. |
| ... | ... |
@@ -758,12 +784,22 @@ class TestDerivedPassphraseConstraints: |
| 758 | 784 |
"prompt_for_passphrase", |
| 759 | 785 |
callables.auto_prompt, |
| 760 | 786 |
) |
| 761 |
- result = runner.invoke( |
|
| 787 |
+ return runner.invoke( |
|
| 762 | 788 |
cli.derivepassphrase_vault, |
| 763 |
- [option, "0", "-p", "--", DUMMY_SERVICE], |
|
| 789 |
+ command_line, |
|
| 764 | 790 |
input=DUMMY_PASSPHRASE, |
| 765 | 791 |
catch_exceptions=False, |
| 766 | 792 |
) |
| 793 |
+ |
|
| 794 |
+ @Parametrize.CHARSET_NAME |
|
| 795 |
+ def test_disable_character_set( |
|
| 796 |
+ self, |
|
| 797 |
+ charset_name: str, |
|
| 798 |
+ ) -> None: |
|
| 799 |
+ """Named character classes can be disabled on the command-line.""" |
|
| 800 |
+ option = f"--{charset_name}"
|
|
| 801 |
+ charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
|
|
| 802 |
+ result = self._test([option, "0", "-p", "--", DUMMY_SERVICE]) |
|
| 767 | 803 |
assert result.clean_exit(empty_stderr=True), "expected clean exit:" |
| 768 | 804 |
for c in charset: |
| 769 | 805 |
assert c not in result.stdout, ( |
| ... | ... |
@@ -774,29 +810,7 @@ class TestDerivedPassphraseConstraints: |
| 774 | 810 |
self, |
| 775 | 811 |
) -> None: |
| 776 | 812 |
"""Character repetition can be disabled on the command-line.""" |
| 777 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 778 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 779 |
- # with-statements. |
|
| 780 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 781 |
- with contextlib.ExitStack() as stack: |
|
| 782 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 783 |
- stack.enter_context( |
|
| 784 |
- pytest_machinery.isolated_config( |
|
| 785 |
- monkeypatch=monkeypatch, |
|
| 786 |
- runner=runner, |
|
| 787 |
- ) |
|
| 788 |
- ) |
|
| 789 |
- monkeypatch.setattr( |
|
| 790 |
- cli_helpers, |
|
| 791 |
- "prompt_for_passphrase", |
|
| 792 |
- callables.auto_prompt, |
|
| 793 |
- ) |
|
| 794 |
- result = runner.invoke( |
|
| 795 |
- cli.derivepassphrase_vault, |
|
| 796 |
- ["--repeat", "0", "-p", "--", DUMMY_SERVICE], |
|
| 797 |
- input=DUMMY_PASSPHRASE, |
|
| 798 |
- catch_exceptions=False, |
|
| 799 |
- ) |
|
| 813 |
+ result = self._test(["--repeat", "0", "-p", "--", DUMMY_SERVICE]) |
|
| 800 | 814 |
assert result.clean_exit(empty_stderr=True), ( |
| 801 | 815 |
"expected clean exit and empty stderr" |
| 802 | 816 |
) |
| ... | ... |
@@ -811,12 +825,17 @@ class TestDerivedPassphraseConstraints: |
| 811 | 825 |
class TestPhraseBasic: |
| 812 | 826 |
"""Tests for master passphrase configuration: basic.""" |
| 813 | 827 |
|
| 814 |
- @Parametrize.CONFIG_WITH_PHRASE |
|
| 815 |
- def test_phrase_from_config( |
|
| 828 |
+ def _test( |
|
| 816 | 829 |
self, |
| 817 |
- config: _types.VaultConfig, |
|
| 830 |
+ command_line: list[str], |
|
| 831 |
+ /, |
|
| 832 |
+ *, |
|
| 833 |
+ config: _types.VaultConfig = { # noqa: B006
|
|
| 834 |
+ "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
|
|
| 835 |
+ }, |
|
| 836 |
+ auto_prompt: bool = False, |
|
| 837 |
+ multiline: bool = False, |
|
| 818 | 838 |
) -> None: |
| 819 |
- """A stored configured master passphrase will be used.""" |
|
| 820 | 839 |
runner = machinery.CliRunner(mix_stderr=False) |
| 821 | 840 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 822 | 841 |
# with-statements. |
| ... | ... |
@@ -837,72 +856,69 @@ class TestPhraseBasic: |
| 837 | 856 |
monkeypatch.setattr( |
| 838 | 857 |
vault.Vault, "phrase_from_key", phrase_from_key |
| 839 | 858 |
) |
| 859 |
+ if auto_prompt: |
|
| 860 |
+ monkeypatch.setattr( |
|
| 861 |
+ cli_helpers, |
|
| 862 |
+ "prompt_for_passphrase", |
|
| 863 |
+ callables.auto_prompt, |
|
| 864 |
+ ) |
|
| 840 | 865 |
result = runner.invoke( |
| 841 | 866 |
cli.derivepassphrase_vault, |
| 842 |
- ["--", DUMMY_SERVICE], |
|
| 867 |
+ command_line, |
|
| 868 |
+ input=None if auto_prompt else DUMMY_PASSPHRASE, |
|
| 843 | 869 |
catch_exceptions=False, |
| 844 | 870 |
) |
| 871 |
+ if multiline: |
|
| 872 |
+ assert result.clean_exit(), "expected clean exit" |
|
| 873 |
+ else: |
|
| 845 | 874 |
assert result.clean_exit(empty_stderr=True), ( |
| 846 | 875 |
"expected clean exit and empty stderr" |
| 847 | 876 |
) |
| 848 |
- assert result.stdout |
|
| 877 |
+ assert result.stdout, "expected program output" |
|
| 878 |
+ last_line = ( |
|
| 879 |
+ result.stdout.splitlines(keepends=True)[-1] |
|
| 880 |
+ if multiline |
|
| 881 |
+ else result.stdout |
|
| 882 |
+ ) |
|
| 849 | 883 |
assert ( |
| 850 |
- result.stdout.rstrip("\n").encode("UTF-8")
|
|
| 851 |
- == DUMMY_RESULT_PASSPHRASE |
|
| 884 |
+ last_line.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_PASSPHRASE
|
|
| 852 | 885 |
), "expected known output" |
| 853 | 886 |
|
| 887 |
+ @Parametrize.CONFIG_WITH_PHRASE |
|
| 888 |
+ def test_phrase_from_config( |
|
| 889 |
+ self, |
|
| 890 |
+ config: _types.VaultConfig, |
|
| 891 |
+ ) -> None: |
|
| 892 |
+ """A stored configured master passphrase will be used.""" |
|
| 893 |
+ self._test(["--", DUMMY_SERVICE], config=config) |
|
| 894 |
+ |
|
| 854 | 895 |
@Parametrize.AUTO_PROMPT |
| 855 | 896 |
def test_phrase_from_command_line( |
| 856 | 897 |
self, |
| 857 | 898 |
auto_prompt: bool, |
| 858 | 899 |
) -> None: |
| 859 | 900 |
"""A master passphrase requested on the command-line will be used.""" |
| 860 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 861 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 862 |
- # with-statements. |
|
| 863 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 864 |
- with contextlib.ExitStack() as stack: |
|
| 865 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 866 |
- stack.enter_context( |
|
| 867 |
- pytest_machinery.isolated_vault_config( |
|
| 868 |
- monkeypatch=monkeypatch, |
|
| 869 |
- runner=runner, |
|
| 870 |
- vault_config={
|
|
| 871 |
- "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
|
|
| 872 |
- }, |
|
| 873 |
- ) |
|
| 874 |
- ) |
|
| 875 |
- if auto_prompt: |
|
| 876 |
- monkeypatch.setattr( |
|
| 877 |
- cli_helpers, |
|
| 878 |
- "prompt_for_passphrase", |
|
| 879 |
- callables.auto_prompt, |
|
| 880 |
- ) |
|
| 881 |
- result = runner.invoke( |
|
| 882 |
- cli.derivepassphrase_vault, |
|
| 901 |
+ self._test( |
|
| 883 | 902 |
["-p", "--", DUMMY_SERVICE], |
| 884 |
- input=None if auto_prompt else DUMMY_PASSPHRASE, |
|
| 885 |
- catch_exceptions=False, |
|
| 903 |
+ auto_prompt=auto_prompt, |
|
| 904 |
+ multiline=True, |
|
| 886 | 905 |
) |
| 887 |
- assert result.clean_exit(), "expected clean exit" |
|
| 888 |
- assert result.stdout, "expected program output" |
|
| 889 |
- last_line = result.stdout.splitlines(True)[-1] |
|
| 890 |
- assert ( |
|
| 891 |
- last_line.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_PASSPHRASE
|
|
| 892 |
- ), "expected known output" |
|
| 893 | 906 |
|
| 894 | 907 |
|
| 895 | 908 |
class TestKeyBasic: |
| 896 | 909 |
"""Tests for SSH key configuration: basic.""" |
| 897 | 910 |
|
| 898 |
- @Parametrize.CONFIG_WITH_KEY |
|
| 899 |
- def test_key_from_config( |
|
| 911 |
+ def _test( |
|
| 900 | 912 |
self, |
| 901 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 902 |
- config: _types.VaultConfig, |
|
| 913 |
+ command_line: list[str], |
|
| 914 |
+ /, |
|
| 915 |
+ *, |
|
| 916 |
+ config: _types.VaultConfig = { # noqa: B006
|
|
| 917 |
+ "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
|
|
| 918 |
+ }, |
|
| 919 |
+ multiline: bool = False, |
|
| 920 |
+ input: str | bytes | None = None, |
|
| 903 | 921 |
) -> None: |
| 904 |
- """A stored configured SSH key will be used.""" |
|
| 905 |
- del running_ssh_agent |
|
| 906 | 922 |
runner = machinery.CliRunner(mix_stderr=False) |
| 907 | 923 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 908 | 924 |
# with-statements. |
| ... | ... |
@@ -916,49 +932,6 @@ class TestKeyBasic: |
| 916 | 932 |
vault_config=config, |
| 917 | 933 |
) |
| 918 | 934 |
) |
| 919 |
- monkeypatch.setattr( |
|
| 920 |
- vault.Vault, |
|
| 921 |
- "phrase_from_key", |
|
| 922 |
- callables.phrase_from_key, |
|
| 923 |
- ) |
|
| 924 |
- result = runner.invoke( |
|
| 925 |
- cli.derivepassphrase_vault, |
|
| 926 |
- ["--", DUMMY_SERVICE], |
|
| 927 |
- catch_exceptions=False, |
|
| 928 |
- ) |
|
| 929 |
- assert result.clean_exit(empty_stderr=True), ( |
|
| 930 |
- "expected clean exit and empty stderr" |
|
| 931 |
- ) |
|
| 932 |
- assert result.stdout |
|
| 933 |
- assert ( |
|
| 934 |
- result.stdout.rstrip("\n").encode("UTF-8")
|
|
| 935 |
- != DUMMY_RESULT_PASSPHRASE |
|
| 936 |
- ), "known false output: phrase-based instead of key-based" |
|
| 937 |
- assert ( |
|
| 938 |
- result.stdout.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1
|
|
| 939 |
- ), "expected known output" |
|
| 940 |
- |
|
| 941 |
- def test_key_from_command_line( |
|
| 942 |
- self, |
|
| 943 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 944 |
- ) -> None: |
|
| 945 |
- """An SSH key requested on the command-line will be used.""" |
|
| 946 |
- del running_ssh_agent |
|
| 947 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 948 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 949 |
- # with-statements. |
|
| 950 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 951 |
- with contextlib.ExitStack() as stack: |
|
| 952 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 953 |
- stack.enter_context( |
|
| 954 |
- pytest_machinery.isolated_vault_config( |
|
| 955 |
- monkeypatch=monkeypatch, |
|
| 956 |
- runner=runner, |
|
| 957 |
- vault_config={
|
|
| 958 |
- "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
|
|
| 959 |
- }, |
|
| 960 |
- ) |
|
| 961 |
- ) |
|
| 962 | 935 |
monkeypatch.setattr( |
| 963 | 936 |
cli_helpers, |
| 964 | 937 |
"get_suitable_ssh_keys", |
| ... | ... |
@@ -971,13 +944,22 @@ class TestKeyBasic: |
| 971 | 944 |
) |
| 972 | 945 |
result = runner.invoke( |
| 973 | 946 |
cli.derivepassphrase_vault, |
| 974 |
- ["-k", "--", DUMMY_SERVICE], |
|
| 975 |
- input="1\n", |
|
| 947 |
+ command_line, |
|
| 948 |
+ input=input, |
|
| 976 | 949 |
catch_exceptions=False, |
| 977 | 950 |
) |
| 951 |
+ if multiline: |
|
| 978 | 952 |
assert result.clean_exit(), "expected clean exit" |
| 953 |
+ else: |
|
| 954 |
+ assert result.clean_exit(empty_stderr=True), ( |
|
| 955 |
+ "expected clean exit and empty stderr" |
|
| 956 |
+ ) |
|
| 979 | 957 |
assert result.stdout, "expected program output" |
| 980 |
- last_line = result.stdout.splitlines(True)[-1] |
|
| 958 |
+ last_line = ( |
|
| 959 |
+ result.stdout.splitlines(keepends=True)[-1] |
|
| 960 |
+ if multiline |
|
| 961 |
+ else result.stdout |
|
| 962 |
+ ) |
|
| 981 | 963 |
assert ( |
| 982 | 964 |
last_line.rstrip("\n").encode("UTF-8") != DUMMY_RESULT_PASSPHRASE
|
| 983 | 965 |
), "known false output: phrase-based instead of key-based" |
| ... | ... |
@@ -985,20 +967,38 @@ class TestKeyBasic: |
| 985 | 967 |
"expected known output" |
| 986 | 968 |
) |
| 987 | 969 |
|
| 970 |
+ @Parametrize.CONFIG_WITH_KEY |
|
| 971 |
+ def test_key_from_config( |
|
| 972 |
+ self, |
|
| 973 |
+ running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 974 |
+ config: _types.VaultConfig, |
|
| 975 |
+ ) -> None: |
|
| 976 |
+ """A stored configured SSH key will be used.""" |
|
| 977 |
+ del running_ssh_agent |
|
| 978 |
+ self._test(["--", DUMMY_SERVICE], config=config) |
|
| 988 | 979 |
|
| 989 |
-class TestPhraseAndKeyOverriding: |
|
| 990 |
- """Tests for master passphrase and SSH key configuration: overriding.""" |
|
| 991 |
- |
|
| 992 |
- @Parametrize.BASE_CONFIG_WITH_KEY_VARIATIONS |
|
| 993 |
- @Parametrize.KEY_INDEX |
|
| 994 |
- def test_key_override_on_command_line( |
|
| 980 |
+ def test_key_from_command_line( |
|
| 995 | 981 |
self, |
| 996 | 982 |
running_ssh_agent: data.RunningSSHAgentInfo, |
| 997 |
- config: dict[str, Any], |
|
| 998 |
- key_index: int, |
|
| 999 | 983 |
) -> None: |
| 1000 |
- """A command-line SSH key will override the configured key.""" |
|
| 984 |
+ """An SSH key requested on the command-line will be used.""" |
|
| 1001 | 985 |
del running_ssh_agent |
| 986 |
+ self._test(["-k", "--", DUMMY_SERVICE], input="1\n", multiline=True) |
|
| 987 |
+ |
|
| 988 |
+ |
|
| 989 |
+class TestPhraseAndKeyOverriding: |
|
| 990 |
+ """Tests for master passphrase and SSH key configuration: overriding.""" |
|
| 991 |
+ |
|
| 992 |
+ def _test( |
|
| 993 |
+ self, |
|
| 994 |
+ command_line: list[str], |
|
| 995 |
+ /, |
|
| 996 |
+ *, |
|
| 997 |
+ config: _types.VaultConfig = { # noqa: B006
|
|
| 998 |
+ "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
|
|
| 999 |
+ }, |
|
| 1000 |
+ input: str | bytes | None = None, |
|
| 1001 |
+ ) -> machinery.ReadableResult: |
|
| 1002 | 1002 |
runner = machinery.CliRunner(mix_stderr=False) |
| 1003 | 1003 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 1004 | 1004 |
# with-statements. |
| ... | ... |
@@ -1022,12 +1022,30 @@ class TestPhraseAndKeyOverriding: |
| 1022 | 1022 |
) |
| 1023 | 1023 |
result = runner.invoke( |
| 1024 | 1024 |
cli.derivepassphrase_vault, |
| 1025 |
- ["-k", "--", DUMMY_SERVICE], |
|
| 1026 |
- input=f"{key_index}\n",
|
|
| 1025 |
+ command_line, |
|
| 1026 |
+ input=input, |
|
| 1027 |
+ catch_exceptions=False, |
|
| 1027 | 1028 |
) |
| 1028 | 1029 |
assert result.clean_exit(), "expected clean exit" |
| 1029 |
- assert result.stdout, "expected program output" |
|
| 1030 |
- assert result.stderr, "expected stderr" |
|
| 1030 |
+ return result |
|
| 1031 |
+ |
|
| 1032 |
+ @Parametrize.BASE_CONFIG_WITH_KEY_VARIATIONS |
|
| 1033 |
+ @Parametrize.KEY_INDEX |
|
| 1034 |
+ def test_key_override_on_command_line( |
|
| 1035 |
+ self, |
|
| 1036 |
+ running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 1037 |
+ config: _types.VaultConfig, |
|
| 1038 |
+ key_index: int, |
|
| 1039 |
+ ) -> None: |
|
| 1040 |
+ """A command-line SSH key will override the configured key.""" |
|
| 1041 |
+ del running_ssh_agent |
|
| 1042 |
+ result = self._test( |
|
| 1043 |
+ ["-k", "--", DUMMY_SERVICE], |
|
| 1044 |
+ config=config, |
|
| 1045 |
+ input=f"{key_index}\n",
|
|
| 1046 |
+ ) |
|
| 1047 |
+ assert result.stdout, "expected program output" |
|
| 1048 |
+ assert result.stderr, "expected stderr" |
|
| 1031 | 1049 |
assert "Error:" not in result.stderr, ( |
| 1032 | 1050 |
"expected no error messages on stderr" |
| 1033 | 1051 |
) |
| ... | ... |
@@ -1036,19 +1054,11 @@ class TestPhraseAndKeyOverriding: |
| 1036 | 1054 |
self, |
| 1037 | 1055 |
running_ssh_agent: data.RunningSSHAgentInfo, |
| 1038 | 1056 |
) -> None: |
| 1039 |
- """A command-line passphrase will override the configured key.""" |
|
| 1057 |
+ """A configured passphrase does not override a configured key.""" |
|
| 1040 | 1058 |
del running_ssh_agent |
| 1041 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1042 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1043 |
- # with-statements. |
|
| 1044 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1045 |
- with contextlib.ExitStack() as stack: |
|
| 1046 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1047 |
- stack.enter_context( |
|
| 1048 |
- pytest_machinery.isolated_vault_config( |
|
| 1049 |
- monkeypatch=monkeypatch, |
|
| 1050 |
- runner=runner, |
|
| 1051 |
- vault_config={
|
|
| 1059 |
+ result = self._test( |
|
| 1060 |
+ ["--", DUMMY_SERVICE], |
|
| 1061 |
+ config={
|
|
| 1052 | 1062 |
"global": {"key": DUMMY_KEY1_B64},
|
| 1053 | 1063 |
"services": {
|
| 1054 | 1064 |
DUMMY_SERVICE: {
|
| ... | ... |
@@ -1058,21 +1068,6 @@ class TestPhraseAndKeyOverriding: |
| 1058 | 1068 |
}, |
| 1059 | 1069 |
}, |
| 1060 | 1070 |
) |
| 1061 |
- ) |
|
| 1062 |
- monkeypatch.setattr( |
|
| 1063 |
- ssh_agent.SSHAgentClient, |
|
| 1064 |
- "list_keys", |
|
| 1065 |
- callables.list_keys, |
|
| 1066 |
- ) |
|
| 1067 |
- monkeypatch.setattr( |
|
| 1068 |
- ssh_agent.SSHAgentClient, "sign", callables.sign |
|
| 1069 |
- ) |
|
| 1070 |
- result = runner.invoke( |
|
| 1071 |
- cli.derivepassphrase_vault, |
|
| 1072 |
- ["--", DUMMY_SERVICE], |
|
| 1073 |
- catch_exceptions=False, |
|
| 1074 |
- ) |
|
| 1075 |
- assert result.clean_exit(), "expected clean exit" |
|
| 1076 | 1071 |
assert result.stdout, "expected program output" |
| 1077 | 1072 |
last_line = result.stdout.splitlines(True)[-1] |
| 1078 | 1073 |
assert ( |
| ... | ... |
@@ -1092,34 +1087,9 @@ class TestPhraseAndKeyOverriding: |
| 1092 | 1087 |
) -> None: |
| 1093 | 1088 |
"""Configuring a passphrase atop an SSH key works, but warns.""" |
| 1094 | 1089 |
del running_ssh_agent |
| 1095 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1096 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1097 |
- # with-statements. |
|
| 1098 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1099 |
- with contextlib.ExitStack() as stack: |
|
| 1100 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1101 |
- stack.enter_context( |
|
| 1102 |
- pytest_machinery.isolated_vault_config( |
|
| 1103 |
- monkeypatch=monkeypatch, |
|
| 1104 |
- runner=runner, |
|
| 1105 |
- vault_config=config, |
|
| 1106 |
- ) |
|
| 1107 |
- ) |
|
| 1108 |
- monkeypatch.setattr( |
|
| 1109 |
- ssh_agent.SSHAgentClient, |
|
| 1110 |
- "list_keys", |
|
| 1111 |
- callables.list_keys, |
|
| 1112 |
- ) |
|
| 1113 |
- monkeypatch.setattr( |
|
| 1114 |
- ssh_agent.SSHAgentClient, "sign", callables.sign |
|
| 1115 |
- ) |
|
| 1116 |
- result = runner.invoke( |
|
| 1117 |
- cli.derivepassphrase_vault, |
|
| 1118 |
- command_line, |
|
| 1119 |
- input=DUMMY_PASSPHRASE, |
|
| 1120 |
- catch_exceptions=False, |
|
| 1090 |
+ result = self._test( |
|
| 1091 |
+ command_line, config=config, input=DUMMY_PASSPHRASE |
|
| 1121 | 1092 |
) |
| 1122 |
- assert result.clean_exit(), "expected clean exit" |
|
| 1123 | 1093 |
assert not result.stdout.strip(), "expected no program output" |
| 1124 | 1094 |
assert result.stderr, "expected known error output" |
| 1125 | 1095 |
err_lines = result.stderr.splitlines(False) |
| ... | ... |
@@ -1140,12 +1110,18 @@ class TestPhraseAndKeyOverriding: |
| 1140 | 1110 |
class TestInvalidCommandLines: |
| 1141 | 1111 |
"""Tests concerning invalid command-lines.""" |
| 1142 | 1112 |
|
| 1143 |
- @Parametrize.VAULT_CHARSET_OPTION |
|
| 1144 |
- def test_invalid_argument_range( |
|
| 1113 |
+ @contextlib.contextmanager |
|
| 1114 |
+ def _setup_environment( |
|
| 1145 | 1115 |
self, |
| 1146 |
- option: str, |
|
| 1147 |
- ) -> None: |
|
| 1148 |
- """Requesting invalidly many characters from a class fails.""" |
|
| 1116 |
+ /, |
|
| 1117 |
+ *, |
|
| 1118 |
+ auto_prompt: bool = False, |
|
| 1119 |
+ config: _types.VaultConfig = { # noqa: B006
|
|
| 1120 |
+ "services": {
|
|
| 1121 |
+ DUMMY_SERVICE: {**DUMMY_CONFIG_SETTINGS},
|
|
| 1122 |
+ }, |
|
| 1123 |
+ }, |
|
| 1124 |
+ ) -> Iterator[machinery.CliRunner]: |
|
| 1149 | 1125 |
runner = machinery.CliRunner(mix_stderr=False) |
| 1150 | 1126 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 1151 | 1127 |
# with-statements. |
| ... | ... |
@@ -1153,11 +1129,57 @@ class TestInvalidCommandLines: |
| 1153 | 1129 |
with contextlib.ExitStack() as stack: |
| 1154 | 1130 |
monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
| 1155 | 1131 |
stack.enter_context( |
| 1156 |
- pytest_machinery.isolated_config( |
|
| 1132 |
+ pytest_machinery.isolated_vault_config( |
|
| 1157 | 1133 |
monkeypatch=monkeypatch, |
| 1158 | 1134 |
runner=runner, |
| 1135 |
+ vault_config=config, |
|
| 1136 |
+ ) |
|
| 1137 |
+ ) |
|
| 1138 |
+ if auto_prompt: |
|
| 1139 |
+ monkeypatch.setattr( |
|
| 1140 |
+ cli_helpers, |
|
| 1141 |
+ "prompt_for_passphrase", |
|
| 1142 |
+ callables.auto_prompt, |
|
| 1143 |
+ ) |
|
| 1144 |
+ yield runner |
|
| 1145 |
+ |
|
| 1146 |
+ def _call( |
|
| 1147 |
+ self, |
|
| 1148 |
+ command_line: list[str], |
|
| 1149 |
+ /, |
|
| 1150 |
+ *, |
|
| 1151 |
+ config: _types.VaultConfig = { # noqa: B006
|
|
| 1152 |
+ "services": {
|
|
| 1153 |
+ DUMMY_SERVICE: {**DUMMY_CONFIG_SETTINGS},
|
|
| 1154 |
+ }, |
|
| 1155 |
+ }, |
|
| 1156 |
+ input: str | bytes | None = None, |
|
| 1157 |
+ runner: machinery.CliRunner | None = None, |
|
| 1158 |
+ ) -> machinery.ReadableResult: |
|
| 1159 |
+ if runner: |
|
| 1160 |
+ return runner.invoke( |
|
| 1161 |
+ cli.derivepassphrase_vault, |
|
| 1162 |
+ command_line, |
|
| 1163 |
+ input=input, |
|
| 1164 |
+ catch_exceptions=False, |
|
| 1159 | 1165 |
) |
| 1166 |
+ with self._setup_environment( |
|
| 1167 |
+ config=config, auto_prompt=input is not None |
|
| 1168 |
+ ) as runner2: |
|
| 1169 |
+ return runner2.invoke( |
|
| 1170 |
+ cli.derivepassphrase_vault, |
|
| 1171 |
+ command_line, |
|
| 1172 |
+ input=input, |
|
| 1173 |
+ catch_exceptions=False, |
|
| 1160 | 1174 |
) |
| 1175 |
+ |
|
| 1176 |
+ @Parametrize.VAULT_CHARSET_OPTION |
|
| 1177 |
+ def test_invalid_argument_range( |
|
| 1178 |
+ self, |
|
| 1179 |
+ option: str, |
|
| 1180 |
+ ) -> None: |
|
| 1181 |
+ """Requesting invalidly many characters from a class fails.""" |
|
| 1182 |
+ with self._setup_environment() as runner: |
|
| 1161 | 1183 |
for value in "-42", "invalid": |
| 1162 | 1184 |
result = runner.invoke( |
| 1163 | 1185 |
cli.derivepassphrase_vault, |
| ... | ... |
@@ -1178,29 +1200,14 @@ class TestInvalidCommandLines: |
| 1178 | 1200 |
check_success: bool, |
| 1179 | 1201 |
) -> None: |
| 1180 | 1202 |
"""We require or forbid a service argument, depending on options.""" |
| 1181 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1182 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1183 |
- # with-statements. |
|
| 1184 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1185 |
- with contextlib.ExitStack() as stack: |
|
| 1186 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1187 |
- stack.enter_context( |
|
| 1188 |
- pytest_machinery.isolated_vault_config( |
|
| 1189 |
- monkeypatch=monkeypatch, |
|
| 1190 |
- runner=runner, |
|
| 1191 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 1192 |
- ) |
|
| 1193 |
- ) |
|
| 1194 |
- monkeypatch.setattr( |
|
| 1195 |
- cli_helpers, |
|
| 1196 |
- "prompt_for_passphrase", |
|
| 1197 |
- callables.auto_prompt, |
|
| 1198 |
- ) |
|
| 1199 |
- result = runner.invoke( |
|
| 1200 |
- cli.derivepassphrase_vault, |
|
| 1203 |
+ config: _types.VaultConfig = {
|
|
| 1204 |
+ "global": {"phrase": "abc"},
|
|
| 1205 |
+ "services": {},
|
|
| 1206 |
+ } |
|
| 1207 |
+ result = self._call( |
|
| 1201 | 1208 |
options if service else [*options, "--", DUMMY_SERVICE], |
| 1209 |
+ config=config, |
|
| 1202 | 1210 |
input=input, |
| 1203 |
- catch_exceptions=False, |
|
| 1204 | 1211 |
) |
| 1205 | 1212 |
if service is not None: |
| 1206 | 1213 |
err_msg = ( |
| ... | ... |
@@ -1211,37 +1218,16 @@ class TestInvalidCommandLines: |
| 1211 | 1218 |
assert result.error_exit(error=err_msg), ( |
| 1212 | 1219 |
"expected error exit and known error message" |
| 1213 | 1220 |
) |
| 1214 |
- else: |
|
| 1215 |
- assert result.clean_exit(empty_stderr=True), ( |
|
| 1216 |
- "expected clean exit" |
|
| 1217 |
- ) |
|
| 1218 | 1221 |
if check_success: |
| 1219 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1220 |
- # with-statements. |
|
| 1221 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1222 |
- with contextlib.ExitStack() as stack: |
|
| 1223 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1224 |
- stack.enter_context( |
|
| 1225 |
- pytest_machinery.isolated_vault_config( |
|
| 1226 |
- monkeypatch=monkeypatch, |
|
| 1227 |
- runner=runner, |
|
| 1228 |
- vault_config={
|
|
| 1229 |
- "global": {"phrase": "abc"},
|
|
| 1230 |
- "services": {},
|
|
| 1231 |
- }, |
|
| 1232 |
- ) |
|
| 1233 |
- ) |
|
| 1234 |
- monkeypatch.setattr( |
|
| 1235 |
- cli_helpers, |
|
| 1236 |
- "prompt_for_passphrase", |
|
| 1237 |
- callables.auto_prompt, |
|
| 1238 |
- ) |
|
| 1239 |
- result = runner.invoke( |
|
| 1240 |
- cli.derivepassphrase_vault, |
|
| 1222 |
+ result = self._call( |
|
| 1241 | 1223 |
[*options, "--", DUMMY_SERVICE] if service else options, |
| 1224 |
+ config=config, |
|
| 1242 | 1225 |
input=input, |
| 1243 |
- catch_exceptions=False, |
|
| 1244 | 1226 |
) |
| 1227 |
+ assert result.clean_exit(empty_stderr=True), ( |
|
| 1228 |
+ "expected clean exit" |
|
| 1229 |
+ ) |
|
| 1230 |
+ else: |
|
| 1245 | 1231 |
assert result.clean_exit(empty_stderr=True), "expected clean exit" |
| 1246 | 1232 |
|
| 1247 | 1233 |
def test_empty_service_name_causes_warning( |
| ... | ... |
@@ -1261,50 +1247,31 @@ class TestInvalidCommandLines: |
| 1261 | 1247 |
"An empty SERVICE is not supported by vault(1)", [record] |
| 1262 | 1248 |
) |
| 1263 | 1249 |
|
| 1264 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1265 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1266 |
- # with-statements. |
|
| 1267 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1268 |
- with contextlib.ExitStack() as stack: |
|
| 1269 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1270 |
- stack.enter_context( |
|
| 1271 |
- pytest_machinery.isolated_vault_config( |
|
| 1272 |
- monkeypatch=monkeypatch, |
|
| 1273 |
- runner=runner, |
|
| 1274 |
- vault_config={"services": {}},
|
|
| 1275 |
- ) |
|
| 1276 |
- ) |
|
| 1277 |
- monkeypatch.setattr( |
|
| 1278 |
- cli_helpers, |
|
| 1279 |
- "prompt_for_passphrase", |
|
| 1280 |
- callables.auto_prompt, |
|
| 1281 |
- ) |
|
| 1282 |
- result = runner.invoke( |
|
| 1283 |
- cli.derivepassphrase_vault, |
|
| 1284 |
- ["--config", "--length=30", "--", ""], |
|
| 1285 |
- catch_exceptions=False, |
|
| 1286 |
- ) |
|
| 1250 |
+ def check_result(result: machinery.ReadableResult) -> None: |
|
| 1287 | 1251 |
assert result.clean_exit(empty_stderr=False), "expected clean exit" |
| 1288 | 1252 |
assert result.stderr is not None, "expected known error output" |
| 1289 | 1253 |
assert all(map(is_expected_warning, caplog.record_tuples)), ( |
| 1290 | 1254 |
"expected known error output" |
| 1291 | 1255 |
) |
| 1256 |
+ |
|
| 1257 |
+ with self._setup_environment( |
|
| 1258 |
+ config={"services": {}}, auto_prompt=True
|
|
| 1259 |
+ ) as runner: |
|
| 1260 |
+ result = self._call( |
|
| 1261 |
+ ["--config", "--length=30", "--", ""], runner=runner |
|
| 1262 |
+ ) |
|
| 1263 |
+ check_result(result) |
|
| 1292 | 1264 |
assert cli_helpers.load_config() == {
|
| 1293 | 1265 |
"global": {"length": 30},
|
| 1294 | 1266 |
"services": {},
|
| 1295 | 1267 |
}, "requested configuration change was not applied" |
| 1296 | 1268 |
caplog.clear() |
| 1297 |
- result = runner.invoke( |
|
| 1298 |
- cli.derivepassphrase_vault, |
|
| 1269 |
+ result = self._call( |
|
| 1299 | 1270 |
["--import", "-"], |
| 1300 | 1271 |
input=json.dumps({"services": {"": {"length": 40}}}),
|
| 1301 |
- catch_exceptions=False, |
|
| 1302 |
- ) |
|
| 1303 |
- assert result.clean_exit(empty_stderr=False), "expected clean exit" |
|
| 1304 |
- assert result.stderr is not None, "expected known error output" |
|
| 1305 |
- assert all(map(is_expected_warning, caplog.record_tuples)), ( |
|
| 1306 |
- "expected known error output" |
|
| 1272 |
+ runner=runner, |
|
| 1307 | 1273 |
) |
| 1274 |
+ check_result(result) |
|
| 1308 | 1275 |
assert cli_helpers.load_config() == {
|
| 1309 | 1276 |
"global": {"length": 30},
|
| 1310 | 1277 |
"services": {"": {"length": 40}},
|
| ... | ... |
@@ -1317,23 +1284,9 @@ class TestInvalidCommandLines: |
| 1317 | 1284 |
service: bool | None, |
| 1318 | 1285 |
) -> None: |
| 1319 | 1286 |
"""Incompatible options are detected.""" |
| 1320 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1321 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1322 |
- # with-statements. |
|
| 1323 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1324 |
- with contextlib.ExitStack() as stack: |
|
| 1325 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1326 |
- stack.enter_context( |
|
| 1327 |
- pytest_machinery.isolated_config( |
|
| 1328 |
- monkeypatch=monkeypatch, |
|
| 1329 |
- runner=runner, |
|
| 1330 |
- ) |
|
| 1331 |
- ) |
|
| 1332 |
- result = runner.invoke( |
|
| 1333 |
- cli.derivepassphrase_vault, |
|
| 1287 |
+ result = self._call( |
|
| 1334 | 1288 |
[*options, "--", DUMMY_SERVICE] if service else options, |
| 1335 | 1289 |
input=DUMMY_PASSPHRASE, |
| 1336 |
- catch_exceptions=False, |
|
| 1337 | 1290 |
) |
| 1338 | 1291 |
assert result.error_exit(error="mutually exclusive with "), ( |
| 1339 | 1292 |
"expected error exit and known error message" |
| ... | ... |
@@ -1341,21 +1294,7 @@ class TestInvalidCommandLines: |
| 1341 | 1294 |
|
| 1342 | 1295 |
def test_no_arguments(self) -> None: |
| 1343 | 1296 |
"""Calling `derivepassphrase vault` without any arguments fails.""" |
| 1344 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1345 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1346 |
- # with-statements. |
|
| 1347 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1348 |
- with contextlib.ExitStack() as stack: |
|
| 1349 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1350 |
- stack.enter_context( |
|
| 1351 |
- pytest_machinery.isolated_config( |
|
| 1352 |
- monkeypatch=monkeypatch, |
|
| 1353 |
- runner=runner, |
|
| 1354 |
- ) |
|
| 1355 |
- ) |
|
| 1356 |
- result = runner.invoke( |
|
| 1357 |
- cli.derivepassphrase_vault, [], catch_exceptions=False |
|
| 1358 |
- ) |
|
| 1297 |
+ result = self._call([], input=DUMMY_PASSPHRASE) |
|
| 1359 | 1298 |
assert result.error_exit( |
| 1360 | 1299 |
error="Deriving a passphrase requires a SERVICE" |
| 1361 | 1300 |
), "expected error exit and known error message" |
| ... | ... |
@@ -1364,23 +1303,7 @@ class TestInvalidCommandLines: |
| 1364 | 1303 |
self, |
| 1365 | 1304 |
) -> None: |
| 1366 | 1305 |
"""Deriving a passphrase without a passphrase or key fails.""" |
| 1367 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1368 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1369 |
- # with-statements. |
|
| 1370 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1371 |
- with contextlib.ExitStack() as stack: |
|
| 1372 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1373 |
- stack.enter_context( |
|
| 1374 |
- pytest_machinery.isolated_config( |
|
| 1375 |
- monkeypatch=monkeypatch, |
|
| 1376 |
- runner=runner, |
|
| 1377 |
- ) |
|
| 1378 |
- ) |
|
| 1379 |
- result = runner.invoke( |
|
| 1380 |
- cli.derivepassphrase_vault, |
|
| 1381 |
- ["--", DUMMY_SERVICE], |
|
| 1382 |
- catch_exceptions=False, |
|
| 1383 |
- ) |
|
| 1306 |
+ result = self._call(["--", DUMMY_SERVICE], input=DUMMY_PASSPHRASE) |
|
| 1384 | 1307 |
assert result.error_exit(error="No passphrase or key was given"), ( |
| 1385 | 1308 |
"expected error exit and known error message" |
| 1386 | 1309 |
) |
| ... | ... |
@@ -1389,13 +1312,15 @@ class TestInvalidCommandLines: |
| 1389 | 1312 |
class TestImportConfigValid: |
| 1390 | 1313 |
"""Tests concerning `vault` configuration imports: valid imports.""" |
| 1391 | 1314 |
|
| 1392 |
- @Parametrize.VALID_TEST_CONFIGS |
|
| 1393 |
- def test_import_config( |
|
| 1315 |
+ def _test( |
|
| 1394 | 1316 |
self, |
| 1317 |
+ /, |
|
| 1318 |
+ *, |
|
| 1395 | 1319 |
caplog: pytest.LogCaptureFixture, |
| 1396 |
- config: Any, |
|
| 1320 |
+ config: _types.VaultConfig, |
|
| 1397 | 1321 |
) -> None: |
| 1398 |
- """Importing a configuration works.""" |
|
| 1322 |
+ config2 = copy.deepcopy(config) |
|
| 1323 |
+ _types.clean_up_falsy_vault_config_values(config2) |
|
| 1399 | 1324 |
runner = machinery.CliRunner(mix_stderr=False) |
| 1400 | 1325 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 1401 | 1326 |
# with-statements. |
| ... | ... |
@@ -1418,14 +1343,23 @@ class TestImportConfigValid: |
| 1418 | 1343 |
config_txt = cli_helpers.config_filename( |
| 1419 | 1344 |
subsystem="vault" |
| 1420 | 1345 |
).read_text(encoding="UTF-8") |
| 1421 |
- config2 = json.loads(config_txt) |
|
| 1346 |
+ config3 = json.loads(config_txt) |
|
| 1422 | 1347 |
assert result.clean_exit(empty_stderr=False), "expected clean exit" |
| 1423 |
- assert config2 == config, "config not imported correctly" |
|
| 1424 |
- assert not result.stderr or all( # pragma: no branch |
|
| 1348 |
+ assert config3 == config2, "config not imported correctly" |
|
| 1349 |
+ assert not result.stderr or all( |
|
| 1425 | 1350 |
map(is_harmless_config_import_warning, caplog.record_tuples) |
| 1426 | 1351 |
), "unexpected error output" |
| 1427 | 1352 |
assert_vault_config_is_indented_and_line_broken(config_txt) |
| 1428 | 1353 |
|
| 1354 |
+ @Parametrize.VALID_TEST_CONFIGS |
|
| 1355 |
+ def test_normal_config( |
|
| 1356 |
+ self, |
|
| 1357 |
+ caplog: pytest.LogCaptureFixture, |
|
| 1358 |
+ config: Any, |
|
| 1359 |
+ ) -> None: |
|
| 1360 |
+ """Importing a configuration works.""" |
|
| 1361 |
+ self._test(caplog=caplog, config=config) |
|
| 1362 |
+ |
|
| 1429 | 1363 |
@hypothesis.settings( |
| 1430 | 1364 |
suppress_health_check=[ |
| 1431 | 1365 |
*hypothesis.settings().suppress_health_check, |
| ... | ... |
@@ -1439,7 +1373,7 @@ class TestImportConfigValid: |
| 1439 | 1373 |
]) |
| 1440 | 1374 |
) |
| 1441 | 1375 |
) |
| 1442 |
- def test_import_smudged_config( |
|
| 1376 |
+ def test_smudged_config( |
|
| 1443 | 1377 |
self, |
| 1444 | 1378 |
caplog: pytest.LogCaptureFixture, |
| 1445 | 1379 |
conf: data.VaultTestConfig, |
| ... | ... |
@@ -1449,49 +1383,16 @@ class TestImportConfigValid: |
| 1449 | 1383 |
Tested via hypothesis. |
| 1450 | 1384 |
|
| 1451 | 1385 |
""" |
| 1452 |
- config = conf.config |
|
| 1453 |
- config2 = copy.deepcopy(config) |
|
| 1454 |
- _types.clean_up_falsy_vault_config_values(config2) |
|
| 1455 | 1386 |
# Reset caplog between hypothesis runs. |
| 1456 | 1387 |
caplog.clear() |
| 1457 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1458 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1459 |
- # with-statements. |
|
| 1460 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1461 |
- with contextlib.ExitStack() as stack: |
|
| 1462 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1463 |
- stack.enter_context( |
|
| 1464 |
- pytest_machinery.isolated_vault_config( |
|
| 1465 |
- monkeypatch=monkeypatch, |
|
| 1466 |
- runner=runner, |
|
| 1467 |
- vault_config={"services": {}},
|
|
| 1468 |
- ) |
|
| 1469 |
- ) |
|
| 1470 |
- result = runner.invoke( |
|
| 1471 |
- cli.derivepassphrase_vault, |
|
| 1472 |
- ["--import", "-"], |
|
| 1473 |
- input=json.dumps(config), |
|
| 1474 |
- catch_exceptions=False, |
|
| 1475 |
- ) |
|
| 1476 |
- config_txt = cli_helpers.config_filename( |
|
| 1477 |
- subsystem="vault" |
|
| 1478 |
- ).read_text(encoding="UTF-8") |
|
| 1479 |
- config3 = json.loads(config_txt) |
|
| 1480 |
- assert result.clean_exit(empty_stderr=False), "expected clean exit" |
|
| 1481 |
- assert config3 == config2, "config not imported correctly" |
|
| 1482 |
- assert not result.stderr or all( |
|
| 1483 |
- map(is_harmless_config_import_warning, caplog.record_tuples) |
|
| 1484 |
- ), "unexpected error output" |
|
| 1485 |
- assert_vault_config_is_indented_and_line_broken(config_txt) |
|
| 1388 |
+ self._test(caplog=caplog, config=conf.config) |
|
| 1486 | 1389 |
|
| 1487 | 1390 |
|
| 1488 | 1391 |
class TestImportConfigInvalid: |
| 1489 | 1392 |
"""Tests concerning `vault` configuration imports: invalid imports.""" |
| 1490 | 1393 |
|
| 1491 |
- def test_import_config_not_a_vault_config( |
|
| 1492 |
- self, |
|
| 1493 |
- ) -> None: |
|
| 1494 |
- """Importing an invalid config fails.""" |
|
| 1394 |
+ @contextlib.contextmanager |
|
| 1395 |
+ def _setup_environment(self) -> Iterator[machinery.CliRunner]: |
|
| 1495 | 1396 |
runner = machinery.CliRunner(mix_stderr=False) |
| 1496 | 1397 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 1497 | 1398 |
# with-statements. |
| ... | ... |
@@ -1504,64 +1405,52 @@ class TestImportConfigInvalid: |
| 1504 | 1405 |
runner=runner, |
| 1505 | 1406 |
) |
| 1506 | 1407 |
) |
| 1507 |
- result = runner.invoke( |
|
| 1408 |
+ yield runner |
|
| 1409 |
+ |
|
| 1410 |
+ def _test( |
|
| 1411 |
+ self, |
|
| 1412 |
+ command_line: list[str], |
|
| 1413 |
+ /, |
|
| 1414 |
+ *, |
|
| 1415 |
+ input: str | bytes | None = None, |
|
| 1416 |
+ ) -> machinery.ReadableResult: |
|
| 1417 |
+ with self._setup_environment() as runner: |
|
| 1418 |
+ return runner.invoke( |
|
| 1508 | 1419 |
cli.derivepassphrase_vault, |
| 1509 |
- ["--import", "-"], |
|
| 1510 |
- input="null", |
|
| 1420 |
+ command_line, |
|
| 1421 |
+ input=input, |
|
| 1511 | 1422 |
catch_exceptions=False, |
| 1512 | 1423 |
) |
| 1424 |
+ |
|
| 1425 |
+ def test_not_a_vault_config( |
|
| 1426 |
+ self, |
|
| 1427 |
+ ) -> None: |
|
| 1428 |
+ """Importing an invalid config fails.""" |
|
| 1429 |
+ result = self._test(["--import", "-"], input="null") |
|
| 1513 | 1430 |
assert result.error_exit(error="Invalid vault config"), ( |
| 1514 | 1431 |
"expected error exit and known error message" |
| 1515 | 1432 |
) |
| 1516 | 1433 |
|
| 1517 |
- def test_import_config_not_json_data( |
|
| 1434 |
+ def test_not_json_data( |
|
| 1518 | 1435 |
self, |
| 1519 | 1436 |
) -> None: |
| 1520 | 1437 |
"""Importing an invalid config fails.""" |
| 1521 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1522 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1523 |
- # with-statements. |
|
| 1524 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1525 |
- with contextlib.ExitStack() as stack: |
|
| 1526 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1527 |
- stack.enter_context( |
|
| 1528 |
- pytest_machinery.isolated_config( |
|
| 1529 |
- monkeypatch=monkeypatch, |
|
| 1530 |
- runner=runner, |
|
| 1531 |
- ) |
|
| 1532 |
- ) |
|
| 1533 |
- result = runner.invoke( |
|
| 1534 |
- cli.derivepassphrase_vault, |
|
| 1535 |
- ["--import", "-"], |
|
| 1536 |
- input="This string is not valid JSON.", |
|
| 1537 |
- catch_exceptions=False, |
|
| 1438 |
+ result = self._test( |
|
| 1439 |
+ ["--import", "-"], input="This string is not valid JSON." |
|
| 1538 | 1440 |
) |
| 1539 | 1441 |
assert result.error_exit(error="cannot decode JSON"), ( |
| 1540 | 1442 |
"expected error exit and known error message" |
| 1541 | 1443 |
) |
| 1542 | 1444 |
|
| 1543 |
- def test_import_config_not_a_file( |
|
| 1445 |
+ def test_not_a_file( |
|
| 1544 | 1446 |
self, |
| 1545 | 1447 |
) -> None: |
| 1546 | 1448 |
"""Importing an invalid config fails.""" |
| 1547 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1548 |
- # `isolated_vault_config` ensures the configuration is valid |
|
| 1549 |
- # JSON. So, to pass an actual broken configuration, we must |
|
| 1550 |
- # open the configuration file ourselves afterwards, inside the |
|
| 1551 |
- # context. |
|
| 1552 |
- # |
|
| 1553 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1554 |
- # with-statements. |
|
| 1555 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1556 |
- with contextlib.ExitStack() as stack: |
|
| 1557 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1558 |
- stack.enter_context( |
|
| 1559 |
- pytest_machinery.isolated_vault_config( |
|
| 1560 |
- monkeypatch=monkeypatch, |
|
| 1561 |
- runner=runner, |
|
| 1562 |
- vault_config={"services": {}},
|
|
| 1563 |
- ) |
|
| 1564 |
- ) |
|
| 1449 |
+ with self._setup_environment() as runner: |
|
| 1450 |
+ # `_setup_environment` (via `isolated_vault_config`) ensures |
|
| 1451 |
+ # the configuration is valid JSON. So, to pass an actual |
|
| 1452 |
+ # broken configuration, we must open the configuration file |
|
| 1453 |
+ # ourselves afterwards, inside the context. |
|
| 1565 | 1454 |
cli_helpers.config_filename(subsystem="vault").write_text( |
| 1566 | 1455 |
"This string is not valid JSON.\n", encoding="UTF-8" |
| 1567 | 1456 |
) |
| ... | ... |
@@ -1582,13 +1471,28 @@ class TestImportConfigInvalid: |
| 1582 | 1471 |
class TestExportConfigValid: |
| 1583 | 1472 |
"""Tests concerning `vault` configuration exports: valid exports.""" |
| 1584 | 1473 |
|
| 1585 |
- @Parametrize.VALID_TEST_CONFIGS |
|
| 1586 |
- def test_export_config_success( |
|
| 1474 |
+ def _assert_result( |
|
| 1587 | 1475 |
self, |
| 1476 |
+ result: machinery.ReadableResult, |
|
| 1477 |
+ /, |
|
| 1478 |
+ *, |
|
| 1588 | 1479 |
caplog: pytest.LogCaptureFixture, |
| 1589 |
- config: Any, |
|
| 1590 | 1480 |
) -> None: |
| 1591 |
- """Exporting a configuration works.""" |
|
| 1481 |
+ assert result.clean_exit(empty_stderr=False), "expected clean exit" |
|
| 1482 |
+ assert not result.stderr or all( |
|
| 1483 |
+ map(is_harmless_config_import_warning, caplog.record_tuples) |
|
| 1484 |
+ ), "unexpected error output" |
|
| 1485 |
+ |
|
| 1486 |
+ def _test( |
|
| 1487 |
+ self, |
|
| 1488 |
+ /, |
|
| 1489 |
+ *, |
|
| 1490 |
+ caplog: pytest.LogCaptureFixture, |
|
| 1491 |
+ config: _types.VaultConfig, |
|
| 1492 |
+ use_import: bool = False, |
|
| 1493 |
+ ) -> None: |
|
| 1494 |
+ config2 = copy.deepcopy(config) |
|
| 1495 |
+ _types.clean_up_falsy_vault_config_values(config2) |
|
| 1592 | 1496 |
runner = machinery.CliRunner(mix_stderr=False) |
| 1593 | 1497 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 1594 | 1498 |
# with-statements. |
| ... | ... |
@@ -1599,9 +1503,18 @@ class TestExportConfigValid: |
| 1599 | 1503 |
pytest_machinery.isolated_vault_config( |
| 1600 | 1504 |
monkeypatch=monkeypatch, |
| 1601 | 1505 |
runner=runner, |
| 1602 |
- vault_config=config, |
|
| 1506 |
+ vault_config={"services": {}},
|
|
| 1603 | 1507 |
) |
| 1604 | 1508 |
) |
| 1509 |
+ if use_import: |
|
| 1510 |
+ result1 = runner.invoke( |
|
| 1511 |
+ cli.derivepassphrase_vault, |
|
| 1512 |
+ ["--import", "-"], |
|
| 1513 |
+ input=json.dumps(config), |
|
| 1514 |
+ catch_exceptions=False, |
|
| 1515 |
+ ) |
|
| 1516 |
+ self._assert_result(result1, caplog=caplog) |
|
| 1517 |
+ else: |
|
| 1605 | 1518 |
with cli_helpers.config_filename(subsystem="vault").open( |
| 1606 | 1519 |
"w", encoding="UTF-8" |
| 1607 | 1520 |
) as outfile: |
| ... | ... |
@@ -1612,17 +1525,20 @@ class TestExportConfigValid: |
| 1612 | 1525 |
["--export", "-"], |
| 1613 | 1526 |
catch_exceptions=False, |
| 1614 | 1527 |
) |
| 1615 |
- with cli_helpers.config_filename(subsystem="vault").open( |
|
| 1616 |
- encoding="UTF-8" |
|
| 1617 |
- ) as infile: |
|
| 1618 |
- config2 = json.load(infile) |
|
| 1619 |
- assert result.clean_exit(empty_stderr=False), "expected clean exit" |
|
| 1620 |
- assert config2 == config, "config not imported correctly" |
|
| 1621 |
- assert not result.stderr or all( # pragma: no branch |
|
| 1622 |
- map(is_harmless_config_import_warning, caplog.record_tuples) |
|
| 1623 |
- ), "unexpected error output" |
|
| 1528 |
+ self._assert_result(result, caplog=caplog) |
|
| 1529 |
+ config3 = json.loads(result.stdout) |
|
| 1530 |
+ assert config3 == config2, "config not exported correctly" |
|
| 1624 | 1531 |
assert_vault_config_is_indented_and_line_broken(result.stdout) |
| 1625 | 1532 |
|
| 1533 |
+ @Parametrize.VALID_TEST_CONFIGS |
|
| 1534 |
+ def test_normal_config( |
|
| 1535 |
+ self, |
|
| 1536 |
+ caplog: pytest.LogCaptureFixture, |
|
| 1537 |
+ config: Any, |
|
| 1538 |
+ ) -> None: |
|
| 1539 |
+ """Exporting a configuration works.""" |
|
| 1540 |
+ self._test(caplog=caplog, config=config, use_import=False) |
|
| 1541 |
+ |
|
| 1626 | 1542 |
@hypothesis.settings( |
| 1627 | 1543 |
suppress_health_check=[ |
| 1628 | 1544 |
*hypothesis.settings().suppress_health_check, |
| ... | ... |
@@ -1646,52 +1562,12 @@ class TestExportConfigValid: |
| 1646 | 1562 |
Tested via hypothesis. |
| 1647 | 1563 |
|
| 1648 | 1564 |
""" |
| 1649 |
- config = conf.config |
|
| 1650 |
- config2 = copy.deepcopy(config) |
|
| 1651 |
- _types.clean_up_falsy_vault_config_values(config2) |
|
| 1652 | 1565 |
# Reset caplog between hypothesis runs. |
| 1653 | 1566 |
caplog.clear() |
| 1654 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1655 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1656 |
- # with-statements. |
|
| 1657 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1658 |
- with contextlib.ExitStack() as stack: |
|
| 1659 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1660 |
- stack.enter_context( |
|
| 1661 |
- pytest_machinery.isolated_vault_config( |
|
| 1662 |
- monkeypatch=monkeypatch, |
|
| 1663 |
- runner=runner, |
|
| 1664 |
- vault_config={"services": {}},
|
|
| 1665 |
- ) |
|
| 1666 |
- ) |
|
| 1667 |
- result1 = runner.invoke( |
|
| 1668 |
- cli.derivepassphrase_vault, |
|
| 1669 |
- ["--import", "-"], |
|
| 1670 |
- input=json.dumps(config), |
|
| 1671 |
- catch_exceptions=False, |
|
| 1672 |
- ) |
|
| 1673 |
- assert result1.clean_exit(empty_stderr=False), ( |
|
| 1674 |
- "expected clean exit" |
|
| 1675 |
- ) |
|
| 1676 |
- assert not result1.stderr or all( |
|
| 1677 |
- map(is_harmless_config_import_warning, caplog.record_tuples) |
|
| 1678 |
- ), "unexpected error output" |
|
| 1679 |
- result2 = runner.invoke( |
|
| 1680 |
- cli.derivepassphrase_vault, |
|
| 1681 |
- ["--export", "-"], |
|
| 1682 |
- catch_exceptions=False, |
|
| 1683 |
- ) |
|
| 1684 |
- assert result2.clean_exit(empty_stderr=False), ( |
|
| 1685 |
- "expected clean exit" |
|
| 1686 |
- ) |
|
| 1687 |
- assert not result2.stderr or all( |
|
| 1688 |
- map(is_harmless_config_import_warning, caplog.record_tuples) |
|
| 1689 |
- ), "unexpected error output" |
|
| 1690 |
- config3 = json.loads(result2.stdout) |
|
| 1691 |
- assert config3 == config2, "config not exported correctly" |
|
| 1567 |
+ self._test(caplog=caplog, config=conf.config, use_import=True) |
|
| 1692 | 1568 |
|
| 1693 | 1569 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
| 1694 |
- def test_export_config_no_stored_settings( |
|
| 1570 |
+ def test_no_stored_settings( |
|
| 1695 | 1571 |
self, |
| 1696 | 1572 |
export_options: list[str], |
| 1697 | 1573 |
) -> None: |
| ... | ... |
@@ -1721,17 +1597,23 @@ class TestExportConfigValid: |
| 1721 | 1597 |
catch_exceptions=False, |
| 1722 | 1598 |
) |
| 1723 | 1599 |
assert result.clean_exit(empty_stderr=True), "expected clean exit" |
| 1600 |
+ assert result.stdout.startswith("#!") or json.loads(result.stdout) == {
|
|
| 1601 |
+ "services": {}
|
|
| 1602 |
+ } |
|
| 1724 | 1603 |
|
| 1725 | 1604 |
|
| 1726 | 1605 |
class TestExportConfigInvalid: |
| 1727 | 1606 |
"""Tests concerning `vault` configuration exports: invalid exports.""" |
| 1728 | 1607 |
|
| 1729 |
- @Parametrize.EXPORT_FORMAT_OPTIONS |
|
| 1730 |
- def test_export_config_bad_stored_config( |
|
| 1608 |
+ @contextlib.contextmanager |
|
| 1609 |
+ def _test( |
|
| 1731 | 1610 |
self, |
| 1732 |
- export_options: list[str], |
|
| 1733 |
- ) -> None: |
|
| 1734 |
- """Exporting an invalid config fails.""" |
|
| 1611 |
+ command_line: list[str], |
|
| 1612 |
+ /, |
|
| 1613 |
+ *, |
|
| 1614 |
+ config: _types.VaultConfig = {"services": {}}, # noqa: B006
|
|
| 1615 |
+ error_messages: tuple[str, ...] = (), |
|
| 1616 |
+ ) -> Iterator[list[str]]: |
|
| 1735 | 1617 |
runner = machinery.CliRunner(mix_stderr=False) |
| 1736 | 1618 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 1737 | 1619 |
# with-statements. |
| ... | ... |
@@ -1742,261 +1624,219 @@ class TestExportConfigInvalid: |
| 1742 | 1624 |
pytest_machinery.isolated_vault_config( |
| 1743 | 1625 |
monkeypatch=monkeypatch, |
| 1744 | 1626 |
runner=runner, |
| 1745 |
- vault_config={},
|
|
| 1627 |
+ vault_config=config, |
|
| 1746 | 1628 |
) |
| 1747 | 1629 |
) |
| 1630 |
+ yield command_line |
|
| 1748 | 1631 |
result = runner.invoke( |
| 1749 | 1632 |
cli.derivepassphrase_vault, |
| 1750 |
- ["--export", "-", *export_options], |
|
| 1633 |
+ command_line, |
|
| 1751 | 1634 |
input="null", |
| 1752 | 1635 |
catch_exceptions=False, |
| 1753 | 1636 |
) |
| 1754 |
- assert result.error_exit(error="Cannot load vault settings:"), ( |
|
| 1637 |
+ assert any([result.error_exit(error=msg) for msg in error_messages]), ( |
|
| 1755 | 1638 |
"expected error exit and known error message" |
| 1756 | 1639 |
) |
| 1757 | 1640 |
|
| 1758 | 1641 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
| 1759 |
- def test_export_config_not_a_file( |
|
| 1642 |
+ def test_bad_stored_config( |
|
| 1760 | 1643 |
self, |
| 1761 | 1644 |
export_options: list[str], |
| 1762 | 1645 |
) -> None: |
| 1763 | 1646 |
"""Exporting an invalid config fails.""" |
| 1764 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1765 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1766 |
- # with-statements. |
|
| 1767 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1768 |
- with contextlib.ExitStack() as stack: |
|
| 1769 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1770 |
- stack.enter_context( |
|
| 1771 |
- pytest_machinery.isolated_config( |
|
| 1772 |
- monkeypatch=monkeypatch, |
|
| 1773 |
- runner=runner, |
|
| 1774 |
- ) |
|
| 1775 |
- ) |
|
| 1647 |
+ with self._test( |
|
| 1648 |
+ ["--export", "-", *export_options], |
|
| 1649 |
+ config=None, # type: ignore[arg-type,typeddict-item] |
|
| 1650 |
+ error_messages=("Cannot load vault settings:",),
|
|
| 1651 |
+ ): |
|
| 1652 |
+ pass |
|
| 1653 |
+ |
|
| 1654 |
+ @Parametrize.EXPORT_FORMAT_OPTIONS |
|
| 1655 |
+ def test_not_a_file( |
|
| 1656 |
+ self, |
|
| 1657 |
+ export_options: list[str], |
|
| 1658 |
+ ) -> None: |
|
| 1659 |
+ """Exporting an invalid config fails.""" |
|
| 1660 |
+ with self._test( |
|
| 1661 |
+ ["--export", "-", *export_options], |
|
| 1662 |
+ error_messages=("Cannot load vault settings:",),
|
|
| 1663 |
+ ): |
|
| 1776 | 1664 |
config_file = cli_helpers.config_filename(subsystem="vault") |
| 1777 | 1665 |
config_file.unlink(missing_ok=True) |
| 1778 | 1666 |
config_file.mkdir(parents=True, exist_ok=True) |
| 1779 |
- result = runner.invoke( |
|
| 1780 |
- cli.derivepassphrase_vault, |
|
| 1781 |
- ["--export", "-", *export_options], |
|
| 1782 |
- input="null", |
|
| 1783 |
- catch_exceptions=False, |
|
| 1784 |
- ) |
|
| 1785 |
- assert result.error_exit(error="Cannot load vault settings:"), ( |
|
| 1786 |
- "expected error exit and known error message" |
|
| 1787 |
- ) |
|
| 1788 | 1667 |
|
| 1789 | 1668 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
| 1790 |
- def test_export_config_target_not_a_file( |
|
| 1669 |
+ def test_target_not_a_file( |
|
| 1791 | 1670 |
self, |
| 1792 | 1671 |
export_options: list[str], |
| 1793 | 1672 |
) -> None: |
| 1794 | 1673 |
"""Exporting an invalid config fails.""" |
| 1795 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1796 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1797 |
- # with-statements. |
|
| 1798 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1799 |
- with contextlib.ExitStack() as stack: |
|
| 1800 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1801 |
- stack.enter_context( |
|
| 1802 |
- pytest_machinery.isolated_config( |
|
| 1803 |
- monkeypatch=monkeypatch, |
|
| 1804 |
- runner=runner, |
|
| 1805 |
- ) |
|
| 1806 |
- ) |
|
| 1674 |
+ with self._test( |
|
| 1675 |
+ [], error_messages=("Cannot export vault settings:",)
|
|
| 1676 |
+ ) as command_line: |
|
| 1807 | 1677 |
dname = cli_helpers.config_filename(subsystem=None) |
| 1808 |
- result = runner.invoke( |
|
| 1809 |
- cli.derivepassphrase_vault, |
|
| 1810 |
- ["--export", os.fsdecode(dname), *export_options], |
|
| 1811 |
- input="null", |
|
| 1812 |
- catch_exceptions=False, |
|
| 1813 |
- ) |
|
| 1814 |
- assert result.error_exit(error="Cannot export vault settings:"), ( |
|
| 1815 |
- "expected error exit and known error message" |
|
| 1816 |
- ) |
|
| 1678 |
+ command_line[:] = ["--export", os.fsdecode(dname), *export_options] |
|
| 1817 | 1679 |
|
| 1818 | 1680 |
@pytest_machinery.skip_if_on_the_annoying_os |
| 1819 | 1681 |
@Parametrize.EXPORT_FORMAT_OPTIONS |
| 1820 |
- def test_export_config_settings_directory_not_a_directory( |
|
| 1682 |
+ def test_settings_directory_not_a_directory( |
|
| 1821 | 1683 |
self, |
| 1822 | 1684 |
export_options: list[str], |
| 1823 | 1685 |
) -> None: |
| 1824 | 1686 |
"""Exporting an invalid config fails.""" |
| 1825 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1687 |
+ with self._test( |
|
| 1688 |
+ ["--export", "-", *export_options], |
|
| 1689 |
+ error_messages=( |
|
| 1690 |
+ "Cannot load vault settings:", |
|
| 1691 |
+ "Cannot load user config:", |
|
| 1692 |
+ ), |
|
| 1693 |
+ ): |
|
| 1694 |
+ config_dir = cli_helpers.config_filename(subsystem=None) |
|
| 1695 |
+ with contextlib.suppress(FileNotFoundError): |
|
| 1696 |
+ shutil.rmtree(config_dir) |
|
| 1697 |
+ config_dir.write_text("Obstruction!!\n")
|
|
| 1698 |
+ |
|
| 1699 |
+ |
|
| 1700 |
+class TestNotesPrinting: |
|
| 1701 |
+ """Tests concerning printing the service notes.""" |
|
| 1702 |
+ |
|
| 1703 |
+ def _test( |
|
| 1704 |
+ self, |
|
| 1705 |
+ notes: str, |
|
| 1706 |
+ /, |
|
| 1707 |
+ notes_placement: Literal["before", "after"] | None = None, |
|
| 1708 |
+ placement_args: list[str] | tuple[str, ...] = (), |
|
| 1709 |
+ ) -> None: |
|
| 1710 |
+ notes_stripped = notes.strip() |
|
| 1711 |
+ maybe_notes = {"notes": notes_stripped} if notes_stripped else {}
|
|
| 1712 |
+ vault_config = {
|
|
| 1713 |
+ "global": {"phrase": DUMMY_PASSPHRASE},
|
|
| 1714 |
+ "services": {
|
|
| 1715 |
+ DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS}
|
|
| 1716 |
+ }, |
|
| 1717 |
+ } |
|
| 1718 |
+ result_phrase = DUMMY_RESULT_PASSPHRASE.decode("ascii")
|
|
| 1719 |
+ expected = ( |
|
| 1720 |
+ f"{notes_stripped}\n\n{result_phrase}\n"
|
|
| 1721 |
+ if notes_placement == "before" |
|
| 1722 |
+ else f"{result_phrase}\n\n{notes_stripped}\n\n"
|
|
| 1723 |
+ if notes_placement == "after" |
|
| 1724 |
+ else None |
|
| 1725 |
+ ) |
|
| 1726 |
+ runner = machinery.CliRunner(mix_stderr=notes_placement is not None) |
|
| 1826 | 1727 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 1827 | 1728 |
# with-statements. |
| 1828 | 1729 |
# https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
| 1829 | 1730 |
with contextlib.ExitStack() as stack: |
| 1830 | 1731 |
monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
| 1831 | 1732 |
stack.enter_context( |
| 1832 |
- pytest_machinery.isolated_config( |
|
| 1733 |
+ pytest_machinery.isolated_vault_config( |
|
| 1833 | 1734 |
monkeypatch=monkeypatch, |
| 1834 | 1735 |
runner=runner, |
| 1736 |
+ vault_config=vault_config, |
|
| 1835 | 1737 |
) |
| 1836 | 1738 |
) |
| 1837 |
- config_dir = cli_helpers.config_filename(subsystem=None) |
|
| 1838 |
- with contextlib.suppress(FileNotFoundError): |
|
| 1839 |
- shutil.rmtree(config_dir) |
|
| 1840 |
- config_dir.write_text("Obstruction!!\n")
|
|
| 1841 | 1739 |
result = runner.invoke( |
| 1842 | 1740 |
cli.derivepassphrase_vault, |
| 1843 |
- ["--export", "-", *export_options], |
|
| 1844 |
- input="null", |
|
| 1741 |
+ [*placement_args, "--", DUMMY_SERVICE], |
|
| 1845 | 1742 |
catch_exceptions=False, |
| 1846 | 1743 |
) |
| 1847 |
- assert result.error_exit( |
|
| 1848 |
- error="Cannot load vault settings:" |
|
| 1849 |
- ) or result.error_exit(error="Cannot load user config:"), ( |
|
| 1850 |
- "expected error exit and known error message" |
|
| 1851 |
- ) |
|
| 1852 |
- |
|
| 1853 |
- |
|
| 1854 |
-class TestNotesPrinting: |
|
| 1855 |
- """Tests concerning printing the service notes.""" |
|
| 1856 |
- |
|
| 1857 |
- @hypothesis.given( |
|
| 1858 |
- notes=strategies.text( |
|
| 1859 |
- strategies.characters( |
|
| 1860 |
- min_codepoint=32, |
|
| 1861 |
- max_codepoint=126, |
|
| 1862 |
- include_characters="\n", |
|
| 1863 |
- ), |
|
| 1864 |
- max_size=256, |
|
| 1865 |
- ), |
|
| 1866 |
- ) |
|
| 1867 |
- def test_service_with_notes_actually_prints_notes( |
|
| 1868 |
- self, |
|
| 1869 |
- notes: str, |
|
| 1870 |
- ) -> None: |
|
| 1871 |
- """Service notes are printed, if they exist.""" |
|
| 1872 |
- hypothesis.assume("Error:" not in notes)
|
|
| 1873 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 1874 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1875 |
- # with-statements. |
|
| 1876 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1877 |
- with contextlib.ExitStack() as stack: |
|
| 1878 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1879 |
- stack.enter_context( |
|
| 1880 |
- pytest_machinery.isolated_vault_config( |
|
| 1881 |
- monkeypatch=monkeypatch, |
|
| 1882 |
- runner=runner, |
|
| 1883 |
- vault_config={
|
|
| 1884 |
- "global": {
|
|
| 1885 |
- "phrase": DUMMY_PASSPHRASE, |
|
| 1886 |
- }, |
|
| 1887 |
- "services": {
|
|
| 1888 |
- DUMMY_SERVICE: {
|
|
| 1889 |
- "notes": notes, |
|
| 1890 |
- **DUMMY_CONFIG_SETTINGS, |
|
| 1891 |
- }, |
|
| 1892 |
- }, |
|
| 1893 |
- }, |
|
| 1894 |
- ) |
|
| 1895 |
- ) |
|
| 1896 |
- result = runner.invoke( |
|
| 1897 |
- cli.derivepassphrase_vault, |
|
| 1898 |
- ["--", DUMMY_SERVICE], |
|
| 1744 |
+ if expected is not None: |
|
| 1745 |
+ assert result.clean_exit(output=expected), ( |
|
| 1746 |
+ "expected clean exit" |
|
| 1899 | 1747 |
) |
| 1748 |
+ else: |
|
| 1900 | 1749 |
assert result.clean_exit(), "expected clean exit" |
| 1901 | 1750 |
assert result.stdout, "expected program output" |
| 1902 |
- assert result.stdout.strip() == DUMMY_RESULT_PASSPHRASE.decode( |
|
| 1903 |
- "ascii" |
|
| 1904 |
- ), "expected known program output" |
|
| 1905 |
- assert result.stderr or not notes.strip(), "expected stderr" |
|
| 1751 |
+ assert result.stdout.strip() == result_phrase, ( |
|
| 1752 |
+ "expected known program output" |
|
| 1753 |
+ ) |
|
| 1754 |
+ assert result.stderr or not notes_stripped, "expected stderr" |
|
| 1906 | 1755 |
assert "Error:" not in result.stderr, ( |
| 1907 | 1756 |
"expected no error messages on stderr" |
| 1908 | 1757 |
) |
| 1909 |
- assert result.stderr.strip() == notes.strip(), ( |
|
| 1758 |
+ assert result.stderr.strip() == notes_stripped, ( |
|
| 1910 | 1759 |
"expected known stderr contents" |
| 1911 | 1760 |
) |
| 1912 | 1761 |
|
| 1762 |
+ @hypothesis.given(notes=Strategies.notes().filter(str.strip)) |
|
| 1763 |
+ def test_service_with_notes_actually_prints_notes( |
|
| 1764 |
+ self, |
|
| 1765 |
+ notes: str, |
|
| 1766 |
+ ) -> None: |
|
| 1767 |
+ """Service notes are printed, if they exist.""" |
|
| 1768 |
+ hypothesis.assume("Error:" not in notes)
|
|
| 1769 |
+ self._test(notes, notes_placement=None, placement_args=()) |
|
| 1770 |
+ |
|
| 1913 | 1771 |
@Parametrize.NOTES_PLACEMENT |
| 1914 |
- @hypothesis.given( |
|
| 1915 |
- notes=strategies.text( |
|
| 1916 |
- strategies.characters( |
|
| 1917 |
- min_codepoint=32, max_codepoint=126, include_characters="\n" |
|
| 1918 |
- ), |
|
| 1919 |
- min_size=1, |
|
| 1920 |
- max_size=512, |
|
| 1921 |
- ).filter(str.strip), |
|
| 1922 |
- ) |
|
| 1772 |
+ @hypothesis.given(notes=Strategies.notes().filter(str.strip)) |
|
| 1923 | 1773 |
def test_notes_placement( |
| 1924 | 1774 |
self, |
| 1925 | 1775 |
notes_placement: Literal["before", "after"], |
| 1926 | 1776 |
placement_args: list[str], |
| 1927 | 1777 |
notes: str, |
| 1928 | 1778 |
) -> None: |
| 1929 |
- notes = notes.strip() |
|
| 1930 |
- maybe_notes = {"notes": notes} if notes else {}
|
|
| 1931 |
- vault_config = {
|
|
| 1932 |
- "global": {"phrase": DUMMY_PASSPHRASE},
|
|
| 1933 |
- "services": {
|
|
| 1934 |
- DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS}
|
|
| 1935 |
- }, |
|
| 1936 |
- } |
|
| 1937 |
- result_phrase = DUMMY_RESULT_PASSPHRASE.decode("ascii")
|
|
| 1938 |
- expected = ( |
|
| 1939 |
- f"{notes}\n\n{result_phrase}\n"
|
|
| 1940 |
- if notes_placement == "before" |
|
| 1941 |
- else f"{result_phrase}\n\n{notes}\n\n"
|
|
| 1779 |
+ self._test( |
|
| 1780 |
+ notes, |
|
| 1781 |
+ notes_placement=notes_placement, |
|
| 1782 |
+ placement_args=placement_args, |
|
| 1942 | 1783 |
) |
| 1943 |
- runner = machinery.CliRunner(mix_stderr=True) |
|
| 1944 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 1945 |
- # with-statements. |
|
| 1946 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 1947 |
- with contextlib.ExitStack() as stack: |
|
| 1948 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 1949 |
- stack.enter_context( |
|
| 1950 |
- pytest_machinery.isolated_vault_config( |
|
| 1951 |
- monkeypatch=monkeypatch, |
|
| 1952 |
- runner=runner, |
|
| 1953 |
- vault_config=vault_config, |
|
| 1954 |
- ) |
|
| 1955 |
- ) |
|
| 1956 |
- result = runner.invoke( |
|
| 1957 |
- cli.derivepassphrase_vault, |
|
| 1958 |
- [*placement_args, "--", DUMMY_SERVICE], |
|
| 1959 |
- catch_exceptions=False, |
|
| 1960 |
- ) |
|
| 1961 |
- assert result.clean_exit(output=expected), "expected clean exit" |
|
| 1962 | 1784 |
|
| 1963 | 1785 |
|
| 1964 |
-class TestNotesEditingValid: |
|
| 1965 |
- """Tests concerning editing service notes: valid calls.""" |
|
| 1786 |
+class TestNotesEditing: |
|
| 1787 |
+ """Superclass for tests concerning editing service notes.""" |
|
| 1966 | 1788 |
|
| 1967 |
- @Parametrize.MODERN_EDITOR_INTERFACE |
|
| 1968 |
- @hypothesis.settings( |
|
| 1969 |
- suppress_health_check=[ |
|
| 1970 |
- *hypothesis.settings().suppress_health_check, |
|
| 1971 |
- hypothesis.HealthCheck.function_scoped_fixture, |
|
| 1972 |
- ], |
|
| 1789 |
+ CURRENT_NOTES = "Contents go here" |
|
| 1790 |
+ OLD_NOTES_TEXT = ( |
|
| 1791 |
+ "These backup notes are left over from the previous session." |
|
| 1973 | 1792 |
) |
| 1974 |
- @hypothesis.given( |
|
| 1975 |
- notes=strategies.text( |
|
| 1976 |
- strategies.characters( |
|
| 1977 |
- min_codepoint=32, max_codepoint=126, include_characters="\n" |
|
| 1978 |
- ), |
|
| 1979 |
- min_size=1, |
|
| 1980 |
- max_size=512, |
|
| 1981 |
- ).filter(str.strip), |
|
| 1982 |
- ) |
|
| 1983 |
- def test_successful_edit( |
|
| 1793 |
+ |
|
| 1794 |
+ def _calculate_expected_contents( |
|
| 1984 | 1795 |
self, |
| 1985 |
- caplog: pytest.LogCaptureFixture, |
|
| 1796 |
+ final_notes: str, |
|
| 1797 |
+ /, |
|
| 1798 |
+ *, |
|
| 1986 | 1799 |
modern_editor_interface: bool, |
| 1987 |
- notes: str, |
|
| 1988 |
- ) -> None: |
|
| 1989 |
- """Editing notes works.""" |
|
| 1990 |
- marker = cli_messages.TranslatedString( |
|
| 1991 |
- cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER |
|
| 1800 |
+ current_notes: str | None = CURRENT_NOTES, |
|
| 1801 |
+ old_notes_text: str | None = OLD_NOTES_TEXT, |
|
| 1802 |
+ ) -> tuple[str, str]: |
|
| 1803 |
+ current_notes = current_notes or "" |
|
| 1804 |
+ old_notes_text = old_notes_text or "" |
|
| 1805 |
+ # For the modern editor interface, the notes change if and only |
|
| 1806 |
+ # if the notes change to a different, non-empty string. There |
|
| 1807 |
+ # are no backup notes, so we return the old ones (which may be |
|
| 1808 |
+ # synthetic) unchanged. |
|
| 1809 |
+ if modern_editor_interface: |
|
| 1810 |
+ return old_notes_text.strip(), ( |
|
| 1811 |
+ final_notes.strip() |
|
| 1812 |
+ if final_notes.strip() |
|
| 1813 |
+ and final_notes.strip() != current_notes.strip() |
|
| 1814 |
+ else current_notes.strip() |
|
| 1815 |
+ ) |
|
| 1816 |
+ # For the legacy editor interface, the notes and the backup |
|
| 1817 |
+ # notes change if and only if the new notes differ from the |
|
| 1818 |
+ # previous notes. |
|
| 1819 |
+ return ( |
|
| 1820 |
+ (current_notes.strip(), final_notes.strip()) |
|
| 1821 |
+ if final_notes.strip() != current_notes.strip() |
|
| 1822 |
+ else (old_notes_text.strip(), current_notes.strip()) |
|
| 1823 |
+ ) |
|
| 1824 |
+ |
|
| 1825 |
+ def _test( |
|
| 1826 |
+ self, |
|
| 1827 |
+ edit_result: str, |
|
| 1828 |
+ /, |
|
| 1829 |
+ *, |
|
| 1830 |
+ modern_editor_interface: bool, |
|
| 1831 |
+ current_notes: str | None = CURRENT_NOTES, |
|
| 1832 |
+ old_notes_text: str | None = OLD_NOTES_TEXT, |
|
| 1833 |
+ ) -> tuple[machinery.ReadableResult, str, _types.VaultConfig]: |
|
| 1834 |
+ if hypothesis.currently_in_test_context(): # pragma: no branch |
|
| 1835 |
+ hypothesis.note(f"{edit_result = }")
|
|
| 1836 |
+ hypothesis.note(f"{modern_editor_interface = }")
|
|
| 1837 |
+ hypothesis.note( |
|
| 1838 |
+ f"vault_config = {self._vault_config(current_notes or '')}"
|
|
| 1992 | 1839 |
) |
| 1993 |
- edit_result = f""" |
|
| 1994 |
- |
|
| 1995 |
-{marker}
|
|
| 1996 |
-{notes}
|
|
| 1997 |
-""" |
|
| 1998 |
- # Reset caplog between hypothesis runs. |
|
| 1999 |
- caplog.clear() |
|
| 2000 | 1840 |
runner = machinery.CliRunner(mix_stderr=False) |
| 2001 | 1841 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2002 | 1842 |
# with-statements. |
| ... | ... |
@@ -2007,18 +1847,15 @@ class TestNotesEditingValid: |
| 2007 | 1847 |
pytest_machinery.isolated_vault_config( |
| 2008 | 1848 |
monkeypatch=monkeypatch, |
| 2009 | 1849 |
runner=runner, |
| 2010 |
- vault_config={
|
|
| 2011 |
- "global": {"phrase": "abc"},
|
|
| 2012 |
- "services": {"sv": {"notes": "Contents go here"}},
|
|
| 2013 |
- }, |
|
| 1850 |
+ vault_config=self._vault_config(current_notes or ""), |
|
| 2014 | 1851 |
) |
| 2015 | 1852 |
) |
| 2016 | 1853 |
notes_backup_file = cli_helpers.config_filename( |
| 2017 | 1854 |
subsystem="notes backup" |
| 2018 | 1855 |
) |
| 1856 |
+ if old_notes_text and old_notes_text.strip(): # pragma: no branch |
|
| 2019 | 1857 |
notes_backup_file.write_text( |
| 2020 |
- "These backup notes are left over from the previous session.", |
|
| 2021 |
- encoding="UTF-8", |
|
| 1858 |
+ old_notes_text.strip(), encoding="UTF-8" |
|
| 2022 | 1859 |
) |
| 2023 | 1860 |
monkeypatch.setattr(click, "edit", lambda *_a, **_kw: edit_result) |
| 2024 | 1861 |
result = runner.invoke( |
| ... | ... |
@@ -2034,113 +1871,217 @@ class TestNotesEditingValid: |
| 2034 | 1871 |
], |
| 2035 | 1872 |
catch_exceptions=False, |
| 2036 | 1873 |
) |
| 1874 |
+ backup_contents = notes_backup_file.read_text(encoding="UTF-8") |
|
| 1875 |
+ with cli_helpers.config_filename(subsystem="vault").open( |
|
| 1876 |
+ encoding="UTF-8" |
|
| 1877 |
+ ) as infile: |
|
| 1878 |
+ config = json.load(infile) |
|
| 1879 |
+ if hypothesis.currently_in_test_context(): # pragma: no branch |
|
| 1880 |
+ hypothesis.note(f"{result = }")
|
|
| 1881 |
+ hypothesis.note(f"{backup_contents = }")
|
|
| 1882 |
+ hypothesis.note(f"{config = }")
|
|
| 1883 |
+ return result, backup_contents, config |
|
| 1884 |
+ |
|
| 1885 |
+ def _assert_noop_exit( |
|
| 1886 |
+ self, |
|
| 1887 |
+ result: machinery.ReadableResult, |
|
| 1888 |
+ /, |
|
| 1889 |
+ *, |
|
| 1890 |
+ modern_editor_interface: bool = False, |
|
| 1891 |
+ ) -> None: |
|
| 1892 |
+ # We do not distinguish between aborts and no-op edits. Aborts |
|
| 1893 |
+ # are treated as failures (error exit), and thus tested |
|
| 1894 |
+ # specifically in a different class. |
|
| 1895 |
+ if modern_editor_interface: |
|
| 1896 |
+ assert result.error_exit( |
|
| 1897 |
+ error="the user aborted the request" |
|
| 1898 |
+ ) or result.clean_exit(empty_stderr=True), "expected clean exit" |
|
| 1899 |
+ else: |
|
| 1900 |
+ assert result.clean_exit(empty_stderr=False), "expected clean exit" |
|
| 1901 |
+ |
|
| 1902 |
+ def _assert_normal_exit(self, result: machinery.ReadableResult) -> None: |
|
| 2037 | 1903 |
assert result.clean_exit(), "expected clean exit" |
| 2038 | 1904 |
assert all(map(is_warning_line, result.stderr.splitlines(True))) |
| 2039 |
- assert modern_editor_interface or machinery.warning_emitted( |
|
| 2040 |
- "A backup copy of the old notes was saved", |
|
| 2041 |
- caplog.record_tuples, |
|
| 2042 |
- ), "expected known warning message in stderr" |
|
| 1905 |
+ |
|
| 1906 |
+ def _assert_notes_backup_warning( |
|
| 1907 |
+ self, |
|
| 1908 |
+ caplog: pytest.LogCaptureFixture, |
|
| 1909 |
+ /, |
|
| 1910 |
+ *, |
|
| 1911 |
+ modern_editor_interface: bool, |
|
| 1912 |
+ notes_unchanged: bool = False, |
|
| 1913 |
+ ) -> None: |
|
| 2043 | 1914 |
assert ( |
| 2044 | 1915 |
modern_editor_interface |
| 2045 |
- or notes_backup_file.read_text(encoding="UTF-8") |
|
| 2046 |
- == "Contents go here" |
|
| 1916 |
+ or notes_unchanged |
|
| 1917 |
+ or machinery.warning_emitted( |
|
| 1918 |
+ "A backup copy of the old notes was saved", |
|
| 1919 |
+ caplog.record_tuples, |
|
| 2047 | 1920 |
) |
| 2048 |
- with cli_helpers.config_filename(subsystem="vault").open( |
|
| 2049 |
- encoding="UTF-8" |
|
| 2050 |
- ) as infile: |
|
| 2051 |
- config = json.load(infile) |
|
| 2052 |
- assert config == {
|
|
| 1921 |
+ ), "expected known warning message on stderr" |
|
| 1922 |
+ |
|
| 1923 |
+ def _assert_notes_and_backup_notes( |
|
| 1924 |
+ self, |
|
| 1925 |
+ /, |
|
| 1926 |
+ *, |
|
| 1927 |
+ final_notes: str, |
|
| 1928 |
+ new_backup_notes: str, |
|
| 1929 |
+ new_config: _types.VaultConfig, |
|
| 1930 |
+ modern_editor_interface: bool, |
|
| 1931 |
+ current_notes: str | None = CURRENT_NOTES, |
|
| 1932 |
+ old_notes_text: str | None = OLD_NOTES_TEXT, |
|
| 1933 |
+ ) -> None: |
|
| 1934 |
+ if hypothesis.currently_in_test_context(): # pragma: no branch |
|
| 1935 |
+ hypothesis.note(f"{final_notes = }")
|
|
| 1936 |
+ hypothesis.note(f"{current_notes = }")
|
|
| 1937 |
+ expected_backup_notes, expected_notes = ( |
|
| 1938 |
+ self._calculate_expected_contents( |
|
| 1939 |
+ final_notes, |
|
| 1940 |
+ modern_editor_interface=modern_editor_interface, |
|
| 1941 |
+ current_notes=current_notes, |
|
| 1942 |
+ old_notes_text=old_notes_text, |
|
| 1943 |
+ ) |
|
| 1944 |
+ ) |
|
| 1945 |
+ expected_config = self._vault_config(expected_notes) |
|
| 1946 |
+ assert new_config == expected_config |
|
| 1947 |
+ assert new_backup_notes == expected_backup_notes |
|
| 1948 |
+ |
|
| 1949 |
+ @staticmethod |
|
| 1950 |
+ def _vault_config( |
|
| 1951 |
+ starting_notes: str = CURRENT_NOTES, / |
|
| 1952 |
+ ) -> _types.VaultConfig: |
|
| 1953 |
+ return {
|
|
| 2053 | 1954 |
"global": {"phrase": "abc"},
|
| 2054 | 1955 |
"services": {
|
| 2055 |
- "sv": {
|
|
| 2056 |
- "notes": notes.strip() |
|
| 2057 |
- if modern_editor_interface |
|
| 2058 |
- else edit_result.strip() |
|
| 2059 |
- } |
|
| 1956 |
+ "sv": {"notes": starting_notes.strip()}
|
|
| 1957 |
+ if starting_notes.strip() |
|
| 1958 |
+ else {}
|
|
| 2060 | 1959 |
}, |
| 2061 | 1960 |
} |
| 2062 | 1961 |
|
| 2063 |
- @Parametrize.NOOP_EDIT_FUNCS |
|
| 1962 |
+ class ExtraArgs(TypedDict): |
|
| 1963 |
+ modern_editor_interface: bool |
|
| 1964 |
+ current_notes: NotRequired[str] |
|
| 1965 |
+ old_notes_text: NotRequired[str] |
|
| 1966 |
+ |
|
| 1967 |
+ |
|
| 1968 |
+class TestNotesEditingValid(TestNotesEditing): |
|
| 1969 |
+ """Tests concerning editing service notes: valid calls.""" |
|
| 1970 |
+ |
|
| 1971 |
+ @Parametrize.MODERN_EDITOR_INTERFACE |
|
| 1972 |
+ @hypothesis.settings( |
|
| 1973 |
+ suppress_health_check=[ |
|
| 1974 |
+ *hypothesis.settings().suppress_health_check, |
|
| 1975 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
| 1976 |
+ ], |
|
| 1977 |
+ ) |
|
| 2064 | 1978 |
@hypothesis.given( |
| 2065 |
- notes=strategies.text( |
|
| 2066 |
- strategies.characters( |
|
| 2067 |
- min_codepoint=32, max_codepoint=126, include_characters="\n" |
|
| 2068 |
- ), |
|
| 2069 |
- min_size=1, |
|
| 2070 |
- max_size=512, |
|
| 2071 |
- ).filter(str.strip), |
|
| 1979 |
+ notes=Strategies.notes() |
|
| 1980 |
+ .filter(str.strip) |
|
| 1981 |
+ .filter(lambda notes: notes != TestNotesEditingValid.CURRENT_NOTES) |
|
| 2072 | 1982 |
) |
| 2073 |
- def test_noop_edit( |
|
| 1983 |
+ @hypothesis.example(TestNotesEditing.CURRENT_NOTES) |
|
| 1984 |
+ def test_successful_edit( |
|
| 2074 | 1985 |
self, |
| 2075 |
- edit_func_name: Literal["empty", "space"], |
|
| 1986 |
+ caplog: pytest.LogCaptureFixture, |
|
| 2076 | 1987 |
modern_editor_interface: bool, |
| 2077 | 1988 |
notes: str, |
| 2078 | 1989 |
) -> None: |
| 2079 |
- """Abandoning edited notes works.""" |
|
| 1990 |
+ """Editing notes works.""" |
|
| 1991 |
+ # Reset caplog between hypothesis runs. |
|
| 1992 |
+ caplog.clear() |
|
| 1993 |
+ marker = cli_messages.TranslatedString( |
|
| 1994 |
+ cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER |
|
| 1995 |
+ ) |
|
| 1996 |
+ edit_result = ( |
|
| 1997 |
+ f""" |
|
| 2080 | 1998 |
|
| 2081 |
- def empty(text: str, *_args: Any, **_kwargs: Any) -> str: |
|
| 2082 |
- del text |
|
| 2083 |
- return "" |
|
| 1999 |
+{marker}
|
|
| 2000 |
+{notes}
|
|
| 2001 |
+""" |
|
| 2002 |
+ if modern_editor_interface |
|
| 2003 |
+ else notes.strip() |
|
| 2004 |
+ ) |
|
| 2084 | 2005 |
|
| 2085 |
- def space(text: str, *_args: Any, **_kwargs: Any) -> str: |
|
| 2086 |
- del text |
|
| 2087 |
- return " " + notes.strip() + "\n\n\n\n\n\n" |
|
| 2006 |
+ extra_args: TestNotesEditing.ExtraArgs = {
|
|
| 2007 |
+ "modern_editor_interface": modern_editor_interface, |
|
| 2008 |
+ "current_notes": self.CURRENT_NOTES, |
|
| 2009 |
+ "old_notes_text": self.OLD_NOTES_TEXT, |
|
| 2010 |
+ } |
|
| 2011 |
+ notes_unchanged = notes.strip() == extra_args["current_notes"].strip() |
|
| 2088 | 2012 |
|
| 2089 |
- edit_funcs = {"empty": empty, "space": space}
|
|
| 2090 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2091 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2092 |
- # with-statements. |
|
| 2093 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2094 |
- with contextlib.ExitStack() as stack: |
|
| 2095 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2096 |
- stack.enter_context( |
|
| 2097 |
- pytest_machinery.isolated_vault_config( |
|
| 2098 |
- monkeypatch=monkeypatch, |
|
| 2099 |
- runner=runner, |
|
| 2100 |
- vault_config={
|
|
| 2101 |
- "global": {"phrase": "abc"},
|
|
| 2102 |
- "services": {"sv": {"notes": notes.strip()}},
|
|
| 2103 |
- }, |
|
| 2013 |
+ result, new_backup_notes, new_config = self._test( |
|
| 2014 |
+ edit_result, **extra_args |
|
| 2104 | 2015 |
) |
| 2016 |
+ self._assert_normal_exit(result) |
|
| 2017 |
+ self._assert_notes_and_backup_notes( |
|
| 2018 |
+ final_notes=notes, |
|
| 2019 |
+ new_backup_notes=new_backup_notes, |
|
| 2020 |
+ new_config=new_config, |
|
| 2021 |
+ **extra_args, |
|
| 2105 | 2022 |
) |
| 2106 |
- notes_backup_file = cli_helpers.config_filename( |
|
| 2107 |
- subsystem="notes backup" |
|
| 2108 |
- ) |
|
| 2109 |
- notes_backup_file.write_text( |
|
| 2110 |
- "These backup notes are left over from the previous session.", |
|
| 2111 |
- encoding="UTF-8", |
|
| 2023 |
+ self._assert_notes_backup_warning( |
|
| 2024 |
+ caplog, |
|
| 2025 |
+ modern_editor_interface=modern_editor_interface, |
|
| 2026 |
+ notes_unchanged=notes_unchanged, |
|
| 2112 | 2027 |
) |
| 2113 |
- monkeypatch.setattr(click, "edit", edit_funcs[edit_func_name]) |
|
| 2114 |
- result = runner.invoke( |
|
| 2115 |
- cli.derivepassphrase_vault, |
|
| 2116 |
- [ |
|
| 2117 |
- "--config", |
|
| 2118 |
- "--notes", |
|
| 2119 |
- "--modern-editor-interface" |
|
| 2120 |
- if modern_editor_interface |
|
| 2121 |
- else "--vault-legacy-editor-interface", |
|
| 2122 |
- "--", |
|
| 2123 |
- "sv", |
|
| 2028 |
+ |
|
| 2029 |
+ @Parametrize.MODERN_EDITOR_INTERFACE |
|
| 2030 |
+ @hypothesis.settings( |
|
| 2031 |
+ suppress_health_check=[ |
|
| 2032 |
+ *hypothesis.settings().suppress_health_check, |
|
| 2033 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
| 2124 | 2034 |
], |
| 2125 |
- catch_exceptions=False, |
|
| 2126 | 2035 |
) |
| 2127 |
- assert result.clean_exit(empty_stderr=True) or result.error_exit( |
|
| 2128 |
- error="the user aborted the request" |
|
| 2129 |
- ), "expected clean exit" |
|
| 2130 |
- assert ( |
|
| 2131 |
- modern_editor_interface |
|
| 2132 |
- or notes_backup_file.read_text(encoding="UTF-8") |
|
| 2133 |
- == "These backup notes are left over from the previous session." |
|
| 2036 |
+ @hypothesis.given(notes=Strategies.notes().filter(str.strip)) |
|
| 2037 |
+ @hypothesis.example(TestNotesEditing.CURRENT_NOTES) |
|
| 2038 |
+ def test_noop_edit( |
|
| 2039 |
+ self, |
|
| 2040 |
+ caplog: pytest.LogCaptureFixture, |
|
| 2041 |
+ modern_editor_interface: bool, |
|
| 2042 |
+ notes: str, |
|
| 2043 |
+ ) -> None: |
|
| 2044 |
+ """No-op editing existing notes works. |
|
| 2045 |
+ |
|
| 2046 |
+ The notes are unchanged, and the command-line interface does not |
|
| 2047 |
+ report an abort. For the legacy editor interface, the backup |
|
| 2048 |
+ notes are unchanged as well. |
|
| 2049 |
+ |
|
| 2050 |
+ """ |
|
| 2051 |
+ # Reset caplog between hypothesis runs. |
|
| 2052 |
+ caplog.clear() |
|
| 2053 |
+ marker = cli_messages.TranslatedString( |
|
| 2054 |
+ cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER |
|
| 2134 | 2055 |
) |
| 2135 |
- with cli_helpers.config_filename(subsystem="vault").open( |
|
| 2136 |
- encoding="UTF-8" |
|
| 2137 |
- ) as infile: |
|
| 2138 |
- config = json.load(infile) |
|
| 2139 |
- assert config == {
|
|
| 2140 |
- "global": {"phrase": "abc"},
|
|
| 2141 |
- "services": {"sv": {"notes": notes.strip()}},
|
|
| 2056 |
+ edit_result = (f"{marker}\n" if modern_editor_interface else "") + (
|
|
| 2057 |
+ " " * 6 + notes + "\n" * 6 |
|
| 2058 |
+ ) |
|
| 2059 |
+ |
|
| 2060 |
+ extra_args: TestNotesEditing.ExtraArgs = {
|
|
| 2061 |
+ "modern_editor_interface": modern_editor_interface, |
|
| 2062 |
+ "current_notes": notes.strip(), |
|
| 2063 |
+ "old_notes_text": self.OLD_NOTES_TEXT, |
|
| 2142 | 2064 |
} |
| 2143 | 2065 |
|
| 2066 |
+ result, new_backup_notes, new_config = self._test( |
|
| 2067 |
+ edit_result, **extra_args |
|
| 2068 |
+ ) |
|
| 2069 |
+ self._assert_noop_exit( |
|
| 2070 |
+ result, |
|
| 2071 |
+ modern_editor_interface=modern_editor_interface, |
|
| 2072 |
+ ) |
|
| 2073 |
+ self._assert_notes_and_backup_notes( |
|
| 2074 |
+ final_notes=notes, |
|
| 2075 |
+ new_backup_notes=new_backup_notes, |
|
| 2076 |
+ new_config=new_config, |
|
| 2077 |
+ **extra_args, |
|
| 2078 |
+ ) |
|
| 2079 |
+ self._assert_notes_backup_warning( |
|
| 2080 |
+ caplog, |
|
| 2081 |
+ modern_editor_interface=modern_editor_interface, |
|
| 2082 |
+ notes_unchanged=True, |
|
| 2083 |
+ ) |
|
| 2084 |
+ |
|
| 2144 | 2085 |
# TODO(the-13th-letter): Keep this behavior or not, with or without |
| 2145 | 2086 |
# warning? |
| 2146 | 2087 |
@Parametrize.MODERN_EDITOR_INTERFACE |
| ... | ... |
@@ -2150,15 +2091,7 @@ class TestNotesEditingValid: |
| 2150 | 2091 |
hypothesis.HealthCheck.function_scoped_fixture, |
| 2151 | 2092 |
], |
| 2152 | 2093 |
) |
| 2153 |
- @hypothesis.given( |
|
| 2154 |
- notes=strategies.text( |
|
| 2155 |
- strategies.characters( |
|
| 2156 |
- min_codepoint=32, max_codepoint=126, include_characters="\n" |
|
| 2157 |
- ), |
|
| 2158 |
- min_size=1, |
|
| 2159 |
- max_size=512, |
|
| 2160 |
- ).filter(str.strip), |
|
| 2161 |
- ) |
|
| 2094 |
+ @hypothesis.given(notes=Strategies.notes().filter(str.strip)) |
|
| 2162 | 2095 |
def test_marker_removed( |
| 2163 | 2096 |
self, |
| 2164 | 2097 |
caplog: pytest.LogCaptureFixture, |
| ... | ... |
@@ -2176,175 +2109,66 @@ class TestNotesEditingValid: |
| 2176 | 2109 |
hypothesis.assume(str(notes_marker) not in notes.strip()) |
| 2177 | 2110 |
# Reset caplog between hypothesis runs. |
| 2178 | 2111 |
caplog.clear() |
| 2179 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2180 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2181 |
- # with-statements. |
|
| 2182 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2183 |
- with contextlib.ExitStack() as stack: |
|
| 2184 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2185 |
- stack.enter_context( |
|
| 2186 |
- pytest_machinery.isolated_vault_config( |
|
| 2187 |
- monkeypatch=monkeypatch, |
|
| 2188 |
- runner=runner, |
|
| 2189 |
- vault_config={
|
|
| 2190 |
- "global": {"phrase": "abc"},
|
|
| 2191 |
- "services": {"sv": {"notes": "Contents go here"}},
|
|
| 2192 |
- }, |
|
| 2193 |
- ) |
|
| 2112 |
+ |
|
| 2113 |
+ extra_args: TestNotesEditing.ExtraArgs = {
|
|
| 2114 |
+ "modern_editor_interface": modern_editor_interface, |
|
| 2115 |
+ "current_notes": self.CURRENT_NOTES, |
|
| 2116 |
+ "old_notes_text": self.OLD_NOTES_TEXT, |
|
| 2117 |
+ } |
|
| 2118 |
+ notes_unchanged = notes.strip() == extra_args["current_notes"].strip() |
|
| 2119 |
+ |
|
| 2120 |
+ result, new_backup_notes, new_config = self._test( |
|
| 2121 |
+ notes.strip(), **extra_args |
|
| 2194 | 2122 |
) |
| 2195 |
- notes_backup_file = cli_helpers.config_filename( |
|
| 2196 |
- subsystem="notes backup" |
|
| 2123 |
+ self._assert_normal_exit(result) |
|
| 2124 |
+ self._assert_notes_and_backup_notes( |
|
| 2125 |
+ final_notes=notes, |
|
| 2126 |
+ new_backup_notes=new_backup_notes, |
|
| 2127 |
+ new_config=new_config, |
|
| 2128 |
+ **extra_args, |
|
| 2197 | 2129 |
) |
| 2198 |
- notes_backup_file.write_text( |
|
| 2199 |
- "These backup notes are left over from the previous session.", |
|
| 2200 |
- encoding="UTF-8", |
|
| 2130 |
+ self._assert_notes_backup_warning( |
|
| 2131 |
+ caplog, |
|
| 2132 |
+ modern_editor_interface=modern_editor_interface, |
|
| 2133 |
+ notes_unchanged=notes_unchanged, |
|
| 2201 | 2134 |
) |
| 2202 |
- monkeypatch.setattr(click, "edit", lambda *_a, **_kw: notes) |
|
| 2203 |
- result = runner.invoke( |
|
| 2204 |
- cli.derivepassphrase_vault, |
|
| 2205 |
- [ |
|
| 2206 |
- "--config", |
|
| 2207 |
- "--notes", |
|
| 2208 |
- "--modern-editor-interface" |
|
| 2209 |
- if modern_editor_interface |
|
| 2210 |
- else "--vault-legacy-editor-interface", |
|
| 2211 |
- "--", |
|
| 2212 |
- "sv", |
|
| 2213 |
- ], |
|
| 2214 |
- catch_exceptions=False, |
|
| 2215 |
- ) |
|
| 2216 |
- assert result.clean_exit(), "expected clean exit" |
|
| 2217 |
- assert not result.stderr or all( |
|
| 2218 |
- map(is_warning_line, result.stderr.splitlines(True)) |
|
| 2219 |
- ) |
|
| 2220 |
- assert not caplog.record_tuples or machinery.warning_emitted( |
|
| 2221 |
- "A backup copy of the old notes was saved", |
|
| 2222 |
- caplog.record_tuples, |
|
| 2223 |
- ), "expected known warning message in stderr" |
|
| 2224 |
- assert ( |
|
| 2225 |
- modern_editor_interface |
|
| 2226 |
- or notes_backup_file.read_text(encoding="UTF-8") |
|
| 2227 |
- == "Contents go here" |
|
| 2228 |
- ) |
|
| 2229 |
- with cli_helpers.config_filename(subsystem="vault").open( |
|
| 2230 |
- encoding="UTF-8" |
|
| 2231 |
- ) as infile: |
|
| 2232 |
- config = json.load(infile) |
|
| 2233 |
- assert config == {
|
|
| 2234 |
- "global": {"phrase": "abc"},
|
|
| 2235 |
- "services": {"sv": {"notes": notes.strip()}},
|
|
| 2236 |
- } |
|
| 2237 | 2135 |
|
| 2238 | 2136 |
|
| 2239 |
-class TestNotesEditingInvalid: |
|
| 2137 |
+class TestNotesEditingInvalid(TestNotesEditing): |
|
| 2240 | 2138 |
"""Tests concerning editing service notes: invalid/error calls.""" |
| 2241 | 2139 |
|
| 2242 |
- @hypothesis.given( |
|
| 2243 |
- notes=strategies.text( |
|
| 2244 |
- strategies.characters( |
|
| 2245 |
- min_codepoint=32, max_codepoint=126, include_characters="\n" |
|
| 2246 |
- ), |
|
| 2247 |
- min_size=1, |
|
| 2248 |
- max_size=512, |
|
| 2249 |
- ).filter(str.strip), |
|
| 2250 |
- ) |
|
| 2140 |
+ @hypothesis.given(notes=Strategies.notes()) |
|
| 2141 |
+ @hypothesis.example("")
|
|
| 2251 | 2142 |
def test_abort( |
| 2252 | 2143 |
self, |
| 2253 | 2144 |
notes: str, |
| 2254 | 2145 |
) -> None: |
| 2255 |
- """Aborting editing notes works. |
|
| 2146 |
+ """Aborting editing notes works, even if no notes are stored yet. |
|
| 2256 | 2147 |
|
| 2257 | 2148 |
Aborting is only supported with the modern editor interface. |
| 2258 | 2149 |
|
| 2259 | 2150 |
""" |
| 2260 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2261 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2262 |
- # with-statements. |
|
| 2263 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2264 |
- with contextlib.ExitStack() as stack: |
|
| 2265 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2266 |
- stack.enter_context( |
|
| 2267 |
- pytest_machinery.isolated_vault_config( |
|
| 2268 |
- monkeypatch=monkeypatch, |
|
| 2269 |
- runner=runner, |
|
| 2270 |
- vault_config={
|
|
| 2271 |
- "global": {"phrase": "abc"},
|
|
| 2272 |
- "services": {"sv": {"notes": notes.strip()}},
|
|
| 2273 |
- }, |
|
| 2274 |
- ) |
|
| 2275 |
- ) |
|
| 2276 |
- monkeypatch.setattr(click, "edit", lambda *_a, **_kw: "") |
|
| 2277 |
- result = runner.invoke( |
|
| 2278 |
- cli.derivepassphrase_vault, |
|
| 2279 |
- [ |
|
| 2280 |
- "--config", |
|
| 2281 |
- "--notes", |
|
| 2282 |
- "--modern-editor-interface", |
|
| 2283 |
- "--", |
|
| 2284 |
- "sv", |
|
| 2285 |
- ], |
|
| 2286 |
- catch_exceptions=False, |
|
| 2287 |
- ) |
|
| 2288 |
- assert result.error_exit(error="the user aborted the request"), ( |
|
| 2289 |
- "expected known error message" |
|
| 2290 |
- ) |
|
| 2291 |
- with cli_helpers.config_filename(subsystem="vault").open( |
|
| 2292 |
- encoding="UTF-8" |
|
| 2293 |
- ) as infile: |
|
| 2294 |
- config = json.load(infile) |
|
| 2295 |
- assert config == {
|
|
| 2296 |
- "global": {"phrase": "abc"},
|
|
| 2297 |
- "services": {"sv": {"notes": notes.strip()}},
|
|
| 2298 |
- } |
|
| 2151 |
+ edit_result = "" |
|
| 2299 | 2152 |
|
| 2300 |
- def test_abort_no_prior_notes( |
|
| 2301 |
- self, |
|
| 2302 |
- ) -> None: |
|
| 2303 |
- """Aborting editing notes works even if no notes are stored yet. |
|
| 2304 |
- |
|
| 2305 |
- Aborting is only supported with the modern editor interface. |
|
| 2306 |
- |
|
| 2307 |
- """ |
|
| 2308 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2309 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2310 |
- # with-statements. |
|
| 2311 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2312 |
- with contextlib.ExitStack() as stack: |
|
| 2313 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2314 |
- stack.enter_context( |
|
| 2315 |
- pytest_machinery.isolated_vault_config( |
|
| 2316 |
- monkeypatch=monkeypatch, |
|
| 2317 |
- runner=runner, |
|
| 2318 |
- vault_config={
|
|
| 2319 |
- "global": {"phrase": "abc"},
|
|
| 2320 |
- "services": {},
|
|
| 2321 |
- }, |
|
| 2322 |
- ) |
|
| 2323 |
- ) |
|
| 2324 |
- monkeypatch.setattr(click, "edit", lambda *_a, **_kw: "") |
|
| 2325 |
- result = runner.invoke( |
|
| 2326 |
- cli.derivepassphrase_vault, |
|
| 2327 |
- [ |
|
| 2328 |
- "--config", |
|
| 2329 |
- "--notes", |
|
| 2330 |
- "--modern-editor-interface", |
|
| 2331 |
- "--", |
|
| 2332 |
- "sv", |
|
| 2333 |
- ], |
|
| 2334 |
- catch_exceptions=False, |
|
| 2335 |
- ) |
|
| 2336 |
- assert result.error_exit(error="the user aborted the request"), ( |
|
| 2337 |
- "expected known error message" |
|
| 2338 |
- ) |
|
| 2339 |
- with cli_helpers.config_filename(subsystem="vault").open( |
|
| 2340 |
- encoding="UTF-8" |
|
| 2341 |
- ) as infile: |
|
| 2342 |
- config = json.load(infile) |
|
| 2343 |
- assert config == {
|
|
| 2344 |
- "global": {"phrase": "abc"},
|
|
| 2345 |
- "services": {},
|
|
| 2153 |
+ extra_args: TestNotesEditing.ExtraArgs = {
|
|
| 2154 |
+ "modern_editor_interface": True, |
|
| 2155 |
+ "current_notes": notes.strip(), |
|
| 2156 |
+ "old_notes_text": self.OLD_NOTES_TEXT, |
|
| 2346 | 2157 |
} |
| 2347 | 2158 |
|
| 2159 |
+ result, new_backup_notes, new_config = self._test( |
|
| 2160 |
+ edit_result, **extra_args |
|
| 2161 |
+ ) |
|
| 2162 |
+ assert result.error_exit(error="the user aborted the request"), ( |
|
| 2163 |
+ "expected error exit" |
|
| 2164 |
+ ) |
|
| 2165 |
+ self._assert_notes_and_backup_notes( |
|
| 2166 |
+ final_notes=notes.strip(), |
|
| 2167 |
+ new_backup_notes=new_backup_notes, |
|
| 2168 |
+ new_config=new_config, |
|
| 2169 |
+ **extra_args, |
|
| 2170 |
+ ) |
|
| 2171 |
+ |
|
| 2348 | 2172 |
@Parametrize.MODERN_EDITOR_INTERFACE |
| 2349 | 2173 |
@hypothesis.settings( |
| 2350 | 2174 |
suppress_health_check=[ |
| ... | ... |
@@ -2352,14 +2176,8 @@ class TestNotesEditingInvalid: |
| 2352 | 2176 |
hypothesis.HealthCheck.function_scoped_fixture, |
| 2353 | 2177 |
], |
| 2354 | 2178 |
) |
| 2355 |
- @hypothesis.given( |
|
| 2356 |
- notes=strategies.text( |
|
| 2357 |
- strategies.characters( |
|
| 2358 |
- min_codepoint=32, max_codepoint=126, include_characters="\n" |
|
| 2359 |
- ), |
|
| 2360 |
- max_size=512, |
|
| 2361 |
- ), |
|
| 2362 |
- ) |
|
| 2179 |
+ @hypothesis.given(notes=Strategies.notes()) |
|
| 2180 |
+ @hypothesis.example("")
|
|
| 2363 | 2181 |
def test_fail_on_config_option_missing( |
| 2364 | 2182 |
self, |
| 2365 | 2183 |
caplog: pytest.LogCaptureFixture, |
| ... | ... |
@@ -2374,6 +2192,9 @@ class TestNotesEditingInvalid: |
| 2374 | 2192 |
DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS}
|
| 2375 | 2193 |
}, |
| 2376 | 2194 |
} |
| 2195 |
+ old_notes_text = ( |
|
| 2196 |
+ "These backup notes are left over from the previous session." |
|
| 2197 |
+ ) |
|
| 2377 | 2198 |
# Reset caplog between hypothesis runs. |
| 2378 | 2199 |
caplog.clear() |
| 2379 | 2200 |
runner = machinery.CliRunner(mix_stderr=False) |
| ... | ... |
@@ -2397,10 +2218,7 @@ class TestNotesEditingInvalid: |
| 2397 | 2218 |
notes_backup_file = cli_helpers.config_filename( |
| 2398 | 2219 |
subsystem="notes backup" |
| 2399 | 2220 |
) |
| 2400 |
- notes_backup_file.write_text( |
|
| 2401 |
- "These backup notes are left over from the previous session.", |
|
| 2402 |
- encoding="UTF-8", |
|
| 2403 |
- ) |
|
| 2221 |
+ notes_backup_file.write_text(old_notes_text, encoding="UTF-8") |
|
| 2404 | 2222 |
monkeypatch.setattr(click, "edit", raiser) |
| 2405 | 2223 |
result = runner.invoke( |
| 2406 | 2224 |
cli.derivepassphrase_vault, |
| ... | ... |
@@ -2432,7 +2250,7 @@ class TestNotesEditingInvalid: |
| 2432 | 2250 |
assert ( |
| 2433 | 2251 |
modern_editor_interface |
| 2434 | 2252 |
or notes_backup_file.read_text(encoding="UTF-8") |
| 2435 |
- == "These backup notes are left over from the previous session." |
|
| 2253 |
+ == old_notes_text |
|
| 2436 | 2254 |
) |
| 2437 | 2255 |
with cli_helpers.config_filename(subsystem="vault").open( |
| 2438 | 2256 |
encoding="UTF-8" |
| ... | ... |
@@ -2444,19 +2262,15 @@ class TestNotesEditingInvalid: |
| 2444 | 2262 |
class TestStoringConfigurationSuccesses: |
| 2445 | 2263 |
"""Tests concerning storing the configuration: successes.""" |
| 2446 | 2264 |
|
| 2447 |
- @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG |
|
| 2448 |
- def test_store_good_config( |
|
| 2265 |
+ def _test( |
|
| 2449 | 2266 |
self, |
| 2450 | 2267 |
command_line: list[str], |
| 2451 |
- input: str, |
|
| 2452 |
- result_config: Any, |
|
| 2453 |
- ) -> None: |
|
| 2454 |
- """Storing valid settings via `--config` works. |
|
| 2455 |
- |
|
| 2456 |
- The format also contains embedded newlines and indentation to make |
|
| 2457 |
- the config more readable. |
|
| 2458 |
- |
|
| 2459 |
- """ |
|
| 2268 |
+ /, |
|
| 2269 |
+ *, |
|
| 2270 |
+ starting_config: _types.VaultConfig | None, |
|
| 2271 |
+ result_config: _types.VaultConfig, |
|
| 2272 |
+ input: str | bytes | None = None, |
|
| 2273 |
+ ) -> machinery.ReadableResult: |
|
| 2460 | 2274 |
runner = machinery.CliRunner(mix_stderr=False) |
| 2461 | 2275 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2462 | 2276 |
# with-statements. |
| ... | ... |
@@ -2467,9 +2281,12 @@ class TestStoringConfigurationSuccesses: |
| 2467 | 2281 |
pytest_machinery.isolated_vault_config( |
| 2468 | 2282 |
monkeypatch=monkeypatch, |
| 2469 | 2283 |
runner=runner, |
| 2470 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2284 |
+ vault_config=starting_config, |
|
| 2471 | 2285 |
) |
| 2472 | 2286 |
) |
| 2287 |
+ if starting_config is None: |
|
| 2288 |
+ with contextlib.suppress(FileNotFoundError): |
|
| 2289 |
+ shutil.rmtree(cli_helpers.config_filename(subsystem=None)) |
|
| 2473 | 2290 |
monkeypatch.setattr( |
| 2474 | 2291 |
cli_helpers, |
| 2475 | 2292 |
"get_suitable_ssh_keys", |
| ... | ... |
@@ -2490,6 +2307,28 @@ class TestStoringConfigurationSuccesses: |
| 2490 | 2307 |
"stored config does not match expectation" |
| 2491 | 2308 |
) |
| 2492 | 2309 |
assert_vault_config_is_indented_and_line_broken(config_txt) |
| 2310 |
+ return result |
|
| 2311 |
+ |
|
| 2312 |
+ @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG |
|
| 2313 |
+ def test_store_good_config( |
|
| 2314 |
+ self, |
|
| 2315 |
+ command_line: list[str], |
|
| 2316 |
+ input: str, |
|
| 2317 |
+ starting_config: Any, |
|
| 2318 |
+ result_config: Any, |
|
| 2319 |
+ ) -> None: |
|
| 2320 |
+ """Storing valid settings via `--config` works. |
|
| 2321 |
+ |
|
| 2322 |
+ The format also contains embedded newlines and indentation to make |
|
| 2323 |
+ the config more readable. |
|
| 2324 |
+ |
|
| 2325 |
+ """ |
|
| 2326 |
+ self._test( |
|
| 2327 |
+ command_line, |
|
| 2328 |
+ input=input, |
|
| 2329 |
+ starting_config=starting_config, |
|
| 2330 |
+ result_config=result_config, |
|
| 2331 |
+ ) |
|
| 2493 | 2332 |
|
| 2494 | 2333 |
def test_config_directory_nonexistant( |
| 2495 | 2334 |
self, |
| ... | ... |
@@ -2504,51 +2343,30 @@ class TestStoringConfigurationSuccesses: |
| 2504 | 2343 |
[PRETTY_PRINT_JSON]: https://the13thletter.info/derivepassphrase/0.x/wishlist/pretty-print-json/ |
| 2505 | 2344 |
|
| 2506 | 2345 |
""" |
| 2507 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2508 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2509 |
- # with-statements. |
|
| 2510 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2511 |
- with contextlib.ExitStack() as stack: |
|
| 2512 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2513 |
- stack.enter_context( |
|
| 2514 |
- pytest_machinery.isolated_config( |
|
| 2515 |
- monkeypatch=monkeypatch, |
|
| 2516 |
- runner=runner, |
|
| 2517 |
- ) |
|
| 2518 |
- ) |
|
| 2519 |
- with contextlib.suppress(FileNotFoundError): |
|
| 2520 |
- shutil.rmtree(cli_helpers.config_filename(subsystem=None)) |
|
| 2521 |
- result = runner.invoke( |
|
| 2522 |
- cli.derivepassphrase_vault, |
|
| 2523 |
- ["--config", "-p"], |
|
| 2524 |
- catch_exceptions=False, |
|
| 2346 |
+ result = self._test( |
|
| 2347 |
+ ["-p"], |
|
| 2348 |
+ starting_config=None, |
|
| 2349 |
+ result_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2525 | 2350 |
input="abc\n", |
| 2526 | 2351 |
) |
| 2527 |
- assert result.clean_exit(), "expected clean exit" |
|
| 2528 |
- assert result.stderr == "Passphrase:", ( |
|
| 2529 |
- "program unexpectedly failed?!" |
|
| 2530 |
- ) |
|
| 2531 |
- with cli_helpers.config_filename(subsystem="vault").open( |
|
| 2532 |
- encoding="UTF-8" |
|
| 2533 |
- ) as infile: |
|
| 2534 |
- config_readback = json.load(infile) |
|
| 2535 |
- assert config_readback == {
|
|
| 2536 |
- "global": {"phrase": "abc"},
|
|
| 2537 |
- "services": {},
|
|
| 2538 |
- }, "config mismatch" |
|
| 2352 |
+ assert result.stderr == "Passphrase:", "program unexpectedly failed?!" |
|
| 2539 | 2353 |
|
| 2540 | 2354 |
|
| 2541 | 2355 |
class TestStoringConfigurationFailures: |
| 2542 | 2356 |
"""Tests concerning storing the configuration: failures.""" |
| 2543 | 2357 |
|
| 2544 |
- @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES |
|
| 2545 |
- def test_store_bad_config( |
|
| 2358 |
+ @contextlib.contextmanager |
|
| 2359 |
+ def _test( |
|
| 2546 | 2360 |
self, |
| 2547 | 2361 |
command_line: list[str], |
| 2548 |
- input: str, |
|
| 2549 |
- err_text: str, |
|
| 2550 |
- ) -> None: |
|
| 2551 |
- """Storing invalid settings via `--config` fails.""" |
|
| 2362 |
+ error_text: str, |
|
| 2363 |
+ input: str | bytes | None = None, |
|
| 2364 |
+ starting_config: _types.VaultConfig = { # noqa: B006
|
|
| 2365 |
+ "global": {"phrase": "abc"},
|
|
| 2366 |
+ "services": {},
|
|
| 2367 |
+ }, |
|
| 2368 |
+ patch_suitable_ssh_keys: bool = True, |
|
| 2369 |
+ ) -> Iterator[pytest.MonkeyPatch]: |
|
| 2552 | 2370 |
runner = machinery.CliRunner(mix_stderr=False) |
| 2553 | 2371 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2554 | 2372 |
# with-statements. |
| ... | ... |
@@ -2559,43 +2377,52 @@ class TestStoringConfigurationFailures: |
| 2559 | 2377 |
pytest_machinery.isolated_vault_config( |
| 2560 | 2378 |
monkeypatch=monkeypatch, |
| 2561 | 2379 |
runner=runner, |
| 2562 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2380 |
+ vault_config=starting_config, |
|
| 2563 | 2381 |
) |
| 2564 | 2382 |
) |
| 2383 |
+ # Patch the list of suitable SSH keys by default, lest we be |
|
| 2384 |
+ # at the mercy of whatever SSH agent may be running. (But |
|
| 2385 |
+ # allow a test to turn this off, if it would interfere with |
|
| 2386 |
+ # the testing target, e.g. because we are testing |
|
| 2387 |
+ # non-reachability of the agent.) |
|
| 2388 |
+ if patch_suitable_ssh_keys: |
|
| 2565 | 2389 |
monkeypatch.setattr( |
| 2566 | 2390 |
cli_helpers, |
| 2567 | 2391 |
"get_suitable_ssh_keys", |
| 2568 | 2392 |
callables.suitable_ssh_keys, |
| 2569 | 2393 |
) |
| 2394 |
+ yield monkeypatch |
|
| 2570 | 2395 |
result = runner.invoke( |
| 2571 | 2396 |
cli.derivepassphrase_vault, |
| 2572 | 2397 |
["--config", *command_line], |
| 2573 | 2398 |
catch_exceptions=False, |
| 2574 | 2399 |
input=input, |
| 2575 | 2400 |
) |
| 2576 |
- assert result.error_exit(error=err_text), ( |
|
| 2401 |
+ assert result.error_exit(error=error_text), ( |
|
| 2577 | 2402 |
"expected error exit and known error message" |
| 2578 | 2403 |
) |
| 2579 | 2404 |
|
| 2580 |
- def test_fail_because_no_ssh_key_selection( |
|
| 2405 |
+ @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES |
|
| 2406 |
+ def test_store_bad_config( |
|
| 2581 | 2407 |
self, |
| 2582 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 2408 |
+ command_line: list[str], |
|
| 2409 |
+ input: str, |
|
| 2410 |
+ err_text: str, |
|
| 2583 | 2411 |
) -> None: |
| 2584 |
- """Not selecting an SSH key during `--config --key` fails.""" |
|
| 2585 |
- del running_ssh_agent |
|
| 2586 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2587 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2588 |
- # with-statements. |
|
| 2589 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2590 |
- with contextlib.ExitStack() as stack: |
|
| 2591 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2592 |
- stack.enter_context( |
|
| 2593 |
- pytest_machinery.isolated_vault_config( |
|
| 2594 |
- monkeypatch=monkeypatch, |
|
| 2595 |
- runner=runner, |
|
| 2596 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2597 |
- ) |
|
| 2598 |
- ) |
|
| 2412 |
+ """Storing invalid settings via `--config` fails.""" |
|
| 2413 |
+ with self._test(command_line, error_text=err_text, input=input): |
|
| 2414 |
+ pass |
|
| 2415 |
+ |
|
| 2416 |
+ def test_fail_because_no_ssh_key_selection(self) -> None: |
|
| 2417 |
+ """Not selecting an SSH key during `--config --key` fails. |
|
| 2418 |
+ |
|
| 2419 |
+ (This test does not actually need a running agent; the agent's |
|
| 2420 |
+ response is mocked by the test harness.) |
|
| 2421 |
+ |
|
| 2422 |
+ """ |
|
| 2423 |
+ with self._test( |
|
| 2424 |
+ ["--key"], error_text="the user aborted the request" |
|
| 2425 |
+ ) as monkeypatch: |
|
| 2599 | 2426 |
|
| 2600 | 2427 |
def prompt_for_selection(*_args: Any, **_kwargs: Any) -> NoReturn: |
| 2601 | 2428 |
raise IndexError(cli_helpers.EMPTY_SELECTION) |
| ... | ... |
@@ -2603,198 +2430,84 @@ class TestStoringConfigurationFailures: |
| 2603 | 2430 |
monkeypatch.setattr( |
| 2604 | 2431 |
cli_helpers, "prompt_for_selection", prompt_for_selection |
| 2605 | 2432 |
) |
| 2606 |
- # Also patch the list of suitable SSH keys, lest we be at |
|
| 2607 |
- # the mercy of whatever SSH agent may be running. |
|
| 2608 |
- monkeypatch.setattr( |
|
| 2609 |
- cli_helpers, |
|
| 2610 |
- "get_suitable_ssh_keys", |
|
| 2611 |
- callables.suitable_ssh_keys, |
|
| 2612 |
- ) |
|
| 2613 |
- result = runner.invoke( |
|
| 2614 |
- cli.derivepassphrase_vault, |
|
| 2615 |
- ["--key", "--config"], |
|
| 2616 |
- catch_exceptions=False, |
|
| 2617 |
- ) |
|
| 2618 |
- assert result.error_exit(error="the user aborted the request"), ( |
|
| 2619 |
- "expected error exit and known error message" |
|
| 2620 |
- ) |
|
| 2621 |
- |
|
| 2622 |
- def test_fail_because_no_ssh_agent( |
|
| 2623 |
- self, |
|
| 2624 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 2625 |
- ) -> None: |
|
| 2626 |
- """Not running an SSH agent during `--config --key` fails.""" |
|
| 2627 |
- del running_ssh_agent |
|
| 2628 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2629 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2630 |
- # with-statements. |
|
| 2631 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2632 |
- with contextlib.ExitStack() as stack: |
|
| 2633 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2634 |
- stack.enter_context( |
|
| 2635 |
- pytest_machinery.isolated_vault_config( |
|
| 2636 |
- monkeypatch=monkeypatch, |
|
| 2637 |
- runner=runner, |
|
| 2638 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2639 |
- ) |
|
| 2640 |
- ) |
|
| 2641 |
- monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
|
| 2642 |
- result = runner.invoke( |
|
| 2643 |
- cli.derivepassphrase_vault, |
|
| 2644 |
- ["--key", "--config"], |
|
| 2645 |
- catch_exceptions=False, |
|
| 2646 |
- ) |
|
| 2647 |
- assert result.error_exit(error="Cannot find any running SSH agent"), ( |
|
| 2648 |
- "expected error exit and known error message" |
|
| 2649 |
- ) |
|
| 2650 |
- |
|
| 2651 |
- def test_fail_because_bad_ssh_agent_connection( |
|
| 2652 |
- self, |
|
| 2653 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 2654 |
- ) -> None: |
|
| 2655 |
- """Not running a reachable SSH agent during `--config --key` fails.""" |
|
| 2656 |
- running_ssh_agent.require_external_address() |
|
| 2657 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2658 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2659 |
- # with-statements. |
|
| 2660 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2661 |
- with contextlib.ExitStack() as stack: |
|
| 2662 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2663 |
- stack.enter_context( |
|
| 2664 |
- pytest_machinery.isolated_vault_config( |
|
| 2665 |
- monkeypatch=monkeypatch, |
|
| 2666 |
- runner=runner, |
|
| 2667 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2668 |
- ) |
|
| 2669 |
- ) |
|
| 2670 |
- cwd = pathlib.Path.cwd().resolve() |
|
| 2671 |
- monkeypatch.setenv("SSH_AUTH_SOCK", str(cwd))
|
|
| 2672 |
- result = runner.invoke( |
|
| 2673 |
- cli.derivepassphrase_vault, |
|
| 2674 |
- ["--key", "--config"], |
|
| 2675 |
- catch_exceptions=False, |
|
| 2676 |
- ) |
|
| 2677 |
- assert result.error_exit(error="Cannot connect to the SSH agent"), ( |
|
| 2678 |
- "expected error exit and known error message" |
|
| 2679 |
- ) |
|
| 2680 |
- |
|
| 2681 |
- @Parametrize.TRY_RACE_FREE_IMPLEMENTATION |
|
| 2682 |
- def test_fail_because_read_only_file( |
|
| 2683 |
- self, |
|
| 2684 |
- try_race_free_implementation: bool, |
|
| 2685 |
- ) -> None: |
|
| 2686 |
- """Using a read-only configuration file with `--config` fails.""" |
|
| 2687 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2688 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2689 |
- # with-statements. |
|
| 2690 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2691 |
- with contextlib.ExitStack() as stack: |
|
| 2692 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2693 |
- stack.enter_context( |
|
| 2694 |
- pytest_machinery.isolated_vault_config( |
|
| 2695 |
- monkeypatch=monkeypatch, |
|
| 2696 |
- runner=runner, |
|
| 2697 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2698 |
- ) |
|
| 2699 |
- ) |
|
| 2700 |
- callables.make_file_readonly( |
|
| 2701 |
- cli_helpers.config_filename(subsystem="vault"), |
|
| 2702 |
- try_race_free_implementation=try_race_free_implementation, |
|
| 2703 |
- ) |
|
| 2704 |
- result = runner.invoke( |
|
| 2705 |
- cli.derivepassphrase_vault, |
|
| 2706 |
- ["--config", "--length=15", "--", DUMMY_SERVICE], |
|
| 2707 |
- catch_exceptions=False, |
|
| 2708 |
- ) |
|
| 2709 |
- assert result.error_exit(error="Cannot store vault settings:"), ( |
|
| 2710 |
- "expected error exit and known error message" |
|
| 2711 |
- ) |
|
| 2712 |
- |
|
| 2713 |
- def test_fail_because_of_custom_error( |
|
| 2714 |
- self, |
|
| 2715 |
- ) -> None: |
|
| 2716 |
- """Triggering internal errors during `--config` leads to failure.""" |
|
| 2717 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2718 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2719 |
- # with-statements. |
|
| 2720 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2721 |
- with contextlib.ExitStack() as stack: |
|
| 2722 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2723 |
- stack.enter_context( |
|
| 2724 |
- pytest_machinery.isolated_vault_config( |
|
| 2725 |
- monkeypatch=monkeypatch, |
|
| 2726 |
- runner=runner, |
|
| 2727 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2728 |
- ) |
|
| 2729 |
- ) |
|
| 2730 |
- custom_error = "custom error message" |
|
| 2731 | 2433 |
|
| 2732 |
- def raiser(config: Any) -> None: |
|
| 2733 |
- del config |
|
| 2734 |
- raise RuntimeError(custom_error) |
|
| 2434 |
+ def test_fail_because_no_ssh_agent(self) -> None: |
|
| 2435 |
+ """Not running an SSH agent during `--config --key` fails. |
|
| 2735 | 2436 |
|
| 2736 |
- monkeypatch.setattr(cli_helpers, "save_config", raiser) |
|
| 2737 |
- result = runner.invoke( |
|
| 2738 |
- cli.derivepassphrase_vault, |
|
| 2739 |
- ["--config", "--length=15", "--", DUMMY_SERVICE], |
|
| 2740 |
- catch_exceptions=False, |
|
| 2741 |
- ) |
|
| 2742 |
- assert result.error_exit(error=custom_error), ( |
|
| 2743 |
- "expected error exit and known error message" |
|
| 2744 |
- ) |
|
| 2437 |
+ (This test does not actually need a running agent; the agent's |
|
| 2438 |
+ response is mocked by the test harness.) |
|
| 2745 | 2439 |
|
| 2746 |
- def test_fail_because_unsetting_and_setting_same_settings( |
|
| 2747 |
- self, |
|
| 2748 |
- ) -> None: |
|
| 2749 |
- """Issuing conflicting settings to `--config` fails.""" |
|
| 2750 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2751 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2752 |
- # with-statements. |
|
| 2753 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2754 |
- with contextlib.ExitStack() as stack: |
|
| 2755 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2756 |
- stack.enter_context( |
|
| 2757 |
- pytest_machinery.isolated_vault_config( |
|
| 2758 |
- monkeypatch=monkeypatch, |
|
| 2759 |
- runner=runner, |
|
| 2760 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2761 |
- ) |
|
| 2762 |
- ) |
|
| 2763 |
- result = runner.invoke( |
|
| 2764 |
- cli.derivepassphrase_vault, |
|
| 2765 |
- [ |
|
| 2766 |
- "--config", |
|
| 2767 |
- "--unset=length", |
|
| 2768 |
- "--length=15", |
|
| 2769 |
- "--", |
|
| 2770 |
- DUMMY_SERVICE, |
|
| 2771 |
- ], |
|
| 2772 |
- catch_exceptions=False, |
|
| 2773 |
- ) |
|
| 2774 |
- assert result.error_exit( |
|
| 2775 |
- error="Attempted to unset and set --length at the same time." |
|
| 2776 |
- ), "expected error exit and known error message" |
|
| 2440 |
+ """ |
|
| 2441 |
+ with self._test( |
|
| 2442 |
+ ["--key"], |
|
| 2443 |
+ error_text="Cannot find any running SSH agent", |
|
| 2444 |
+ patch_suitable_ssh_keys=False, |
|
| 2445 |
+ ) as monkeypatch: |
|
| 2446 |
+ monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
|
| 2777 | 2447 |
|
| 2778 |
- def test_fail_because_ssh_agent_has_no_keys_loaded( |
|
| 2779 |
- self, |
|
| 2780 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 2448 |
+ def test_fail_because_bad_ssh_agent_connection(self) -> None: |
|
| 2449 |
+ """Not running a reachable SSH agent during `--config --key` fails. |
|
| 2450 |
+ |
|
| 2451 |
+ (This test does not actually need a running agent; the agent's |
|
| 2452 |
+ response is mocked by the test harness.) |
|
| 2453 |
+ |
|
| 2454 |
+ """ |
|
| 2455 |
+ with self._test( |
|
| 2456 |
+ ["--key"], |
|
| 2457 |
+ error_text="Cannot connect to the SSH agent", |
|
| 2458 |
+ patch_suitable_ssh_keys=False, |
|
| 2459 |
+ ) as monkeypatch: |
|
| 2460 |
+ cwd = pathlib.Path.cwd().resolve() |
|
| 2461 |
+ monkeypatch.setenv("SSH_AUTH_SOCK", str(cwd))
|
|
| 2462 |
+ |
|
| 2463 |
+ @Parametrize.TRY_RACE_FREE_IMPLEMENTATION |
|
| 2464 |
+ def test_fail_because_read_only_file( |
|
| 2465 |
+ self, try_race_free_implementation: bool |
|
| 2781 | 2466 |
) -> None: |
| 2782 |
- """Not holding any SSH keys during `--config --key` fails.""" |
|
| 2783 |
- del running_ssh_agent |
|
| 2784 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2785 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2786 |
- # with-statements. |
|
| 2787 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2788 |
- with contextlib.ExitStack() as stack: |
|
| 2789 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2790 |
- stack.enter_context( |
|
| 2791 |
- pytest_machinery.isolated_vault_config( |
|
| 2792 |
- monkeypatch=monkeypatch, |
|
| 2793 |
- runner=runner, |
|
| 2794 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2795 |
- ) |
|
| 2467 |
+ """Using a read-only configuration file with `--config` fails.""" |
|
| 2468 |
+ with self._test( |
|
| 2469 |
+ ["--length=15", "--", DUMMY_SERVICE], |
|
| 2470 |
+ error_text="Cannot store vault settings:", |
|
| 2471 |
+ ): |
|
| 2472 |
+ callables.make_file_readonly( |
|
| 2473 |
+ cli_helpers.config_filename(subsystem="vault"), |
|
| 2474 |
+ try_race_free_implementation=try_race_free_implementation, |
|
| 2796 | 2475 |
) |
| 2797 | 2476 |
|
| 2477 |
+ def test_fail_because_of_custom_error(self) -> None: |
|
| 2478 |
+ """Triggering internal errors during `--config` leads to failure.""" |
|
| 2479 |
+ custom_error = "custom error message" |
|
| 2480 |
+ with self._test( |
|
| 2481 |
+ ["--length=15", "--", DUMMY_SERVICE], error_text=custom_error |
|
| 2482 |
+ ) as monkeypatch: |
|
| 2483 |
+ |
|
| 2484 |
+ def raiser(config: Any) -> None: |
|
| 2485 |
+ del config |
|
| 2486 |
+ raise RuntimeError(custom_error) |
|
| 2487 |
+ |
|
| 2488 |
+ monkeypatch.setattr(cli_helpers, "save_config", raiser) |
|
| 2489 |
+ |
|
| 2490 |
+ def test_fail_because_unsetting_and_setting_same_settings(self) -> None: |
|
| 2491 |
+ """Issuing conflicting settings to `--config` fails.""" |
|
| 2492 |
+ with self._test( |
|
| 2493 |
+ ["--unset=length", "--length=15", "--", DUMMY_SERVICE], |
|
| 2494 |
+ error_text="Attempted to unset and set --length at the same time.", |
|
| 2495 |
+ ): |
|
| 2496 |
+ pass |
|
| 2497 |
+ |
|
| 2498 |
+ def test_fail_because_ssh_agent_has_no_keys_loaded(self) -> None: |
|
| 2499 |
+ """Not holding any SSH keys during `--config --key` fails. |
|
| 2500 |
+ |
|
| 2501 |
+ (This test does not actually need a running agent; the agent's |
|
| 2502 |
+ response is mocked by the test harness.) |
|
| 2503 |
+ |
|
| 2504 |
+ """ |
|
| 2505 |
+ with self._test( |
|
| 2506 |
+ ["--key"], |
|
| 2507 |
+ error_text="no keys suitable", |
|
| 2508 |
+ patch_suitable_ssh_keys=False, |
|
| 2509 |
+ ) as monkeypatch: |
|
| 2510 |
+ |
|
| 2798 | 2511 |
def func( |
| 2799 | 2512 |
*_args: Any, |
| 2800 | 2513 |
**_kwargs: Any, |
| ... | ... |
@@ -2802,67 +2515,35 @@ class TestStoringConfigurationFailures: |
| 2802 | 2515 |
return [] |
| 2803 | 2516 |
|
| 2804 | 2517 |
monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", func) |
| 2805 |
- result = runner.invoke( |
|
| 2806 |
- cli.derivepassphrase_vault, |
|
| 2807 |
- ["--key", "--config"], |
|
| 2808 |
- catch_exceptions=False, |
|
| 2809 |
- ) |
|
| 2810 |
- assert result.error_exit(error="no keys suitable"), ( |
|
| 2811 |
- "expected error exit and known error message" |
|
| 2812 |
- ) |
|
| 2813 | 2518 |
|
| 2814 |
- def test_store_config_fail_manual_ssh_agent_runtime_error( |
|
| 2815 |
- self, |
|
| 2816 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 2817 |
- ) -> None: |
|
| 2818 |
- """Triggering an error in the SSH agent during `--config --key` leads to failure.""" |
|
| 2819 |
- del running_ssh_agent |
|
| 2820 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2821 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2822 |
- # with-statements. |
|
| 2823 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2824 |
- with contextlib.ExitStack() as stack: |
|
| 2825 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2826 |
- stack.enter_context( |
|
| 2827 |
- pytest_machinery.isolated_vault_config( |
|
| 2828 |
- monkeypatch=monkeypatch, |
|
| 2829 |
- runner=runner, |
|
| 2830 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2831 |
- ) |
|
| 2832 |
- ) |
|
| 2519 |
+ def test_store_config_fail_manual_ssh_agent_runtime_error(self) -> None: |
|
| 2520 |
+ """Triggering an error in the SSH agent during `--config --key` leads to failure. |
|
| 2521 |
+ |
|
| 2522 |
+ (This test does not actually need a running agent; the agent's |
|
| 2523 |
+ response is mocked by the test harness.) |
|
| 2524 |
+ |
|
| 2525 |
+ """ |
|
| 2526 |
+ with self._test( |
|
| 2527 |
+ ["--key"], |
|
| 2528 |
+ error_text="violates the communication protocol", |
|
| 2529 |
+ patch_suitable_ssh_keys=False, |
|
| 2530 |
+ ) as monkeypatch: |
|
| 2833 | 2531 |
|
| 2834 | 2532 |
def raiser(*_args: Any, **_kwargs: Any) -> None: |
| 2835 | 2533 |
raise ssh_agent.TrailingDataError() |
| 2836 | 2534 |
|
| 2837 | 2535 |
monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", raiser) |
| 2838 |
- result = runner.invoke( |
|
| 2839 |
- cli.derivepassphrase_vault, |
|
| 2840 |
- ["--key", "--config"], |
|
| 2841 |
- catch_exceptions=False, |
|
| 2842 |
- ) |
|
| 2843 |
- assert result.error_exit( |
|
| 2844 |
- error="violates the communication protocol." |
|
| 2845 |
- ), "expected error exit and known error message" |
|
| 2846 | 2536 |
|
| 2847 |
- def test_store_config_fail_manual_ssh_agent_refuses( |
|
| 2848 |
- self, |
|
| 2849 |
- running_ssh_agent: data.RunningSSHAgentInfo, |
|
| 2850 |
- ) -> None: |
|
| 2851 |
- """The SSH agent refusing during `--config --key` leads to failure.""" |
|
| 2852 |
- del running_ssh_agent |
|
| 2853 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2854 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2855 |
- # with-statements. |
|
| 2856 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2857 |
- with contextlib.ExitStack() as stack: |
|
| 2858 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2859 |
- stack.enter_context( |
|
| 2860 |
- pytest_machinery.isolated_vault_config( |
|
| 2861 |
- monkeypatch=monkeypatch, |
|
| 2862 |
- runner=runner, |
|
| 2863 |
- vault_config={"global": {"phrase": "abc"}, "services": {}},
|
|
| 2864 |
- ) |
|
| 2865 |
- ) |
|
| 2537 |
+ def test_store_config_fail_manual_ssh_agent_refuses(self) -> None: |
|
| 2538 |
+ """The SSH agent refusing during `--config --key` leads to failure. |
|
| 2539 |
+ |
|
| 2540 |
+ (This test does not actually need a running agent; the agent's |
|
| 2541 |
+ response is mocked by the test harness.) |
|
| 2542 |
+ |
|
| 2543 |
+ """ |
|
| 2544 |
+ with self._test( |
|
| 2545 |
+ ["--key"], error_text="refused to", patch_suitable_ssh_keys=False |
|
| 2546 |
+ ) as monkeypatch: |
|
| 2866 | 2547 |
|
| 2867 | 2548 |
def func(*_args: Any, **_kwargs: Any) -> NoReturn: |
| 2868 | 2549 |
raise ssh_agent.SSHAgentFailedError( |
| ... | ... |
@@ -2870,18 +2551,8 @@ class TestStoringConfigurationFailures: |
| 2870 | 2551 |
) |
| 2871 | 2552 |
|
| 2872 | 2553 |
monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", func) |
| 2873 |
- result = runner.invoke( |
|
| 2874 |
- cli.derivepassphrase_vault, |
|
| 2875 |
- ["--key", "--config"], |
|
| 2876 |
- catch_exceptions=False, |
|
| 2877 |
- ) |
|
| 2878 |
- assert result.error_exit(error="refused to"), ( |
|
| 2879 |
- "expected error exit and known error message" |
|
| 2880 |
- ) |
|
| 2881 | 2554 |
|
| 2882 |
- def test_config_directory_not_a_file( |
|
| 2883 |
- self, |
|
| 2884 |
- ) -> None: |
|
| 2555 |
+ def test_config_directory_not_a_file(self) -> None: |
|
| 2885 | 2556 |
"""Erroring without an existing config directory errors normally. |
| 2886 | 2557 |
|
| 2887 | 2558 |
That is, the missing configuration directory does not cause any |
| ... | ... |
@@ -2895,18 +2566,11 @@ class TestStoringConfigurationFailures: |
| 2895 | 2566 |
[PRETTY_PRINT_JSON]: https://the13thletter.info/derivepassphrase/0.x/wishlist/pretty-print-json/ |
| 2896 | 2567 |
|
| 2897 | 2568 |
""" |
| 2898 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2899 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2900 |
- # with-statements. |
|
| 2901 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2902 |
- with contextlib.ExitStack() as stack: |
|
| 2903 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2904 |
- stack.enter_context( |
|
| 2905 |
- pytest_machinery.isolated_config( |
|
| 2906 |
- monkeypatch=monkeypatch, |
|
| 2907 |
- runner=runner, |
|
| 2908 |
- ) |
|
| 2909 |
- ) |
|
| 2569 |
+ with self._test( |
|
| 2570 |
+ ["--phrase"], |
|
| 2571 |
+ error_text="Cannot store vault settings:", |
|
| 2572 |
+ input="abc\n", |
|
| 2573 |
+ ) as monkeypatch: |
|
| 2910 | 2574 |
save_config_ = cli_helpers.save_config |
| 2911 | 2575 |
|
| 2912 | 2576 |
def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any: |
| ... | ... |
@@ -2920,30 +2584,25 @@ class TestStoringConfigurationFailures: |
| 2920 | 2584 |
monkeypatch.setattr( |
| 2921 | 2585 |
cli_helpers, "save_config", obstruct_config_saving |
| 2922 | 2586 |
) |
| 2923 |
- result = runner.invoke( |
|
| 2924 |
- cli.derivepassphrase_vault, |
|
| 2925 |
- ["--config", "-p"], |
|
| 2926 |
- catch_exceptions=False, |
|
| 2927 |
- input="abc\n", |
|
| 2928 |
- ) |
|
| 2929 |
- assert result.error_exit(error="Cannot store vault settings:"), ( |
|
| 2930 |
- "expected error exit and known error message" |
|
| 2931 |
- ) |
|
| 2932 | 2587 |
|
| 2933 | 2588 |
|
| 2934 | 2589 |
class TestPassphraseUnicodeNormalization: |
| 2935 | 2590 |
"""Tests concerning the Unicode normalization of passphrases.""" |
| 2936 | 2591 |
|
| 2937 |
- @Parametrize.UNICODE_NORMALIZATION_WARNING_INPUTS |
|
| 2938 |
- def test_warning( |
|
| 2592 |
+ DEFAULT_VAULT_CONFIG: ClassVar[_types.VaultConfig] = {
|
|
| 2593 |
+ "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}
|
|
| 2594 |
+ } |
|
| 2595 |
+ |
|
| 2596 |
+ def _test( |
|
| 2939 | 2597 |
self, |
| 2940 |
- caplog: pytest.LogCaptureFixture, |
|
| 2941 |
- main_config: str, |
|
| 2942 | 2598 |
command_line: list[str], |
| 2943 |
- input: str | None, |
|
| 2944 |
- warning_message: str, |
|
| 2599 |
+ /, |
|
| 2600 |
+ *, |
|
| 2601 |
+ main_config: str, |
|
| 2602 |
+ message: str, |
|
| 2603 |
+ caplog: pytest.LogCaptureFixture | None = None, |
|
| 2604 |
+ input: str | None = None, |
|
| 2945 | 2605 |
) -> None: |
| 2946 |
- """Using unnormalized Unicode passphrases warns.""" |
|
| 2947 | 2606 |
runner = machinery.CliRunner(mix_stderr=False) |
| 2948 | 2607 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2949 | 2608 |
# with-statements. |
| ... | ... |
@@ -2954,115 +2613,75 @@ class TestPassphraseUnicodeNormalization: |
| 2954 | 2613 |
pytest_machinery.isolated_vault_config( |
| 2955 | 2614 |
monkeypatch=monkeypatch, |
| 2956 | 2615 |
runner=runner, |
| 2957 |
- vault_config={
|
|
| 2958 |
- "services": {
|
|
| 2959 |
- DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy() |
|
| 2960 |
- } |
|
| 2961 |
- }, |
|
| 2616 |
+ vault_config=self.DEFAULT_VAULT_CONFIG, |
|
| 2962 | 2617 |
main_config_str=main_config, |
| 2963 | 2618 |
) |
| 2964 | 2619 |
) |
| 2965 | 2620 |
result = runner.invoke( |
| 2966 | 2621 |
cli.derivepassphrase_vault, |
| 2967 |
- ["--debug", *command_line], |
|
| 2622 |
+ ["--debug", *command_line] |
|
| 2623 |
+ if caplog is not None |
|
| 2624 |
+ else command_line, |
|
| 2968 | 2625 |
catch_exceptions=False, |
| 2969 | 2626 |
input=input, |
| 2970 | 2627 |
) |
| 2628 |
+ if caplog is not None: |
|
| 2971 | 2629 |
assert result.clean_exit(), "expected clean exit" |
| 2972 |
- assert machinery.warning_emitted( |
|
| 2973 |
- warning_message, caplog.record_tuples |
|
| 2974 |
- ), "expected known warning message in stderr" |
|
| 2630 |
+ assert machinery.warning_emitted(message, caplog.record_tuples), ( |
|
| 2631 |
+ "expected known warning message in stderr" |
|
| 2632 |
+ ) |
|
| 2633 |
+ else: |
|
| 2634 |
+ assert result.error_exit( |
|
| 2635 |
+ error="The user configuration file is invalid." |
|
| 2636 |
+ ), "expected error exit and known error message" |
|
| 2637 |
+ assert result.error_exit(error=message), ( |
|
| 2638 |
+ "expected error exit and known error message" |
|
| 2639 |
+ ) |
|
| 2975 | 2640 |
|
| 2976 |
- @Parametrize.UNICODE_NORMALIZATION_ERROR_INPUTS |
|
| 2977 |
- def test_error( |
|
| 2641 |
+ @Parametrize.UNICODE_NORMALIZATION_WARNING_INPUTS |
|
| 2642 |
+ def test_warning( |
|
| 2978 | 2643 |
self, |
| 2644 |
+ caplog: pytest.LogCaptureFixture, |
|
| 2979 | 2645 |
main_config: str, |
| 2980 | 2646 |
command_line: list[str], |
| 2981 | 2647 |
input: str | None, |
| 2982 |
- error_message: str, |
|
| 2648 |
+ warning_message: str, |
|
| 2983 | 2649 |
) -> None: |
| 2984 |
- """Using unknown Unicode normalization forms fails.""" |
|
| 2985 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 2986 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 2987 |
- # with-statements. |
|
| 2988 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 2989 |
- with contextlib.ExitStack() as stack: |
|
| 2990 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 2991 |
- stack.enter_context( |
|
| 2992 |
- pytest_machinery.isolated_vault_config( |
|
| 2993 |
- monkeypatch=monkeypatch, |
|
| 2994 |
- runner=runner, |
|
| 2995 |
- vault_config={
|
|
| 2996 |
- "services": {
|
|
| 2997 |
- DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy() |
|
| 2998 |
- } |
|
| 2999 |
- }, |
|
| 3000 |
- main_config_str=main_config, |
|
| 3001 |
- ) |
|
| 3002 |
- ) |
|
| 3003 |
- result = runner.invoke( |
|
| 3004 |
- cli.derivepassphrase_vault, |
|
| 2650 |
+ """Using unnormalized Unicode passphrases warns.""" |
|
| 2651 |
+ self._test( |
|
| 3005 | 2652 |
command_line, |
| 3006 |
- catch_exceptions=False, |
|
| 2653 |
+ main_config=main_config, |
|
| 2654 |
+ message=warning_message, |
|
| 2655 |
+ caplog=caplog, |
|
| 3007 | 2656 |
input=input, |
| 3008 | 2657 |
) |
| 3009 |
- assert result.error_exit( |
|
| 3010 |
- error="The user configuration file is invalid." |
|
| 3011 |
- ), "expected error exit and known error message" |
|
| 3012 |
- assert result.error_exit(error=error_message), ( |
|
| 3013 |
- "expected error exit and known error message" |
|
| 3014 |
- ) |
|
| 3015 | 2658 |
|
| 3016 |
- @Parametrize.UNICODE_NORMALIZATION_COMMAND_LINES |
|
| 3017 |
- def test_error_from_stored_config( |
|
| 2659 |
+ @Parametrize.UNICODE_NORMALIZATION_ERROR_INPUTS |
|
| 2660 |
+ def test_error( |
|
| 3018 | 2661 |
self, |
| 2662 |
+ main_config: str, |
|
| 3019 | 2663 |
command_line: list[str], |
| 2664 |
+ input: str | None, |
|
| 2665 |
+ error_message: str, |
|
| 3020 | 2666 |
) -> None: |
| 3021 |
- """Using unknown Unicode normalization forms in the config fails.""" |
|
| 3022 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 3023 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 3024 |
- # with-statements. |
|
| 3025 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 3026 |
- with contextlib.ExitStack() as stack: |
|
| 3027 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 3028 |
- stack.enter_context( |
|
| 3029 |
- pytest_machinery.isolated_vault_config( |
|
| 3030 |
- monkeypatch=monkeypatch, |
|
| 3031 |
- runner=runner, |
|
| 3032 |
- vault_config={
|
|
| 3033 |
- "services": {
|
|
| 3034 |
- DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy() |
|
| 3035 |
- } |
|
| 3036 |
- }, |
|
| 3037 |
- main_config_str=( |
|
| 3038 |
- "[vault]\ndefault-unicode-normalization-form = 'XXX'\n" |
|
| 3039 |
- ), |
|
| 3040 |
- ) |
|
| 3041 |
- ) |
|
| 3042 |
- result = runner.invoke( |
|
| 3043 |
- cli.derivepassphrase_vault, |
|
| 2667 |
+ """Using unknown Unicode normalization forms fails.""" |
|
| 2668 |
+ self._test( |
|
| 3044 | 2669 |
command_line, |
| 3045 |
- input=DUMMY_PASSPHRASE, |
|
| 3046 |
- catch_exceptions=False, |
|
| 2670 |
+ main_config=main_config, |
|
| 2671 |
+ message=error_message, |
|
| 2672 |
+ input=input, |
|
| 3047 | 2673 |
) |
| 3048 |
- assert result.error_exit( |
|
| 3049 |
- error="The user configuration file is invalid." |
|
| 3050 |
- ), "expected error exit and known error message" |
|
| 3051 |
- assert result.error_exit( |
|
| 3052 |
- error=( |
|
| 3053 |
- "Invalid value 'XXX' for config key " |
|
| 3054 |
- "vault.default-unicode-normalization-form" |
|
| 3055 |
- ), |
|
| 3056 |
- ), "expected error exit and known error message" |
|
| 3057 | 2674 |
|
| 3058 | 2675 |
|
| 3059 | 2676 |
class TestUserConfigurationFileOther: |
| 3060 | 2677 |
"""Other tests concerning the user configuration file.""" |
| 3061 | 2678 |
|
| 3062 |
- def test_bad_user_config_file( |
|
| 2679 |
+ @contextlib.contextmanager |
|
| 2680 |
+ def _test( |
|
| 3063 | 2681 |
self, |
| 3064 |
- ) -> None: |
|
| 3065 |
- """Loading a user configuration file in an invalid format fails.""" |
|
| 2682 |
+ *, |
|
| 2683 |
+ main_config: str = "", |
|
| 2684 |
+ ) -> Iterator[pytest.MonkeyPatch]: |
|
| 3066 | 2685 |
runner = machinery.CliRunner(mix_stderr=False) |
| 3067 | 2686 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 3068 | 2687 |
# with-statements. |
| ... | ... |
@@ -3074,9 +2693,10 @@ class TestUserConfigurationFileOther: |
| 3074 | 2693 |
monkeypatch=monkeypatch, |
| 3075 | 2694 |
runner=runner, |
| 3076 | 2695 |
vault_config={"services": {}},
|
| 3077 |
- main_config_str="This file is not valid TOML.\n", |
|
| 2696 |
+ main_config_str=main_config, |
|
| 3078 | 2697 |
) |
| 3079 | 2698 |
) |
| 2699 |
+ yield monkeypatch |
|
| 3080 | 2700 |
result = runner.invoke( |
| 3081 | 2701 |
cli.derivepassphrase_vault, |
| 3082 | 2702 |
["--phrase", "--", DUMMY_SERVICE], |
| ... | ... |
@@ -3087,38 +2707,23 @@ class TestUserConfigurationFileOther: |
| 3087 | 2707 |
"expected error exit and known error message" |
| 3088 | 2708 |
) |
| 3089 | 2709 |
|
| 2710 |
+ def test_bad_user_config_file( |
|
| 2711 |
+ self, |
|
| 2712 |
+ ) -> None: |
|
| 2713 |
+ """Loading a user configuration file in an invalid format fails.""" |
|
| 2714 |
+ with self._test(main_config="This file is not valid TOML.\n"): |
|
| 2715 |
+ pass |
|
| 2716 |
+ |
|
| 3090 | 2717 |
def test_user_config_is_a_directory( |
| 3091 | 2718 |
self, |
| 3092 | 2719 |
) -> None: |
| 3093 | 2720 |
"""Loading a user configuration non-file fails.""" |
| 3094 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
| 3095 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
| 3096 |
- # with-statements. |
|
| 3097 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
| 3098 |
- with contextlib.ExitStack() as stack: |
|
| 3099 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 3100 |
- stack.enter_context( |
|
| 3101 |
- pytest_machinery.isolated_vault_config( |
|
| 3102 |
- monkeypatch=monkeypatch, |
|
| 3103 |
- runner=runner, |
|
| 3104 |
- vault_config={"services": {}},
|
|
| 3105 |
- main_config_str="", |
|
| 3106 |
- ) |
|
| 3107 |
- ) |
|
| 2721 |
+ with self._test(): |
|
| 3108 | 2722 |
user_config = cli_helpers.config_filename( |
| 3109 | 2723 |
subsystem="user configuration" |
| 3110 | 2724 |
) |
| 3111 | 2725 |
user_config.unlink() |
| 3112 | 2726 |
user_config.mkdir(parents=True, exist_ok=True) |
| 3113 |
- result = runner.invoke( |
|
| 3114 |
- cli.derivepassphrase_vault, |
|
| 3115 |
- ["--phrase", "--", DUMMY_SERVICE], |
|
| 3116 |
- input=DUMMY_PASSPHRASE, |
|
| 3117 |
- catch_exceptions=False, |
|
| 3118 |
- ) |
|
| 3119 |
- assert result.error_exit(error="Cannot load user config:"), ( |
|
| 3120 |
- "expected error exit and known error message" |
|
| 3121 |
- ) |
|
| 3122 | 2727 |
|
| 3123 | 2728 |
|
| 3124 | 2729 |
class TestSSHAgentAvailability: |
| 3125 | 2730 |