Marco Ricci commited on 2025-08-09 16:22:42
              Zeige 5 geänderte Dateien mit 4745 Einfügungen und 4597 Löschungen.
            
The CLI tests, already loosely grouped through the use of classes, are now distributed to different files, one file per test class. This is mostly an attempt to keep the file size managable. Navigating in a multi-thousand-line Python file with very similar looking tests gets disorienting very quickly.
| ... | ... | 
                      @@ -4,30 +4,19 @@  | 
                  
| 4 | 4 | 
                         | 
                    
| 5 | 5 | 
                        from __future__ import annotations  | 
                    
| 6 | 6 | 
                         | 
                    
| 7 | 
                        -import base64  | 
                    |
| 8 | 7 | 
                        import contextlib  | 
                    
| 9 | 8 | 
                        import copy  | 
                    
| 10 | 
                        -import ctypes  | 
                    |
| 11 | 
                        -import enum  | 
                    |
| 12 | 9 | 
                        import errno  | 
                    
| 13 | 
                        -import io  | 
                    |
| 14 | 10 | 
                        import json  | 
                    
| 15 | 
                        -import logging  | 
                    |
| 16 | 
                        -import operator  | 
                    |
| 17 | 11 | 
                        import os  | 
                    
| 18 | 12 | 
                        import pathlib  | 
                    
| 19 | 
                        -import re  | 
                    |
| 20 | 
                        -import shlex  | 
                    |
| 21 | 13 | 
                        import shutil  | 
                    
| 22 | 14 | 
                        import socket  | 
                    
| 23 | 
                        -import tempfile  | 
                    |
| 24 | 15 | 
                        import textwrap  | 
                    
| 25 | 16 | 
                        import types  | 
                    
| 26 | 
                        -import warnings  | 
                    |
| 27 | 17 | 
                        from typing import TYPE_CHECKING  | 
                    
| 28 | 18 | 
                         | 
                    
| 29 | 19 | 
                        import click.testing  | 
                    
| 30 | 
                        -import exceptiongroup  | 
                    |
| 31 | 20 | 
                        import hypothesis  | 
                    
| 32 | 21 | 
                        import pytest  | 
                    
| 33 | 22 | 
                        from hypothesis import strategies  | 
                    
| ... | ... | 
                      @@ -36,18 +25,14 @@ from typing_extensions import Any, NamedTuple  | 
                  
| 36 | 25 | 
                        from derivepassphrase import _types, cli, ssh_agent, vault  | 
                    
| 37 | 26 | 
                        from derivepassphrase._internals import (  | 
                    
| 38 | 27 | 
                        cli_helpers,  | 
                    
| 39 | 
                        - cli_machinery,  | 
                    |
| 40 | 28 | 
                        cli_messages,  | 
                    
| 41 | 29 | 
                        )  | 
                    
| 42 | 
                        -from derivepassphrase.ssh_agent import socketprovider  | 
                    |
| 43 | 30 | 
                        from tests import data, machinery  | 
                    
| 44 | 31 | 
                        from tests.data import callables  | 
                    
| 45 | 32 | 
                        from tests.machinery import hypothesis as hypothesis_machinery  | 
                    
| 46 | 33 | 
                        from tests.machinery import pytest as pytest_machinery  | 
                    
| 47 | 34 | 
                         | 
                    
| 48 | 35 | 
                        if TYPE_CHECKING:  | 
                    
| 49 | 
                        - from collections.abc import Callable, Iterable, Iterator, Sequence  | 
                    |
| 50 | 
                        - from collections.abc import Set as AbstractSet  | 
                    |
| 51 | 36 | 
                        from typing import NoReturn  | 
                    
| 52 | 37 | 
                         | 
                    
| 53 | 38 | 
                        from typing_extensions import Literal  | 
                    
| ... | ... | 
                      @@ -90,43 +75,6 @@ class OptionCombination(NamedTuple):  | 
                  
| 90 | 75 | 
                        check_success: bool  | 
                    
| 91 | 76 | 
                         | 
                    
| 92 | 77 | 
                         | 
                    
| 93 | 
                        -class VersionOutputData(NamedTuple):  | 
                    |
| 94 | 
                        - derivation_schemes: dict[str, bool]  | 
                    |
| 95 | 
                        - foreign_configuration_formats: dict[str, bool]  | 
                    |
| 96 | 
                        - extras: frozenset[str]  | 
                    |
| 97 | 
                        - subcommands: frozenset[str]  | 
                    |
| 98 | 
                        - features: dict[str, bool]  | 
                    |
| 99 | 
                        -  | 
                    |
| 100 | 
                        -  | 
                    |
| 101 | 
                        -class KnownLineType(str, enum.Enum):  | 
                    |
| 102 | 
                        - SUPPORTED_FOREIGN_CONFS = cli_messages.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS.value.singular.rstrip(  | 
                    |
| 103 | 
                        - ":"  | 
                    |
| 104 | 
                        - )  | 
                    |
| 105 | 
                        - UNAVAILABLE_FOREIGN_CONFS = cli_messages.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS.value.singular.rstrip(  | 
                    |
| 106 | 
                        - ":"  | 
                    |
| 107 | 
                        - )  | 
                    |
| 108 | 
                        - SUPPORTED_SCHEMES = (  | 
                    |
| 109 | 
                        - cli_messages.Label.SUPPORTED_DERIVATION_SCHEMES.value.singular.rstrip(  | 
                    |
| 110 | 
                        - ":"  | 
                    |
| 111 | 
                        - )  | 
                    |
| 112 | 
                        - )  | 
                    |
| 113 | 
                        - UNAVAILABLE_SCHEMES = cli_messages.Label.UNAVAILABLE_DERIVATION_SCHEMES.value.singular.rstrip(  | 
                    |
| 114 | 
                        - ":"  | 
                    |
| 115 | 
                        - )  | 
                    |
| 116 | 
                        - SUPPORTED_SUBCOMMANDS = (  | 
                    |
| 117 | 
                        -        cli_messages.Label.SUPPORTED_SUBCOMMANDS.value.singular.rstrip(":")
                       | 
                    |
| 118 | 
                        - )  | 
                    |
| 119 | 
                        - SUPPORTED_FEATURES = (  | 
                    |
| 120 | 
                        -        cli_messages.Label.SUPPORTED_FEATURES.value.singular.rstrip(":")
                       | 
                    |
| 121 | 
                        - )  | 
                    |
| 122 | 
                        - UNAVAILABLE_FEATURES = (  | 
                    |
| 123 | 
                        -        cli_messages.Label.UNAVAILABLE_FEATURES.value.singular.rstrip(":")
                       | 
                    |
| 124 | 
                        - )  | 
                    |
| 125 | 
                        - ENABLED_EXTRAS = (  | 
                    |
| 126 | 
                        -        cli_messages.Label.ENABLED_PEP508_EXTRAS.value.singular.rstrip(":")
                       | 
                    |
| 127 | 
                        - )  | 
                    |
| 128 | 
                        -  | 
                    |
| 129 | 
                        -  | 
                    |
| 130 | 78 | 
                        PASSPHRASE_GENERATION_OPTIONS: list[tuple[str, ...]] = [  | 
                    
| 131 | 79 | 
                             ("--phrase",),
                       | 
                    
| 132 | 80 | 
                             ("--key",),
                       | 
                    
| ... | ... | 
                      @@ -320,535 +268,12 @@ def assert_vault_config_is_indented_and_line_broken(  | 
                  
| 320 | 268 | 
                        ])  | 
                    
| 321 | 269 | 
                         | 
                    
| 322 | 270 | 
                         | 
                    
| 323 | 
                        -def vault_config_exporter_shell_interpreter( # noqa: C901  | 
                    |
| 324 | 
                        - script: str | Iterable[str],  | 
                    |
| 325 | 
                        - /,  | 
                    |
| 326 | 
                        - *,  | 
                    |
| 327 | 
                        - prog_name_list: list[str] | None = None,  | 
                    |
| 328 | 
                        - command: click.BaseCommand | None = None,  | 
                    |
| 329 | 
                        - runner: machinery.CliRunner | None = None,  | 
                    |
| 330 | 
                        -) -> Iterator[machinery.ReadableResult]:  | 
                    |
| 331 | 
                        - """A rudimentary sh(1) interpreter for `--export-as=sh` output.  | 
                    |
| 332 | 
                        -  | 
                    |
| 333 | 
                        - Assumes a script as emitted by `derivepassphrase vault  | 
                    |
| 334 | 
                        - --export-as=sh --export -` and interprets the calls to  | 
                    |
| 335 | 
                        - `derivepassphrase vault` within. (One call per line, skips all  | 
                    |
| 336 | 
                        - other lines.) Also has rudimentary support for (quoted)  | 
                    |
| 337 | 
                        - here-documents using `HERE` as the marker.  | 
                    |
| 338 | 
                        -  | 
                    |
| 339 | 
                        - """  | 
                    |
| 340 | 
                        - if isinstance(script, str): # pragma: no cover  | 
                    |
| 341 | 
                        - script = script.splitlines(False)  | 
                    |
| 342 | 
                        - if prog_name_list is None: # pragma: no cover  | 
                    |
| 343 | 
                        - prog_name_list = ["derivepassphrase", "vault"]  | 
                    |
| 344 | 
                        - if command is None: # pragma: no cover  | 
                    |
| 345 | 
                        - command = cli.derivepassphrase_vault  | 
                    |
| 346 | 
                        - if runner is None: # pragma: no cover  | 
                    |
| 347 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 348 | 
                        - n = len(prog_name_list)  | 
                    |
| 349 | 
                        - it = iter(script)  | 
                    |
| 350 | 
                        - while True:  | 
                    |
| 351 | 
                        - try:  | 
                    |
| 352 | 
                        - raw_line = next(it)  | 
                    |
| 353 | 
                        - except StopIteration:  | 
                    |
| 354 | 
                        - break  | 
                    |
| 355 | 
                        - else:  | 
                    |
| 356 | 
                        - line = shlex.split(raw_line)  | 
                    |
| 357 | 
                        - input_buffer: list[str] = []  | 
                    |
| 358 | 
                        - if line[:n] != prog_name_list:  | 
                    |
| 359 | 
                        - continue  | 
                    |
| 360 | 
                        - line[:n] = []  | 
                    |
| 361 | 
                        - if line and line[-1] == "<<HERE":  | 
                    |
| 362 | 
                        - # naive HERE document support  | 
                    |
| 363 | 
                        - while True:  | 
                    |
| 364 | 
                        - try:  | 
                    |
| 365 | 
                        - raw_line = next(it)  | 
                    |
| 366 | 
                        - except StopIteration as exc: # pragma: no cover  | 
                    |
| 367 | 
                        - msg = "incomplete here document"  | 
                    |
| 368 | 
                        - raise EOFError(msg) from exc  | 
                    |
| 369 | 
                        - else:  | 
                    |
| 370 | 
                        - if raw_line == "HERE":  | 
                    |
| 371 | 
                        - break  | 
                    |
| 372 | 
                        - input_buffer.append(raw_line)  | 
                    |
| 373 | 
                        - line.pop()  | 
                    |
| 374 | 
                        - yield runner.invoke(  | 
                    |
| 375 | 
                        - command,  | 
                    |
| 376 | 
                        - line,  | 
                    |
| 377 | 
                        - catch_exceptions=False,  | 
                    |
| 378 | 
                        -            input=("".join(x + "\n" for x in input_buffer) or None),
                       | 
                    |
| 379 | 
                        - )  | 
                    |
| 380 | 
                        -  | 
                    |
| 381 | 
                        -  | 
                    |
| 382 | 
                        -def parse_version_output( # noqa: C901  | 
                    |
| 383 | 
                        - version_output: str,  | 
                    |
| 384 | 
                        - /,  | 
                    |
| 385 | 
                        - *,  | 
                    |
| 386 | 
                        - prog_name: str | None = cli_messages.PROG_NAME,  | 
                    |
| 387 | 
                        - version: str | None = cli_messages.VERSION,  | 
                    |
| 388 | 
                        -) -> VersionOutputData:  | 
                    |
| 389 | 
                        - r"""Parse the output of the `--version` option.  | 
                    |
| 390 | 
                        -  | 
                    |
| 391 | 
                        - The version output contains two paragraphs. The first paragraph  | 
                    |
| 392 | 
                        - details the version number, and the version number of any major  | 
                    |
| 393 | 
                        - libraries in use. The second paragraph details known and supported  | 
                    |
| 394 | 
                        - passphrase derivation schemes, foreign configuration formats,  | 
                    |
| 395 | 
                        - subcommands and PEP 508 package extras. For the schemes and  | 
                    |
| 396 | 
                        - formats, there is a "supported" line for supported items, and  | 
                    |
| 397 | 
                        - a "known" line for known but currently unsupported items (usually  | 
                    |
| 398 | 
                        - because of missing dependencies), either of which may be empty and  | 
                    |
| 399 | 
                        - thus omitted. For extras, only active items are shown, and there is  | 
                    |
| 400 | 
                        - a separate message for the "no extras active" case. Item lists may  | 
                    |
| 401 | 
                        - be spilled across multiple lines, but only at item boundaries, and  | 
                    |
| 402 | 
                        - the continuation lines are then indented.  | 
                    |
| 403 | 
                        -  | 
                    |
| 404 | 
                        - Args:  | 
                    |
| 405 | 
                        - version_output:  | 
                    |
| 406 | 
                        - The version output text to parse.  | 
                    |
| 407 | 
                        - prog_name:  | 
                    |
| 408 | 
                        - The program name to assert, defaulting to the true program  | 
                    |
| 409 | 
                        - name, `derivepassphrase`. Set to `None` to disable this  | 
                    |
| 410 | 
                        - check.  | 
                    |
| 411 | 
                        - version:  | 
                    |
| 412 | 
                        - The program version to assert, defaulting to the true  | 
                    |
| 413 | 
                        - current version of `derivepassphrase`. Set to `None` to  | 
                    |
| 414 | 
                        - disable this check.  | 
                    |
| 415 | 
                        -  | 
                    |
| 416 | 
                        - Examples:  | 
                    |
| 417 | 
                        - See [`Parametrize.VERSION_OUTPUT_DATA`][].  | 
                    |
| 418 | 
                        -  | 
                    |
| 419 | 
                        - """  | 
                    |
| 420 | 
                        - paragraphs: list[list[str]] = []  | 
                    |
| 421 | 
                        - paragraph: list[str] = []  | 
                    |
| 422 | 
                        - for line in version_output.splitlines(keepends=False):  | 
                    |
| 423 | 
                        - if not line.strip():  | 
                    |
| 424 | 
                        - if paragraph:  | 
                    |
| 425 | 
                        - paragraphs.append(paragraph.copy())  | 
                    |
| 426 | 
                        - paragraph.clear()  | 
                    |
| 427 | 
                        - elif paragraph and line.lstrip() != line:  | 
                    |
| 428 | 
                        -            paragraph[-1] = f"{paragraph[-1]} {line.lstrip()}"
                       | 
                    |
| 429 | 
                        - else:  | 
                    |
| 430 | 
                        - paragraph.append(line)  | 
                    |
| 431 | 
                        - if paragraph: # pragma: no branch  | 
                    |
| 432 | 
                        - paragraphs.append(paragraph.copy())  | 
                    |
| 433 | 
                        - paragraph.clear()  | 
                    |
| 434 | 
                        - assert paragraphs, (  | 
                    |
| 435 | 
                        -        f"expected at least one paragraph of version output: {paragraphs!r}"
                       | 
                    |
| 436 | 
                        - )  | 
                    |
| 437 | 
                        - assert prog_name is None or prog_name in paragraphs[0][0], (  | 
                    |
| 438 | 
                        - f"first version output line should mention "  | 
                    |
| 439 | 
                        -        f"{prog_name}: {paragraphs[0][0]!r}"
                       | 
                    |
| 440 | 
                        - )  | 
                    |
| 441 | 
                        - assert version is None or version in paragraphs[0][0], (  | 
                    |
| 442 | 
                        - f"first version output line should mention the version number "  | 
                    |
| 443 | 
                        -        f"{version}: {paragraphs[0][0]!r}"
                       | 
                    |
| 444 | 
                        - )  | 
                    |
| 445 | 
                        -    schemes: dict[str, bool] = {}
                       | 
                    |
| 446 | 
                        -    formats: dict[str, bool] = {}
                       | 
                    |
| 447 | 
                        - subcommands: set[str] = set()  | 
                    |
| 448 | 
                        - extras: set[str] = set()  | 
                    |
| 449 | 
                        -    features: dict[str, bool] = {}
                       | 
                    |
| 450 | 
                        - if len(paragraphs) < 2: # pragma: no cover  | 
                    |
| 451 | 
                        - return VersionOutputData(  | 
                    |
| 452 | 
                        - derivation_schemes=schemes,  | 
                    |
| 453 | 
                        - foreign_configuration_formats=formats,  | 
                    |
| 454 | 
                        - subcommands=frozenset(subcommands),  | 
                    |
| 455 | 
                        - extras=frozenset(extras),  | 
                    |
| 456 | 
                        - features=features,  | 
                    |
| 457 | 
                        - )  | 
                    |
| 458 | 
                        - for line in paragraphs[1]:  | 
                    |
| 459 | 
                        -        line_type, _, value = line.partition(":")
                       | 
                    |
| 460 | 
                        - if line_type == line:  | 
                    |
| 461 | 
                        - continue  | 
                    |
| 462 | 
                        - for item_ in re.split(r"(?:, *|.$)", value):  | 
                    |
| 463 | 
                        - item = item_.strip()  | 
                    |
| 464 | 
                        - if not item:  | 
                    |
| 465 | 
                        - continue  | 
                    |
| 466 | 
                        - if line_type == KnownLineType.SUPPORTED_FOREIGN_CONFS:  | 
                    |
| 467 | 
                        - formats[item] = True  | 
                    |
| 468 | 
                        - elif line_type == KnownLineType.UNAVAILABLE_FOREIGN_CONFS:  | 
                    |
| 469 | 
                        - formats[item] = False  | 
                    |
| 470 | 
                        - elif line_type == KnownLineType.SUPPORTED_SCHEMES:  | 
                    |
| 471 | 
                        - schemes[item] = True  | 
                    |
| 472 | 
                        - elif line_type == KnownLineType.UNAVAILABLE_SCHEMES:  | 
                    |
| 473 | 
                        - schemes[item] = False  | 
                    |
| 474 | 
                        - elif line_type == KnownLineType.SUPPORTED_SUBCOMMANDS:  | 
                    |
| 475 | 
                        - subcommands.add(item)  | 
                    |
| 476 | 
                        - elif line_type == KnownLineType.ENABLED_EXTRAS:  | 
                    |
| 477 | 
                        - extras.add(item)  | 
                    |
| 478 | 
                        - elif line_type == KnownLineType.SUPPORTED_FEATURES:  | 
                    |
| 479 | 
                        - features[item] = True  | 
                    |
| 480 | 
                        - elif line_type == KnownLineType.UNAVAILABLE_FEATURES:  | 
                    |
| 481 | 
                        - features[item] = False  | 
                    |
| 482 | 
                        - else:  | 
                    |
| 483 | 
                        - raise AssertionError( # noqa: TRY003  | 
                    |
| 484 | 
                        -                    f"Unknown version info line type: {line_type!r}"  # noqa: EM102
                       | 
                    |
| 485 | 
                        - )  | 
                    |
| 486 | 
                        - return VersionOutputData(  | 
                    |
| 487 | 
                        - derivation_schemes=schemes,  | 
                    |
| 488 | 
                        - foreign_configuration_formats=formats,  | 
                    |
| 489 | 
                        - subcommands=frozenset(subcommands),  | 
                    |
| 490 | 
                        - extras=frozenset(extras),  | 
                    |
| 491 | 
                        - features=features,  | 
                    |
| 492 | 
                        - )  | 
                    |
| 493 | 
                        -  | 
                    |
| 494 | 
                        -  | 
                    |
| 495 | 
                        -def bash_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 496 | 
                        - """A formatter for `bash`-style shell completion items.  | 
                    |
| 497 | 
                        -  | 
                    |
| 498 | 
                        - The format is `type,value`, and is dictated by [`click`][].  | 
                    |
| 499 | 
                        -  | 
                    |
| 500 | 
                        - """  | 
                    |
| 501 | 
                        - type, value = ( # noqa: A001  | 
                    |
| 502 | 
                        - item.type,  | 
                    |
| 503 | 
                        - item.value,  | 
                    |
| 504 | 
                        - )  | 
                    |
| 505 | 
                        -    return f"{type},{value}"
                       | 
                    |
| 506 | 
                        -  | 
                    |
| 507 | 
                        -  | 
                    |
| 508 | 
                        -def fish_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 509 | 
                        - r"""A formatter for `fish`-style shell completion items.  | 
                    |
| 510 | 
                        -  | 
                    |
| 511 | 
                        - The format is `type,value<tab>help`, and is dictated by [`click`][].  | 
                    |
| 512 | 
                        -  | 
                    |
| 513 | 
                        - """  | 
                    |
| 514 | 
                        - type, value, help = ( # noqa: A001  | 
                    |
| 515 | 
                        - item.type,  | 
                    |
| 516 | 
                        - item.value,  | 
                    |
| 517 | 
                        - item.help,  | 
                    |
| 518 | 
                        - )  | 
                    |
| 519 | 
                        -    return f"{type},{value}\t{help}" if help else f"{type},{value}"
                       | 
                    |
| 520 | 
                        -  | 
                    |
| 521 | 
                        -  | 
                    |
| 522 | 
                        -def zsh_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 523 | 
                        - r"""A formatter for `zsh`-style shell completion items.  | 
                    |
| 524 | 
                        -  | 
                    |
| 525 | 
                        - The format is `type<newline>value<newline>help<newline>`, and is  | 
                    |
| 526 | 
                        - dictated by [`click`][]. Upstream `click` currently (v8.2.0) does  | 
                    |
| 527 | 
                        - not deal with colons in the value correctly when the help text is  | 
                    |
| 528 | 
                        - non-degenerate. Our formatter here does, provided the upstream  | 
                    |
| 529 | 
                        - `zsh` completion script is used; see the  | 
                    |
| 530 | 
                        - [`cli_machinery.ZshComplete`][] class. A request is underway to  | 
                    |
| 531 | 
                        - merge this change into upstream `click`; see  | 
                    |
| 532 | 
                        - [`pallets/click#2846`][PR2846].  | 
                    |
| 533 | 
                        -  | 
                    |
| 534 | 
                        - [PR2846]: https://github.com/pallets/click/pull/2846  | 
                    |
| 535 | 
                        -  | 
                    |
| 536 | 
                        - """  | 
                    |
| 537 | 
                        - empty_help = "_"  | 
                    |
| 538 | 
                        - help_, value = (  | 
                    |
| 539 | 
                        -        (item.help, item.value.replace(":", r"\:"))
                       | 
                    |
| 540 | 
                        - if item.help and item.help == empty_help  | 
                    |
| 541 | 
                        - else (empty_help, item.value)  | 
                    |
| 542 | 
                        - )  | 
                    |
| 543 | 
                        -    return f"{item.type}\n{value}\n{help_}"
                       | 
                    |
| 544 | 
                        -  | 
                    |
| 545 | 
                        -  | 
                    |
| 546 | 
                        -class ListKeysAction(str, enum.Enum):  | 
                    |
| 547 | 
                        - """Test fixture settings for [`ssh_agent.SSHAgentClient.list_keys`][].  | 
                    |
| 548 | 
                        -  | 
                    |
| 549 | 
                        - Attributes:  | 
                    |
| 550 | 
                        - EMPTY: Return an empty key list.  | 
                    |
| 551 | 
                        - FAIL: Raise an [`ssh_agent.SSHAgentFailedError`][].  | 
                    |
| 552 | 
                        - FAIL_RUNTIME: Raise an [`ssh_agent.TrailingDataError`][].  | 
                    |
| 553 | 
                        -  | 
                    |
| 554 | 
                        - """  | 
                    |
| 555 | 
                        -  | 
                    |
| 556 | 
                        - EMPTY = enum.auto()  | 
                    |
| 557 | 
                        - """"""  | 
                    |
| 558 | 
                        - FAIL = enum.auto()  | 
                    |
| 559 | 
                        - """"""  | 
                    |
| 560 | 
                        - FAIL_RUNTIME = enum.auto()  | 
                    |
| 561 | 
                        - """"""  | 
                    |
| 562 | 
                        -  | 
                    |
| 563 | 
                        - def __call__(self, *_args: Any, **_kwargs: Any) -> Any:  | 
                    |
| 564 | 
                        - """Execute the respective action."""  | 
                    |
| 565 | 
                        - # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 566 | 
                        - # matching.  | 
                    |
| 567 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 568 | 
                        - if self == self.EMPTY:  | 
                    |
| 569 | 
                        - return []  | 
                    |
| 570 | 
                        - if self == self.FAIL:  | 
                    |
| 571 | 
                        - raise ssh_agent.SSHAgentFailedError(  | 
                    |
| 572 | 
                        - _types.SSH_AGENT.FAILURE.value, b""  | 
                    |
| 573 | 
                        - )  | 
                    |
| 574 | 
                        - if self == self.FAIL_RUNTIME:  | 
                    |
| 575 | 
                        - raise ssh_agent.TrailingDataError()  | 
                    |
| 576 | 
                        - raise AssertionError()  | 
                    |
| 577 | 
                        -  | 
                    |
| 578 | 
                        -  | 
                    |
| 579 | 
                        -class SignAction(str, enum.Enum):  | 
                    |
| 580 | 
                        - """Test fixture settings for [`ssh_agent.SSHAgentClient.sign`][].  | 
                    |
| 581 | 
                        -  | 
                    |
| 582 | 
                        - Attributes:  | 
                    |
| 583 | 
                        - FAIL: Raise an [`ssh_agent.SSHAgentFailedError`][].  | 
                    |
| 584 | 
                        - FAIL_RUNTIME: Raise an [`ssh_agent.TrailingDataError`][].  | 
                    |
| 585 | 
                        -  | 
                    |
| 586 | 
                        - """  | 
                    |
| 587 | 
                        -  | 
                    |
| 588 | 
                        - FAIL = enum.auto()  | 
                    |
| 589 | 
                        - """"""  | 
                    |
| 590 | 
                        - FAIL_RUNTIME = enum.auto()  | 
                    |
| 591 | 
                        - """"""  | 
                    |
| 592 | 
                        -  | 
                    |
| 593 | 
                        - def __call__(self, *_args: Any, **_kwargs: Any) -> Any:  | 
                    |
| 594 | 
                        - """Execute the respective action."""  | 
                    |
| 595 | 
                        - # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 596 | 
                        - # matching.  | 
                    |
| 597 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 598 | 
                        - if self == self.FAIL:  | 
                    |
| 599 | 
                        - raise ssh_agent.SSHAgentFailedError(  | 
                    |
| 600 | 
                        - _types.SSH_AGENT.FAILURE.value, b""  | 
                    |
| 601 | 
                        - )  | 
                    |
| 602 | 
                        - if self == self.FAIL_RUNTIME:  | 
                    |
| 603 | 
                        - raise ssh_agent.TrailingDataError()  | 
                    |
| 604 | 
                        - raise AssertionError()  | 
                    |
| 605 | 
                        -  | 
                    |
| 606 | 
                        -  | 
                    |
| 607 | 
                        -class SocketAddressAction(str, enum.Enum):  | 
                    |
| 608 | 
                        - """Test fixture settings for the SSH agent socket address.  | 
                    |
| 609 | 
                        -  | 
                    |
| 610 | 
                        - Attributes:  | 
                    |
| 611 | 
                        - MANGLE_ANNOYING_OS_NAMED_PIPE:  | 
                    |
| 612 | 
                        - Mangle the address for the Annoying OS named pipe endpoint.  | 
                    |
| 613 | 
                        - MANGLE_SSH_AUTH_SOCK:  | 
                    |
| 614 | 
                        - Mangle the address for the UNIX domain socket (the  | 
                    |
| 615 | 
                        - `SSH_AUTH_SOCK` environment variable).  | 
                    |
| 616 | 
                        - UNSET_ANNOYING_OS_NAMED_PIPE:  | 
                    |
| 617 | 
                        - Unset the address for the Annoying OS named pipe endpoint.  | 
                    |
| 618 | 
                        - UNSET_SSH_AUTH_SOCK:  | 
                    |
| 619 | 
                        - Unset the `SSH_AUTH_SOCK` environment variable (the address  | 
                    |
| 620 | 
                        - for the UNIX domain socket).  | 
                    |
| 621 | 
                        -  | 
                    |
| 622 | 
                        - """  | 
                    |
| 623 | 
                        -  | 
                    |
| 624 | 
                        - MANGLE_ANNOYING_OS_NAMED_PIPE = enum.auto()  | 
                    |
| 625 | 
                        - """"""  | 
                    |
| 626 | 
                        - MANGLE_SSH_AUTH_SOCK = enum.auto()  | 
                    |
| 627 | 
                        - """"""  | 
                    |
| 628 | 
                        - UNSET_ANNOYING_OS_NAMED_PIPE = enum.auto()  | 
                    |
| 629 | 
                        - """"""  | 
                    |
| 630 | 
                        - UNSET_SSH_AUTH_SOCK = enum.auto()  | 
                    |
| 631 | 
                        - """"""  | 
                    |
| 632 | 
                        -  | 
                    |
| 633 | 
                        - def __call__(  | 
                    |
| 634 | 
                        - self, monkeypatch: pytest.MonkeyPatch, /, *_args: Any, **_kwargs: Any  | 
                    |
| 635 | 
                        - ) -> None:  | 
                    |
| 636 | 
                        - """Execute the respective action."""  | 
                    |
| 637 | 
                        - # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 638 | 
                        - # matching.  | 
                    |
| 639 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 640 | 
                        -        if self in {
                       | 
                    |
| 641 | 
                        - self.MANGLE_ANNOYING_OS_NAMED_PIPE,  | 
                    |
| 642 | 
                        - self.UNSET_ANNOYING_OS_NAMED_PIPE,  | 
                    |
| 643 | 
                        - }: # pragma: no cover [unused]  | 
                    |
| 644 | 
                        - pass  | 
                    |
| 645 | 
                        - elif self == self.MANGLE_SSH_AUTH_SOCK:  | 
                    |
| 646 | 
                        - monkeypatch.setenv(  | 
                    |
| 647 | 
                        - "SSH_AUTH_SOCK", os.environ["SSH_AUTH_SOCK"] + "~"  | 
                    |
| 648 | 
                        - )  | 
                    |
| 649 | 
                        - elif self == self.UNSET_SSH_AUTH_SOCK:  | 
                    |
| 650 | 
                        -            monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
                       | 
                    |
| 651 | 
                        - else:  | 
                    |
| 652 | 
                        - raise AssertionError()  | 
                    |
| 653 | 
                        -  | 
                    |
| 654 | 
                        -  | 
                    |
| 655 | 
                        -class SystemSupportAction(str, enum.Enum):  | 
                    |
| 656 | 
                        - """Test fixture settings for [`ssh_agent.SSHAgentClient`][] system support.  | 
                    |
| 657 | 
                        -  | 
                    |
| 658 | 
                        - Attributes:  | 
                    |
| 659 | 
                        - UNSET_AF_UNIX:  | 
                    |
| 660 | 
                        - Ensure lack of support for UNIX domain sockets.  | 
                    |
| 661 | 
                        - UNSET_AF_UNIX_AND_ENSURE_USE:  | 
                    |
| 662 | 
                        - Ensure lack of support for UNIX domain sockets, and that the  | 
                    |
| 663 | 
                        - agent will use this socket provider.  | 
                    |
| 664 | 
                        - UNSET_NATIVE:  | 
                    |
| 665 | 
                        - Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`.  | 
                    |
| 666 | 
                        - UNSET_NATIVE_AND_ENSURE_USE:  | 
                    |
| 667 | 
                        - Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`, and that the  | 
                    |
| 668 | 
                        - agent will use the native socket provider.  | 
                    |
| 669 | 
                        - UNSET_PROVIDER_LIST:  | 
                    |
| 670 | 
                        - Ensure an empty list of SSH agent socket providers.  | 
                    |
| 671 | 
                        - UNSET_WINDLL:  | 
                    |
| 672 | 
                        - Ensure lack of support for The Annoying OS named pipes.  | 
                    |
| 673 | 
                        - UNSET_WINDLL_AND_ENSURE_USE:  | 
                    |
| 674 | 
                        - Ensure lack of support for The Annoying OS named pipes, and  | 
                    |
| 675 | 
                        - that the agent will use this socket provider.  | 
                    |
| 676 | 
                        -  | 
                    |
| 677 | 
                        - """  | 
                    |
| 678 | 
                        -  | 
                    |
| 679 | 
                        - UNSET_AF_UNIX = enum.auto()  | 
                    |
| 680 | 
                        - """"""  | 
                    |
| 681 | 
                        - UNSET_AF_UNIX_AND_ENSURE_USE = enum.auto()  | 
                    |
| 682 | 
                        - """"""  | 
                    |
| 683 | 
                        - UNSET_NATIVE = enum.auto()  | 
                    |
| 684 | 
                        - """"""  | 
                    |
| 685 | 
                        - UNSET_NATIVE_AND_ENSURE_USE = enum.auto()  | 
                    |
| 686 | 
                        - """"""  | 
                    |
| 687 | 
                        - UNSET_PROVIDER_LIST = enum.auto()  | 
                    |
| 688 | 
                        - """"""  | 
                    |
| 689 | 
                        - UNSET_WINDLL = enum.auto()  | 
                    |
| 690 | 
                        - """"""  | 
                    |
| 691 | 
                        - UNSET_WINDLL_AND_ENSURE_USE = enum.auto()  | 
                    |
| 692 | 
                        - """"""  | 
                    |
| 693 | 
                        -  | 
                    |
| 694 | 
                        - def __call__(  | 
                    |
| 695 | 
                        - self, monkeypatch: pytest.MonkeyPatch, /, *_args: Any, **_kwargs: Any  | 
                    |
| 696 | 
                        - ) -> None:  | 
                    |
| 697 | 
                        - """Execute the respective action.  | 
                    |
| 698 | 
                        -  | 
                    |
| 699 | 
                        - Args:  | 
                    |
| 700 | 
                        - monkeypatch: The current monkeypatch context.  | 
                    |
| 701 | 
                        -  | 
                    |
| 702 | 
                        - """  | 
                    |
| 703 | 
                        - # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 704 | 
                        - # matching.  | 
                    |
| 705 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 706 | 
                        - if self == self.UNSET_PROVIDER_LIST:  | 
                    |
| 707 | 
                        - monkeypatch.setattr(  | 
                    |
| 708 | 
                        - ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", []  | 
                    |
| 709 | 
                        - )  | 
                    |
| 710 | 
                        -        elif self in {self.UNSET_NATIVE, self.UNSET_NATIVE_AND_ENSURE_USE}:
                       | 
                    |
| 711 | 
                        - self.check_or_ensure_use(  | 
                    |
| 712 | 
                        - "native",  | 
                    |
| 713 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 714 | 
                        - ensure_use=(self == self.UNSET_NATIVE_AND_ENSURE_USE),  | 
                    |
| 715 | 
                        - )  | 
                    |
| 716 | 
                        - monkeypatch.delattr(socket, "AF_UNIX", raising=False)  | 
                    |
| 717 | 
                        - monkeypatch.delattr(ctypes, "WinDLL", raising=False)  | 
                    |
| 718 | 
                        - monkeypatch.delattr(ctypes, "windll", raising=False)  | 
                    |
| 719 | 
                        -        elif self in {self.UNSET_AF_UNIX, self.UNSET_AF_UNIX_AND_ENSURE_USE}:
                       | 
                    |
| 720 | 
                        - self.check_or_ensure_use(  | 
                    |
| 721 | 
                        - "posix",  | 
                    |
| 722 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 723 | 
                        - ensure_use=(self == self.UNSET_AF_UNIX_AND_ENSURE_USE),  | 
                    |
| 724 | 
                        - )  | 
                    |
| 725 | 
                        - monkeypatch.delattr(socket, "AF_UNIX", raising=False)  | 
                    |
| 726 | 
                        -        elif self in {self.UNSET_WINDLL, self.UNSET_WINDLL_AND_ENSURE_USE}:
                       | 
                    |
| 727 | 
                        - self.check_or_ensure_use(  | 
                    |
| 728 | 
                        - "the_annoying_os",  | 
                    |
| 729 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 730 | 
                        - ensure_use=(self == self.UNSET_WINDLL_AND_ENSURE_USE),  | 
                    |
| 731 | 
                        - )  | 
                    |
| 732 | 
                        - monkeypatch.delattr(ctypes, "WinDLL", raising=False)  | 
                    |
| 733 | 
                        - monkeypatch.delattr(ctypes, "windll", raising=False)  | 
                    |
| 734 | 
                        - else:  | 
                    |
| 735 | 
                        - raise AssertionError()  | 
                    |
| 736 | 
                        -  | 
                    |
| 737 | 
                        - @staticmethod  | 
                    |
| 738 | 
                        - def check_or_ensure_use(  | 
                    |
| 739 | 
                        - provider: str, /, *, monkeypatch: pytest.MonkeyPatch, ensure_use: bool  | 
                    |
| 740 | 
                        - ) -> None:  | 
                    |
| 741 | 
                        - """Check that the named SSH agent socket provider will be used.  | 
                    |
| 742 | 
                        -  | 
                    |
| 743 | 
                        - Either ensure that the socket provider will definitely be used,  | 
                    |
| 744 | 
                        - or, upon detecting that it won't be used, skip the test.  | 
                    |
| 745 | 
                        -  | 
                    |
| 746 | 
                        - Args:  | 
                    |
| 747 | 
                        - provider:  | 
                    |
| 748 | 
                        - The provider to check for.  | 
                    |
| 749 | 
                        - ensure_use:  | 
                    |
| 750 | 
                        - If true, ensure that the socket provider will definitely  | 
                    |
| 751 | 
                        - be used. If false, then check for whether it will be  | 
                    |
| 752 | 
                        - used, and skip this test if not.  | 
                    |
| 753 | 
                        - monkeypatch:  | 
                    |
| 754 | 
                        - The monkeypatch context within which the fixture  | 
                    |
| 755 | 
                        - adjustments should be executed.  | 
                    |
| 756 | 
                        -  | 
                    |
| 757 | 
                        - """  | 
                    |
| 758 | 
                        - if ensure_use:  | 
                    |
| 759 | 
                        - monkeypatch.setattr(  | 
                    |
| 760 | 
                        - ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", [provider]  | 
                    |
| 761 | 
                        - )  | 
                    |
| 762 | 
                        - else: # pragma: no cover [external]  | 
                    |
| 763 | 
                        - # This branch operates completely on instrumented or on  | 
                    |
| 764 | 
                        - # externally defined, non-deterministic state.  | 
                    |
| 765 | 
                        - intended: (  | 
                    |
| 766 | 
                        - _types.SSHAgentSocketProvider  | 
                    |
| 767 | 
                        - | socketprovider.NoSuchProviderError  | 
                    |
| 768 | 
                        - | None  | 
                    |
| 769 | 
                        - )  | 
                    |
| 770 | 
                        - try:  | 
                    |
| 771 | 
                        - intended = socketprovider.SocketProvider.lookup(provider)  | 
                    |
| 772 | 
                        - except socketprovider.NoSuchProviderError as exc:  | 
                    |
| 773 | 
                        - intended = exc  | 
                    |
| 774 | 
                        - actual: (  | 
                    |
| 775 | 
                        - _types.SSHAgentSocketProvider  | 
                    |
| 776 | 
                        - | socketprovider.NoSuchProviderError  | 
                    |
| 777 | 
                        - | None  | 
                    |
| 778 | 
                        - )  | 
                    |
| 779 | 
                        - for name in ssh_agent.SSHAgentClient.SOCKET_PROVIDERS:  | 
                    |
| 780 | 
                        - try:  | 
                    |
| 781 | 
                        - actual = socketprovider.SocketProvider.lookup(name)  | 
                    |
| 782 | 
                        - except socketprovider.NoSuchProviderError as exc:  | 
                    |
| 783 | 
                        - actual = exc  | 
                    |
| 784 | 
                        - if actual is None:  | 
                    |
| 785 | 
                        - continue  | 
                    |
| 786 | 
                        - break  | 
                    |
| 787 | 
                        - else:  | 
                    |
| 788 | 
                        - actual = None  | 
                    |
| 789 | 
                        - if intended != actual:  | 
                    |
| 790 | 
                        - pytest.skip(  | 
                    |
| 791 | 
                        -                    f"{provider!r} SSH agent socket provider "
                       | 
                    |
| 792 | 
                        - f"is not currently in use"  | 
                    |
| 793 | 
                        - )  | 
                    |
| 794 | 
                        -  | 
                    |
| 795 | 
                        -  | 
                    |
| 796 | 271 | 
                        class Parametrize(types.SimpleNamespace):  | 
                    
| 797 | 272 | 
                        """Common test parametrizations."""  | 
                    
| 798 | 273 | 
                         | 
                    
| 799 | 
                        - EAGER_ARGUMENTS = pytest.mark.parametrize(  | 
                    |
| 800 | 
                        - "arguments",  | 
                    |
| 801 | 
                        - [["--help"], ["--version"]],  | 
                    |
| 802 | 
                        - ids=["help", "version"],  | 
                    |
| 803 | 
                        - )  | 
                    |
| 804 | 274 | 
                        CHARSET_NAME = pytest.mark.parametrize(  | 
                    
| 805 | 275 | 
                        "charset_name", ["lower", "upper", "number", "space", "dash", "symbol"]  | 
                    
| 806 | 276 | 
                        )  | 
                    
| 807 | 
                        - COMMAND_NON_EAGER_ARGUMENTS = pytest.mark.parametrize(  | 
                    |
| 808 | 
                        - ["command", "non_eager_arguments"],  | 
                    |
| 809 | 
                        - [  | 
                    |
| 810 | 
                        - pytest.param(  | 
                    |
| 811 | 
                        - [],  | 
                    |
| 812 | 
                        - [],  | 
                    |
| 813 | 
                        - id="top-nothing",  | 
                    |
| 814 | 
                        - ),  | 
                    |
| 815 | 
                        - pytest.param(  | 
                    |
| 816 | 
                        - [],  | 
                    |
| 817 | 
                        - ["export"],  | 
                    |
| 818 | 
                        - id="top-export",  | 
                    |
| 819 | 
                        - ),  | 
                    |
| 820 | 
                        - pytest.param(  | 
                    |
| 821 | 
                        - ["export"],  | 
                    |
| 822 | 
                        - [],  | 
                    |
| 823 | 
                        - id="export-nothing",  | 
                    |
| 824 | 
                        - ),  | 
                    |
| 825 | 
                        - pytest.param(  | 
                    |
| 826 | 
                        - ["export"],  | 
                    |
| 827 | 
                        - ["vault"],  | 
                    |
| 828 | 
                        - id="export-vault",  | 
                    |
| 829 | 
                        - ),  | 
                    |
| 830 | 
                        - pytest.param(  | 
                    |
| 831 | 
                        - ["export", "vault"],  | 
                    |
| 832 | 
                        - [],  | 
                    |
| 833 | 
                        - id="export-vault-nothing",  | 
                    |
| 834 | 
                        - ),  | 
                    |
| 835 | 
                        - pytest.param(  | 
                    |
| 836 | 
                        - ["export", "vault"],  | 
                    |
| 837 | 
                        - ["--format", "this-format-doesnt-exist"],  | 
                    |
| 838 | 
                        - id="export-vault-args",  | 
                    |
| 839 | 
                        - ),  | 
                    |
| 840 | 
                        - pytest.param(  | 
                    |
| 841 | 
                        - ["vault"],  | 
                    |
| 842 | 
                        - [],  | 
                    |
| 843 | 
                        - id="vault-nothing",  | 
                    |
| 844 | 
                        - ),  | 
                    |
| 845 | 
                        - pytest.param(  | 
                    |
| 846 | 
                        - ["vault"],  | 
                    |
| 847 | 
                        - ["--export", "./"],  | 
                    |
| 848 | 
                        - id="vault-args",  | 
                    |
| 849 | 
                        - ),  | 
                    |
| 850 | 
                        - ],  | 
                    |
| 851 | 
                        - )  | 
                    |
| 852 | 277 | 
                        UNICODE_NORMALIZATION_COMMAND_LINES = pytest.mark.parametrize(  | 
                    
| 853 | 278 | 
                        "command_line",  | 
                    
| 854 | 279 | 
                        [  | 
                    
| ... | ... | 
                      @@ -866,45 +291,6 @@ class Parametrize(types.SimpleNamespace):  | 
                  
| 866 | 291 | 
                        ),  | 
                    
| 867 | 292 | 
                        ],  | 
                    
| 868 | 293 | 
                        )  | 
                    
| 869 | 
                        - DELETE_CONFIG_INPUT = pytest.mark.parametrize(  | 
                    |
| 870 | 
                        - ["command_line", "config", "result_config"],  | 
                    |
| 871 | 
                        - [  | 
                    |
| 872 | 
                        - pytest.param(  | 
                    |
| 873 | 
                        - ["--delete-globals"],  | 
                    |
| 874 | 
                        -                {"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 875 | 
                        -                {"services": {}},
                       | 
                    |
| 876 | 
                        - id="globals",  | 
                    |
| 877 | 
                        - ),  | 
                    |
| 878 | 
                        - pytest.param(  | 
                    |
| 879 | 
                        - ["--delete", "--", DUMMY_SERVICE],  | 
                    |
| 880 | 
                        -                {
                       | 
                    |
| 881 | 
                        -                    "global": {"phrase": "abc"},
                       | 
                    |
| 882 | 
                        -                    "services": {DUMMY_SERVICE: {"notes": "..."}},
                       | 
                    |
| 883 | 
                        - },  | 
                    |
| 884 | 
                        -                {"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 885 | 
                        - id="service",  | 
                    |
| 886 | 
                        - ),  | 
                    |
| 887 | 
                        - pytest.param(  | 
                    |
| 888 | 
                        - ["--clear"],  | 
                    |
| 889 | 
                        -                {
                       | 
                    |
| 890 | 
                        -                    "global": {"phrase": "abc"},
                       | 
                    |
| 891 | 
                        -                    "services": {DUMMY_SERVICE: {"notes": "..."}},
                       | 
                    |
| 892 | 
                        - },  | 
                    |
| 893 | 
                        -                {"services": {}},
                       | 
                    |
| 894 | 
                        - id="all",  | 
                    |
| 895 | 
                        - ),  | 
                    |
| 896 | 
                        - ],  | 
                    |
| 897 | 
                        - )  | 
                    |
| 898 | 
                        - COLORFUL_COMMAND_INPUT = pytest.mark.parametrize(  | 
                    |
| 899 | 
                        - ["command_line", "input"],  | 
                    |
| 900 | 
                        - [  | 
                    |
| 901 | 
                        - (  | 
                    |
| 902 | 
                        - ["vault", "--import", "-"],  | 
                    |
| 903 | 
                        -                '{"services": {"": {"length": 20}}}',
                       | 
                    |
| 904 | 
                        - ),  | 
                    |
| 905 | 
                        - ],  | 
                    |
| 906 | 
                        - ids=["cmd"],  | 
                    |
| 907 | 
                        - )  | 
                    |
| 908 | 294 | 
                        CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES = pytest.mark.parametrize(  | 
                    
| 909 | 295 | 
                        ["command_line", "input", "err_text"],  | 
                    
| 910 | 296 | 
                        [  | 
                    
| ... | ... | 
                      @@ -993,182 +379,6 @@ class Parametrize(types.SimpleNamespace):  | 
                  
| 993 | 379 | 
                        ),  | 
                    
| 994 | 380 | 
                        ],  | 
                    
| 995 | 381 | 
                        )  | 
                    
| 996 | 
                        - COMPLETABLE_PATH_ARGUMENT = pytest.mark.parametrize(  | 
                    |
| 997 | 
                        - "command_prefix",  | 
                    |
| 998 | 
                        - [  | 
                    |
| 999 | 
                        - pytest.param(  | 
                    |
| 1000 | 
                        -                ("export", "vault"),
                       | 
                    |
| 1001 | 
                        - id="derivepassphrase-export-vault",  | 
                    |
| 1002 | 
                        - ),  | 
                    |
| 1003 | 
                        - pytest.param(  | 
                    |
| 1004 | 
                        -                ("vault", "--export"),
                       | 
                    |
| 1005 | 
                        - id="derivepassphrase-vault--export",  | 
                    |
| 1006 | 
                        - ),  | 
                    |
| 1007 | 
                        - pytest.param(  | 
                    |
| 1008 | 
                        -                ("vault", "--import"),
                       | 
                    |
| 1009 | 
                        - id="derivepassphrase-vault--import",  | 
                    |
| 1010 | 
                        - ),  | 
                    |
| 1011 | 
                        - ],  | 
                    |
| 1012 | 
                        - )  | 
                    |
| 1013 | 
                        - COMPLETABLE_OPTIONS = pytest.mark.parametrize(  | 
                    |
| 1014 | 
                        - ["command_prefix", "incomplete", "completions"],  | 
                    |
| 1015 | 
                        - [  | 
                    |
| 1016 | 
                        - pytest.param(  | 
                    |
| 1017 | 
                        - (),  | 
                    |
| 1018 | 
                        - "-",  | 
                    |
| 1019 | 
                        -                frozenset({
                       | 
                    |
| 1020 | 
                        - "--help",  | 
                    |
| 1021 | 
                        - "-h",  | 
                    |
| 1022 | 
                        - "--version",  | 
                    |
| 1023 | 
                        - "--debug",  | 
                    |
| 1024 | 
                        - "--verbose",  | 
                    |
| 1025 | 
                        - "-v",  | 
                    |
| 1026 | 
                        - "--quiet",  | 
                    |
| 1027 | 
                        - "-q",  | 
                    |
| 1028 | 
                        - }),  | 
                    |
| 1029 | 
                        - id="derivepassphrase",  | 
                    |
| 1030 | 
                        - ),  | 
                    |
| 1031 | 
                        - pytest.param(  | 
                    |
| 1032 | 
                        -                ("export",),
                       | 
                    |
| 1033 | 
                        - "-",  | 
                    |
| 1034 | 
                        -                frozenset({
                       | 
                    |
| 1035 | 
                        - "--help",  | 
                    |
| 1036 | 
                        - "-h",  | 
                    |
| 1037 | 
                        - "--version",  | 
                    |
| 1038 | 
                        - "--debug",  | 
                    |
| 1039 | 
                        - "--verbose",  | 
                    |
| 1040 | 
                        - "-v",  | 
                    |
| 1041 | 
                        - "--quiet",  | 
                    |
| 1042 | 
                        - "-q",  | 
                    |
| 1043 | 
                        - }),  | 
                    |
| 1044 | 
                        - id="derivepassphrase-export",  | 
                    |
| 1045 | 
                        - ),  | 
                    |
| 1046 | 
                        - pytest.param(  | 
                    |
| 1047 | 
                        -                ("export", "vault"),
                       | 
                    |
| 1048 | 
                        - "-",  | 
                    |
| 1049 | 
                        -                frozenset({
                       | 
                    |
| 1050 | 
                        - "--help",  | 
                    |
| 1051 | 
                        - "-h",  | 
                    |
| 1052 | 
                        - "--version",  | 
                    |
| 1053 | 
                        - "--debug",  | 
                    |
| 1054 | 
                        - "--verbose",  | 
                    |
| 1055 | 
                        - "-v",  | 
                    |
| 1056 | 
                        - "--quiet",  | 
                    |
| 1057 | 
                        - "-q",  | 
                    |
| 1058 | 
                        - "--format",  | 
                    |
| 1059 | 
                        - "-f",  | 
                    |
| 1060 | 
                        - "--key",  | 
                    |
| 1061 | 
                        - "-k",  | 
                    |
| 1062 | 
                        - }),  | 
                    |
| 1063 | 
                        - id="derivepassphrase-export-vault",  | 
                    |
| 1064 | 
                        - ),  | 
                    |
| 1065 | 
                        - pytest.param(  | 
                    |
| 1066 | 
                        -                ("vault",),
                       | 
                    |
| 1067 | 
                        - "-",  | 
                    |
| 1068 | 
                        -                frozenset({
                       | 
                    |
| 1069 | 
                        - "--help",  | 
                    |
| 1070 | 
                        - "-h",  | 
                    |
| 1071 | 
                        - "--version",  | 
                    |
| 1072 | 
                        - "--debug",  | 
                    |
| 1073 | 
                        - "--verbose",  | 
                    |
| 1074 | 
                        - "-v",  | 
                    |
| 1075 | 
                        - "--quiet",  | 
                    |
| 1076 | 
                        - "-q",  | 
                    |
| 1077 | 
                        - "--phrase",  | 
                    |
| 1078 | 
                        - "-p",  | 
                    |
| 1079 | 
                        - "--key",  | 
                    |
| 1080 | 
                        - "-k",  | 
                    |
| 1081 | 
                        - "--length",  | 
                    |
| 1082 | 
                        - "-l",  | 
                    |
| 1083 | 
                        - "--repeat",  | 
                    |
| 1084 | 
                        - "-r",  | 
                    |
| 1085 | 
                        - "--upper",  | 
                    |
| 1086 | 
                        - "--lower",  | 
                    |
| 1087 | 
                        - "--number",  | 
                    |
| 1088 | 
                        - "--space",  | 
                    |
| 1089 | 
                        - "--dash",  | 
                    |
| 1090 | 
                        - "--symbol",  | 
                    |
| 1091 | 
                        - "--config",  | 
                    |
| 1092 | 
                        - "-c",  | 
                    |
| 1093 | 
                        - "--notes",  | 
                    |
| 1094 | 
                        - "-n",  | 
                    |
| 1095 | 
                        - "--delete",  | 
                    |
| 1096 | 
                        - "-x",  | 
                    |
| 1097 | 
                        - "--delete-globals",  | 
                    |
| 1098 | 
                        - "--clear",  | 
                    |
| 1099 | 
                        - "-X",  | 
                    |
| 1100 | 
                        - "--export",  | 
                    |
| 1101 | 
                        - "-e",  | 
                    |
| 1102 | 
                        - "--import",  | 
                    |
| 1103 | 
                        - "-i",  | 
                    |
| 1104 | 
                        - "--overwrite-existing",  | 
                    |
| 1105 | 
                        - "--merge-existing",  | 
                    |
| 1106 | 
                        - "--unset",  | 
                    |
| 1107 | 
                        - "--export-as",  | 
                    |
| 1108 | 
                        - "--modern-editor-interface",  | 
                    |
| 1109 | 
                        - "--vault-legacy-editor-interface",  | 
                    |
| 1110 | 
                        - "--print-notes-before",  | 
                    |
| 1111 | 
                        - "--print-notes-after",  | 
                    |
| 1112 | 
                        - }),  | 
                    |
| 1113 | 
                        - id="derivepassphrase-vault",  | 
                    |
| 1114 | 
                        - ),  | 
                    |
| 1115 | 
                        - ],  | 
                    |
| 1116 | 
                        - )  | 
                    |
| 1117 | 
                        - COMPLETABLE_SUBCOMMANDS = pytest.mark.parametrize(  | 
                    |
| 1118 | 
                        - ["command_prefix", "incomplete", "completions"],  | 
                    |
| 1119 | 
                        - [  | 
                    |
| 1120 | 
                        - pytest.param(  | 
                    |
| 1121 | 
                        - (),  | 
                    |
| 1122 | 
                        - "",  | 
                    |
| 1123 | 
                        -                frozenset({"export", "vault"}),
                       | 
                    |
| 1124 | 
                        - id="derivepassphrase",  | 
                    |
| 1125 | 
                        - ),  | 
                    |
| 1126 | 
                        - pytest.param(  | 
                    |
| 1127 | 
                        -                ("export",),
                       | 
                    |
| 1128 | 
                        - "",  | 
                    |
| 1129 | 
                        -                frozenset({"vault"}),
                       | 
                    |
| 1130 | 
                        - id="derivepassphrase-export",  | 
                    |
| 1131 | 
                        - ),  | 
                    |
| 1132 | 
                        - ],  | 
                    |
| 1133 | 
                        - )  | 
                    |
| 1134 | 
                        - BAD_CONFIGS = pytest.mark.parametrize(  | 
                    |
| 1135 | 
                        - "config",  | 
                    |
| 1136 | 
                        - [  | 
                    |
| 1137 | 
                        -            {"global": "", "services": {}},
                       | 
                    |
| 1138 | 
                        -            {"global": 0, "services": {}},
                       | 
                    |
| 1139 | 
                        -            {
                       | 
                    |
| 1140 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 1141 | 
                        - "services": False,  | 
                    |
| 1142 | 
                        - },  | 
                    |
| 1143 | 
                        -            {
                       | 
                    |
| 1144 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 1145 | 
                        - "services": True,  | 
                    |
| 1146 | 
                        - },  | 
                    |
| 1147 | 
                        -            {
                       | 
                    |
| 1148 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 1149 | 
                        - "services": None,  | 
                    |
| 1150 | 
                        - },  | 
                    |
| 1151 | 
                        - ],  | 
                    |
| 1152 | 
                        - )  | 
                    |
| 1153 | 
                        - BASE_CONFIG_VARIATIONS = pytest.mark.parametrize(  | 
                    |
| 1154 | 
                        - "config",  | 
                    |
| 1155 | 
                        - [  | 
                    |
| 1156 | 
                        -            {"global": {"phrase": "my passphrase"}, "services": {}},
                       | 
                    |
| 1157 | 
                        -            {"global": {"key": DUMMY_KEY1_B64}, "services": {}},
                       | 
                    |
| 1158 | 
                        -            {
                       | 
                    |
| 1159 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 1160 | 
                        -                "services": {"sv": {"phrase": "my passphrase"}},
                       | 
                    |
| 1161 | 
                        - },  | 
                    |
| 1162 | 
                        -            {
                       | 
                    |
| 1163 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 1164 | 
                        -                "services": {"sv": {"key": DUMMY_KEY1_B64}},
                       | 
                    |
| 1165 | 
                        - },  | 
                    |
| 1166 | 
                        -            {
                       | 
                    |
| 1167 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 1168 | 
                        -                "services": {"sv": {"key": DUMMY_KEY1_B64, "length": 15}},
                       | 
                    |
| 1169 | 
                        - },  | 
                    |
| 1170 | 
                        - ],  | 
                    |
| 1171 | 
                        - )  | 
                    |
| 1172 | 382 | 
                        BASE_CONFIG_WITH_KEY_VARIATIONS = pytest.mark.parametrize(  | 
                    
| 1173 | 383 | 
                        "config",  | 
                    
| 1174 | 384 | 
                        [  | 
                    
| ... | ... | 
                      @@ -1253,404 +463,24 @@ class Parametrize(types.SimpleNamespace):  | 
                  
| 1253 | 463 | 
                        ),  | 
                    
| 1254 | 464 | 
                        ],  | 
                    
| 1255 | 465 | 
                        )  | 
                    
| 1256 | 
                        - COMPLETION_FUNCTION_INPUTS = pytest.mark.parametrize(  | 
                    |
| 1257 | 
                        - ["config", "comp_func", "args", "incomplete", "results"],  | 
                    |
| 1258 | 
                        - [  | 
                    |
| 1259 | 
                        - pytest.param(  | 
                    |
| 1260 | 
                        -                {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
                       | 
                    |
| 1261 | 
                        - cli_helpers.shell_complete_service,  | 
                    |
| 1262 | 
                        - ["vault"],  | 
                    |
| 1263 | 
                        - "",  | 
                    |
| 1264 | 
                        - [DUMMY_SERVICE],  | 
                    |
| 1265 | 
                        - id="base_config-service",  | 
                    |
| 1266 | 
                        - ),  | 
                    |
| 1267 | 
                        - pytest.param(  | 
                    |
| 1268 | 
                        -                {"services": {}},
                       | 
                    |
| 1269 | 
                        - cli_helpers.shell_complete_service,  | 
                    |
| 1270 | 
                        - ["vault"],  | 
                    |
| 1271 | 
                        - "",  | 
                    |
| 1272 | 
                        - [],  | 
                    |
| 1273 | 
                        - id="empty_config-service",  | 
                    |
| 1274 | 
                        - ),  | 
                    |
| 1275 | 
                        - pytest.param(  | 
                    |
| 1276 | 
                        -                {
                       | 
                    |
| 1277 | 
                        -                    "services": {
                       | 
                    |
| 1278 | 
                        - DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1279 | 
                        - "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1280 | 
                        - }  | 
                    |
| 1281 | 
                        - },  | 
                    |
| 1282 | 
                        - cli_helpers.shell_complete_service,  | 
                    |
| 1283 | 
                        - ["vault"],  | 
                    |
| 1284 | 
                        - "",  | 
                    |
| 1285 | 
                        - [DUMMY_SERVICE],  | 
                    |
| 1286 | 
                        - id="incompletable_newline_config-service",  | 
                    |
| 1287 | 
                        - ),  | 
                    |
| 1288 | 
                        - pytest.param(  | 
                    |
| 1289 | 
                        -                {
                       | 
                    |
| 1290 | 
                        -                    "services": {
                       | 
                    |
| 1291 | 
                        - DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1292 | 
                        - "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1293 | 
                        - }  | 
                    |
| 1294 | 
                        - },  | 
                    |
| 1295 | 
                        - cli_helpers.shell_complete_service,  | 
                    |
| 1296 | 
                        - ["vault"],  | 
                    |
| 1297 | 
                        - "",  | 
                    |
| 1298 | 
                        - [DUMMY_SERVICE],  | 
                    |
| 1299 | 
                        - id="incompletable_backspace_config-service",  | 
                    |
| 1300 | 
                        - ),  | 
                    |
| 1301 | 
                        - pytest.param(  | 
                    |
| 1302 | 
                        -                {
                       | 
                    |
| 1303 | 
                        -                    "services": {
                       | 
                    |
| 1304 | 
                        - DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1305 | 
                        - "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1306 | 
                        - }  | 
                    |
| 1307 | 
                        - },  | 
                    |
| 1308 | 
                        - cli_helpers.shell_complete_service,  | 
                    |
| 1309 | 
                        - ["vault"],  | 
                    |
| 1310 | 
                        - "",  | 
                    |
| 1311 | 
                        - sorted([DUMMY_SERVICE, "colon:in:name"]),  | 
                    |
| 1312 | 
                        - id="brittle_colon_config-service",  | 
                    |
| 1313 | 
                        - ),  | 
                    |
| 1314 | 
                        - pytest.param(  | 
                    |
| 1315 | 
                        -                {
                       | 
                    |
| 1316 | 
                        -                    "services": {
                       | 
                    |
| 1317 | 
                        - DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1318 | 
                        - "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1319 | 
                        - "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1320 | 
                        - "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1321 | 
                        - "nul\x00in\x00name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1322 | 
                        - "del\x7fin\x7fname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1323 | 
                        - }  | 
                    |
| 1324 | 
                        - },  | 
                    |
| 1325 | 
                        - cli_helpers.shell_complete_service,  | 
                    |
| 1326 | 
                        - ["vault"],  | 
                    |
| 1327 | 
                        - "",  | 
                    |
| 1328 | 
                        - sorted([DUMMY_SERVICE, "colon:in:name"]),  | 
                    |
| 1329 | 
                        - id="brittle_incompletable_multi_config-service",  | 
                    |
| 1330 | 
                        - ),  | 
                    |
| 1331 | 
                        - pytest.param(  | 
                    |
| 1332 | 
                        -                {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
                       | 
                    |
| 1333 | 
                        - cli_helpers.shell_complete_path,  | 
                    |
| 1334 | 
                        - ["vault", "--import"],  | 
                    |
| 1335 | 
                        - "",  | 
                    |
| 1336 | 
                        -                [click.shell_completion.CompletionItem("", type="file")],
                       | 
                    |
| 1337 | 
                        - id="base_config-path",  | 
                    |
| 1338 | 
                        - ),  | 
                    |
| 1339 | 
                        - pytest.param(  | 
                    |
| 1340 | 
                        -                {"services": {}},
                       | 
                    |
| 1341 | 
                        - cli_helpers.shell_complete_path,  | 
                    |
| 1342 | 
                        - ["vault", "--import"],  | 
                    |
| 1343 | 
                        - "",  | 
                    |
| 1344 | 
                        -                [click.shell_completion.CompletionItem("", type="file")],
                       | 
                    |
| 1345 | 
                        - id="empty_config-path",  | 
                    |
| 1346 | 
                        - ),  | 
                    |
| 1347 | 
                        - ],  | 
                    |
| 1348 | 
                        - )  | 
                    |
| 1349 | 
                        - COMPLETABLE_SERVICE_NAMES = pytest.mark.parametrize(  | 
                    |
| 1350 | 
                        - ["config", "incomplete", "completions"],  | 
                    |
| 1351 | 
                        - [  | 
                    |
| 1352 | 
                        - pytest.param(  | 
                    |
| 1353 | 
                        -                {"services": {}},
                       | 
                    |
| 1354 | 
                        - "",  | 
                    |
| 1355 | 
                        - frozenset(),  | 
                    |
| 1356 | 
                        - id="no_services",  | 
                    |
| 1357 | 
                        - ),  | 
                    |
| 1358 | 
                        - pytest.param(  | 
                    |
| 1359 | 
                        -                {"services": {}},
                       | 
                    |
| 1360 | 
                        - "partial",  | 
                    |
| 1361 | 
                        - frozenset(),  | 
                    |
| 1362 | 
                        - id="no_services_partial",  | 
                    |
| 1363 | 
                        - ),  | 
                    |
| 1364 | 
                        - pytest.param(  | 
                    |
| 1365 | 
                        -                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 1366 | 
                        - "",  | 
                    |
| 1367 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1368 | 
                        - id="one_service",  | 
                    |
| 1369 | 
                        - ),  | 
                    |
| 1370 | 
                        - pytest.param(  | 
                    |
| 1371 | 
                        -                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 1372 | 
                        - DUMMY_SERVICE[:4],  | 
                    |
| 1373 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1374 | 
                        - id="one_service_partial",  | 
                    |
| 1375 | 
                        - ),  | 
                    |
| 1376 | 
                        - pytest.param(  | 
                    |
| 1377 | 
                        -                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 1378 | 
                        - DUMMY_SERVICE[-4:],  | 
                    |
| 1379 | 
                        - frozenset(),  | 
                    |
| 1380 | 
                        - id="one_service_partial_miss",  | 
                    |
| 1381 | 
                        - ),  | 
                    |
| 1382 | 
                        - ],  | 
                    |
| 1383 | 
                        - )  | 
                    |
| 1384 | 
                        - SERVICE_NAME_COMPLETION_INPUTS = pytest.mark.parametrize(  | 
                    |
| 1385 | 
                        - ["config", "key", "incomplete", "completions"],  | 
                    |
| 1386 | 
                        - [  | 
                    |
| 1387 | 
                        - pytest.param(  | 
                    |
| 1388 | 
                        -                {
                       | 
                    |
| 1389 | 
                        -                    "services": {
                       | 
                    |
| 1390 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1391 | 
                        -                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 1392 | 
                        - },  | 
                    |
| 1393 | 
                        - },  | 
                    |
| 1394 | 
                        - "newline\nin\nname",  | 
                    |
| 1395 | 
                        - "",  | 
                    |
| 1396 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1397 | 
                        - id="newline",  | 
                    |
| 1398 | 
                        - ),  | 
                    |
| 1399 | 
                        - pytest.param(  | 
                    |
| 1400 | 
                        -                {
                       | 
                    |
| 1401 | 
                        -                    "services": {
                       | 
                    |
| 1402 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1403 | 
                        -                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 1404 | 
                        - },  | 
                    |
| 1405 | 
                        - },  | 
                    |
| 1406 | 
                        - "newline\nin\nname",  | 
                    |
| 1407 | 
                        - "serv",  | 
                    |
| 1408 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1409 | 
                        - id="newline_partial_other",  | 
                    |
| 1410 | 
                        - ),  | 
                    |
| 1411 | 
                        - pytest.param(  | 
                    |
| 1412 | 
                        -                {
                       | 
                    |
| 1413 | 
                        -                    "services": {
                       | 
                    |
| 1414 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1415 | 
                        -                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 1416 | 
                        - },  | 
                    |
| 1417 | 
                        - },  | 
                    |
| 1418 | 
                        - "newline\nin\nname",  | 
                    |
| 1419 | 
                        - "newline",  | 
                    |
| 1420 | 
                        -                frozenset({}),
                       | 
                    |
| 1421 | 
                        - id="newline_partial_specific",  | 
                    |
| 1422 | 
                        - ),  | 
                    |
| 1423 | 
                        - pytest.param(  | 
                    |
| 1424 | 
                        -                {
                       | 
                    |
| 1425 | 
                        -                    "services": {
                       | 
                    |
| 1426 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1427 | 
                        -                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 1428 | 
                        - },  | 
                    |
| 1429 | 
                        - },  | 
                    |
| 1430 | 
                        - "nul\x00in\x00name",  | 
                    |
| 1431 | 
                        - "",  | 
                    |
| 1432 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1433 | 
                        - id="nul",  | 
                    |
| 1434 | 
                        - ),  | 
                    |
| 1435 | 
                        - pytest.param(  | 
                    |
| 1436 | 
                        -                {
                       | 
                    |
| 1437 | 
                        -                    "services": {
                       | 
                    |
| 1438 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1439 | 
                        -                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 1440 | 
                        - },  | 
                    |
| 1441 | 
                        - },  | 
                    |
| 1442 | 
                        - "nul\x00in\x00name",  | 
                    |
| 1443 | 
                        - "serv",  | 
                    |
| 1444 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1445 | 
                        - id="nul_partial_other",  | 
                    |
| 1446 | 
                        - ),  | 
                    |
| 1447 | 
                        - pytest.param(  | 
                    |
| 1448 | 
                        -                {
                       | 
                    |
| 1449 | 
                        -                    "services": {
                       | 
                    |
| 1450 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1451 | 
                        -                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 1452 | 
                        - },  | 
                    |
| 1453 | 
                        - },  | 
                    |
| 1454 | 
                        - "nul\x00in\x00name",  | 
                    |
| 1455 | 
                        - "nul",  | 
                    |
| 1456 | 
                        -                frozenset({}),
                       | 
                    |
| 1457 | 
                        - id="nul_partial_specific",  | 
                    |
| 1458 | 
                        - ),  | 
                    |
| 1459 | 
                        - pytest.param(  | 
                    |
| 1460 | 
                        -                {
                       | 
                    |
| 1461 | 
                        -                    "services": {
                       | 
                    |
| 1462 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1463 | 
                        -                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 1464 | 
                        - },  | 
                    |
| 1465 | 
                        - },  | 
                    |
| 1466 | 
                        - "backspace\bin\bname",  | 
                    |
| 1467 | 
                        - "",  | 
                    |
| 1468 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1469 | 
                        - id="backspace",  | 
                    |
| 1470 | 
                        - ),  | 
                    |
| 1471 | 
                        - pytest.param(  | 
                    |
| 1472 | 
                        -                {
                       | 
                    |
| 1473 | 
                        -                    "services": {
                       | 
                    |
| 1474 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1475 | 
                        -                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 1476 | 
                        - },  | 
                    |
| 1477 | 
                        - },  | 
                    |
| 1478 | 
                        - "backspace\bin\bname",  | 
                    |
| 1479 | 
                        - "serv",  | 
                    |
| 1480 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1481 | 
                        - id="backspace_partial_other",  | 
                    |
| 1482 | 
                        - ),  | 
                    |
| 1483 | 
                        - pytest.param(  | 
                    |
| 1484 | 
                        -                {
                       | 
                    |
| 1485 | 
                        -                    "services": {
                       | 
                    |
| 1486 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1487 | 
                        -                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 1488 | 
                        - },  | 
                    |
| 1489 | 
                        - },  | 
                    |
| 1490 | 
                        - "backspace\bin\bname",  | 
                    |
| 1491 | 
                        - "back",  | 
                    |
| 1492 | 
                        -                frozenset({}),
                       | 
                    |
| 1493 | 
                        - id="backspace_partial_specific",  | 
                    |
| 1494 | 
                        - ),  | 
                    |
| 1495 | 
                        - pytest.param(  | 
                    |
| 1496 | 
                        -                {
                       | 
                    |
| 1497 | 
                        -                    "services": {
                       | 
                    |
| 1498 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1499 | 
                        -                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 1500 | 
                        - },  | 
                    |
| 1501 | 
                        - },  | 
                    |
| 1502 | 
                        - "del\x7fin\x7fname",  | 
                    |
| 1503 | 
                        - "",  | 
                    |
| 1504 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1505 | 
                        - id="del",  | 
                    |
| 1506 | 
                        - ),  | 
                    |
| 1507 | 
                        - pytest.param(  | 
                    |
| 1508 | 
                        -                {
                       | 
                    |
| 1509 | 
                        -                    "services": {
                       | 
                    |
| 1510 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1511 | 
                        -                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 1512 | 
                        - },  | 
                    |
| 1513 | 
                        - },  | 
                    |
| 1514 | 
                        - "del\x7fin\x7fname",  | 
                    |
| 1515 | 
                        - "serv",  | 
                    |
| 1516 | 
                        -                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 1517 | 
                        - id="del_partial_other",  | 
                    |
| 1518 | 
                        - ),  | 
                    |
| 1519 | 
                        - pytest.param(  | 
                    |
| 1520 | 
                        -                {
                       | 
                    |
| 1521 | 
                        -                    "services": {
                       | 
                    |
| 1522 | 
                        -                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 1523 | 
                        -                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 1524 | 
                        - },  | 
                    |
| 1525 | 
                        - },  | 
                    |
| 1526 | 
                        - "del\x7fin\x7fname",  | 
                    |
| 1527 | 
                        - "del",  | 
                    |
| 1528 | 
                        -                frozenset({}),
                       | 
                    |
| 1529 | 
                        - id="del_partial_specific",  | 
                    |
| 1530 | 
                        - ),  | 
                    |
| 1531 | 
                        - ],  | 
                    |
| 1532 | 
                        - )  | 
                    |
| 1533 | 
                        - CONNECTION_HINTS = pytest.mark.parametrize(  | 
                    |
| 1534 | 
                        - "conn_hint", ["none", "socket", "client"]  | 
                    |
| 1535 | 
                        - )  | 
                    |
| 1536 | 
                        - NOOP_EDIT_FUNCS = pytest.mark.parametrize(  | 
                    |
| 1537 | 
                        - ["edit_func_name", "modern_editor_interface"],  | 
                    |
| 1538 | 
                        - [  | 
                    |
| 1539 | 
                        -            pytest.param("empty", True, id="empty"),
                       | 
                    |
| 1540 | 
                        -            pytest.param("space", False, id="space-legacy"),
                       | 
                    |
| 1541 | 
                        -            pytest.param("space", True, id="space-modern"),
                       | 
                    |
| 1542 | 
                        - ],  | 
                    |
| 1543 | 
                        - )  | 
                    |
| 1544 | 
                        - SERVICE_NAME_EXCEPTIONS = pytest.mark.parametrize(  | 
                    |
| 1545 | 
                        - "exc_type", [RuntimeError, KeyError, ValueError]  | 
                    |
| 1546 | 
                        - )  | 
                    |
| 1547 | 
                        - EXPORT_FORMAT_OPTIONS = pytest.mark.parametrize(  | 
                    |
| 1548 | 
                        - "export_options",  | 
                    |
| 466 | 
                        + NOOP_EDIT_FUNCS = pytest.mark.parametrize(  | 
                    |
| 467 | 
                        + ["edit_func_name", "modern_editor_interface"],  | 
                    |
| 468 | 
                        + [  | 
                    |
| 469 | 
                        +            pytest.param("empty", True, id="empty"),
                       | 
                    |
| 470 | 
                        +            pytest.param("space", False, id="space-legacy"),
                       | 
                    |
| 471 | 
                        +            pytest.param("space", True, id="space-modern"),
                       | 
                    |
| 472 | 
                        + ],  | 
                    |
| 473 | 
                        + )  | 
                    |
| 474 | 
                        + EXPORT_FORMAT_OPTIONS = pytest.mark.parametrize(  | 
                    |
| 475 | 
                        + "export_options",  | 
                    |
| 1549 | 476 | 
                        [  | 
                    
| 1550 | 477 | 
                        [],  | 
                    
| 1551 | 478 | 
                        ["--export-as=sh"],  | 
                    
| 1552 | 479 | 
                        ],  | 
                    
| 1553 | 480 | 
                        )  | 
                    
| 1554 | 
                        -    INCOMPLETE = pytest.mark.parametrize("incomplete", ["", "partial"])
                       | 
                    |
| 1555 | 
                        - ISATTY = pytest.mark.parametrize(  | 
                    |
| 1556 | 
                        - "isatty",  | 
                    |
| 1557 | 
                        - [False, True],  | 
                    |
| 1558 | 
                        - ids=["notty", "tty"],  | 
                    |
| 1559 | 
                        - )  | 
                    |
| 1560 | 481 | 
                        KEY_INDEX = pytest.mark.parametrize(  | 
                    
| 1561 | 482 | 
                                 "key_index", [1, 2, 3], ids=lambda i: f"index{i}"
                       | 
                    
| 1562 | 483 | 
                        )  | 
                    
| 1563 | 
                        - KEY_TO_PHRASE_SETTINGS = pytest.mark.parametrize(  | 
                    |
| 1564 | 
                        - [  | 
                    |
| 1565 | 
                        - "list_keys_action",  | 
                    |
| 1566 | 
                        - "address_action",  | 
                    |
| 1567 | 
                        - "system_support_action",  | 
                    |
| 1568 | 
                        - "sign_action",  | 
                    |
| 1569 | 
                        - "pattern",  | 
                    |
| 1570 | 
                        - ],  | 
                    |
| 1571 | 
                        - [  | 
                    |
| 1572 | 
                        - pytest.param(  | 
                    |
| 1573 | 
                        - ListKeysAction.EMPTY,  | 
                    |
| 1574 | 
                        - None,  | 
                    |
| 1575 | 
                        - None,  | 
                    |
| 1576 | 
                        - SignAction.FAIL,  | 
                    |
| 1577 | 
                        - "not loaded into the agent",  | 
                    |
| 1578 | 
                        - id="key-not-loaded",  | 
                    |
| 1579 | 
                        - ),  | 
                    |
| 1580 | 
                        - pytest.param(  | 
                    |
| 1581 | 
                        - ListKeysAction.FAIL,  | 
                    |
| 1582 | 
                        - None,  | 
                    |
| 1583 | 
                        - None,  | 
                    |
| 1584 | 
                        - SignAction.FAIL,  | 
                    |
| 1585 | 
                        - "SSH agent failed to or refused to",  | 
                    |
| 1586 | 
                        - id="list-keys-refused",  | 
                    |
| 1587 | 
                        - ),  | 
                    |
| 1588 | 
                        - pytest.param(  | 
                    |
| 1589 | 
                        - ListKeysAction.FAIL_RUNTIME,  | 
                    |
| 1590 | 
                        - None,  | 
                    |
| 1591 | 
                        - None,  | 
                    |
| 1592 | 
                        - SignAction.FAIL,  | 
                    |
| 1593 | 
                        - "SSH agent failed to or refused to",  | 
                    |
| 1594 | 
                        - id="list-keys-protocol-error",  | 
                    |
| 1595 | 
                        - ),  | 
                    |
| 1596 | 
                        - pytest.param(  | 
                    |
| 1597 | 
                        - None,  | 
                    |
| 1598 | 
                        - SocketAddressAction.UNSET_SSH_AUTH_SOCK,  | 
                    |
| 1599 | 
                        - None,  | 
                    |
| 1600 | 
                        - SignAction.FAIL,  | 
                    |
| 1601 | 
                        - "Cannot find any running SSH agent",  | 
                    |
| 1602 | 
                        - id="agent-address-missing",  | 
                    |
| 1603 | 
                        - ),  | 
                    |
| 1604 | 
                        - pytest.param(  | 
                    |
| 1605 | 
                        - None,  | 
                    |
| 1606 | 
                        - SocketAddressAction.MANGLE_SSH_AUTH_SOCK,  | 
                    |
| 1607 | 
                        - None,  | 
                    |
| 1608 | 
                        - SignAction.FAIL,  | 
                    |
| 1609 | 
                        - "Cannot connect to the SSH agent",  | 
                    |
| 1610 | 
                        - id="agent-address-mangled",  | 
                    |
| 1611 | 
                        - ),  | 
                    |
| 1612 | 
                        - pytest.param(  | 
                    |
| 1613 | 
                        - None,  | 
                    |
| 1614 | 
                        - None,  | 
                    |
| 1615 | 
                        - SystemSupportAction.UNSET_NATIVE,  | 
                    |
| 1616 | 
                        - SignAction.FAIL,  | 
                    |
| 1617 | 
                        - "does not support communicating with it",  | 
                    |
| 1618 | 
                        - id="no-agent-support",  | 
                    |
| 1619 | 
                        - ),  | 
                    |
| 1620 | 
                        - pytest.param(  | 
                    |
| 1621 | 
                        - None,  | 
                    |
| 1622 | 
                        - None,  | 
                    |
| 1623 | 
                        - SystemSupportAction.UNSET_PROVIDER_LIST,  | 
                    |
| 1624 | 
                        - SignAction.FAIL,  | 
                    |
| 1625 | 
                        - "does not support communicating with it",  | 
                    |
| 1626 | 
                        - id="no-agent-support",  | 
                    |
| 1627 | 
                        - ),  | 
                    |
| 1628 | 
                        - pytest.param(  | 
                    |
| 1629 | 
                        - None,  | 
                    |
| 1630 | 
                        - None,  | 
                    |
| 1631 | 
                        - SystemSupportAction.UNSET_AF_UNIX_AND_ENSURE_USE,  | 
                    |
| 1632 | 
                        - SignAction.FAIL,  | 
                    |
| 1633 | 
                        - "does not support communicating with it",  | 
                    |
| 1634 | 
                        - id="no-agent-support",  | 
                    |
| 1635 | 
                        - ),  | 
                    |
| 1636 | 
                        - pytest.param(  | 
                    |
| 1637 | 
                        - None,  | 
                    |
| 1638 | 
                        - None,  | 
                    |
| 1639 | 
                        - SystemSupportAction.UNSET_WINDLL_AND_ENSURE_USE,  | 
                    |
| 1640 | 
                        - SignAction.FAIL,  | 
                    |
| 1641 | 
                        - "does not support communicating with it",  | 
                    |
| 1642 | 
                        - id="no-agent-support",  | 
                    |
| 1643 | 
                        - ),  | 
                    |
| 1644 | 
                        - pytest.param(  | 
                    |
| 1645 | 
                        - None,  | 
                    |
| 1646 | 
                        - None,  | 
                    |
| 1647 | 
                        - None,  | 
                    |
| 1648 | 
                        - SignAction.FAIL_RUNTIME,  | 
                    |
| 1649 | 
                        - "violates the communication protocol",  | 
                    |
| 1650 | 
                        - id="sign-violates-protocol",  | 
                    |
| 1651 | 
                        - ),  | 
                    |
| 1652 | 
                        - ],  | 
                    |
| 1653 | 
                        - )  | 
                    |
| 1654 | 484 | 
                        UNICODE_NORMALIZATION_ERROR_INPUTS = pytest.mark.parametrize(  | 
                    
| 1655 | 485 | 
                        ["main_config", "command_line", "input", "error_message"],  | 
                    
| 1656 | 486 | 
                        [  | 
                    
| ... | ... | 
                      @@ -1791,9 +621,6 @@ class Parametrize(types.SimpleNamespace):  | 
                  
| 1791 | 621 | 
                        ),  | 
                    
| 1792 | 622 | 
                        ],  | 
                    
| 1793 | 623 | 
                        )  | 
                    
| 1794 | 
                        -    MASK_PROG_NAME = pytest.mark.parametrize("mask_prog_name", [False, True])
                       | 
                    |
| 1795 | 
                        -    MASK_VERSION = pytest.mark.parametrize("mask_version", [False, True])
                       | 
                    |
| 1796 | 
                        -    CONFIG_SETTING_MODE = pytest.mark.parametrize("mode", ["config", "import"])
                       | 
                    |
| 1797 | 624 | 
                        MODERN_EDITOR_INTERFACE = pytest.mark.parametrize(  | 
                    
| 1798 | 625 | 
                        "modern_editor_interface", [False, True], ids=["legacy", "modern"]  | 
                    
| 1799 | 626 | 
                        )  | 
                    
| ... | ... | 
                      @@ -1839,181 +666,18 @@ class Parametrize(types.SimpleNamespace):  | 
                  
| 1839 | 666 | 
                        if not o.incompatible  | 
                    
| 1840 | 667 | 
                        ],  | 
                    
| 1841 | 668 | 
                        )  | 
                    
| 1842 | 
                        - COMPLETABLE_ITEMS = pytest.mark.parametrize(  | 
                    |
| 1843 | 
                        - ["partial", "is_completable"],  | 
                    |
| 1844 | 
                        - [  | 
                    |
| 1845 | 
                        -            ("", True),
                       | 
                    |
| 1846 | 
                        - (DUMMY_SERVICE, True),  | 
                    |
| 1847 | 
                        -            ("a\bn", False),
                       | 
                    |
| 1848 | 
                        -            ("\b", False),
                       | 
                    |
| 1849 | 
                        -            ("\x00", False),
                       | 
                    |
| 1850 | 
                        -            ("\x20", True),
                       | 
                    |
| 1851 | 
                        -            ("\x7f", False),
                       | 
                    |
| 1852 | 
                        -            ("service with spaces", True),
                       | 
                    |
| 1853 | 
                        -            ("service\nwith\nnewlines", False),
                       | 
                    |
| 1854 | 
                        - ],  | 
                    |
| 1855 | 
                        - )  | 
                    |
| 1856 | 
                        - SHELL_FORMATTER = pytest.mark.parametrize(  | 
                    |
| 1857 | 
                        - ["shell", "format_func"],  | 
                    |
| 1858 | 
                        - [  | 
                    |
| 1859 | 
                        -            pytest.param("bash", bash_format, id="bash"),
                       | 
                    |
| 1860 | 
                        -            pytest.param("fish", fish_format, id="fish"),
                       | 
                    |
| 1861 | 
                        -            pytest.param("zsh", zsh_format, id="zsh"),
                       | 
                    |
| 1862 | 
                        - ],  | 
                    |
| 1863 | 
                        - )  | 
                    |
| 1864 | 669 | 
                        TRY_RACE_FREE_IMPLEMENTATION = pytest.mark.parametrize(  | 
                    
| 1865 | 670 | 
                        "try_race_free_implementation", [True, False]  | 
                    
| 1866 | 671 | 
                        )  | 
                    
| 1867 | 
                        - VERSION_OUTPUT_DATA = pytest.mark.parametrize(  | 
                    |
| 1868 | 
                        - ["version_output", "prog_name", "version", "expected_parse"],  | 
                    |
| 1869 | 
                        - [  | 
                    |
| 1870 | 
                        - pytest.param(  | 
                    |
| 1871 | 
                        - """\  | 
                    |
| 1872 | 
                        -derivepassphrase 0.4.0  | 
                    |
| 1873 | 
                        -Using cryptography 44.0.0  | 
                    |
| 1874 | 
                        -  | 
                    |
| 1875 | 
                        -Supported foreign configuration formats: vault storeroom, vault v0.2,  | 
                    |
| 1876 | 
                        - vault v0.3.  | 
                    |
| 1877 | 
                        -PEP 508 extras: export.  | 
                    |
| 1878 | 
                        -""",  | 
                    |
| 1879 | 
                        - "derivepassphrase",  | 
                    |
| 1880 | 
                        - "0.4.0",  | 
                    |
| 1881 | 
                        - VersionOutputData(  | 
                    |
| 1882 | 
                        -                    derivation_schemes={},
                       | 
                    |
| 1883 | 
                        -                    foreign_configuration_formats={
                       | 
                    |
| 1884 | 
                        - "vault storeroom": True,  | 
                    |
| 1885 | 
                        - "vault v0.2": True,  | 
                    |
| 1886 | 
                        - "vault v0.3": True,  | 
                    |
| 1887 | 
                        - },  | 
                    |
| 1888 | 
                        - subcommands=frozenset(),  | 
                    |
| 1889 | 
                        -                    features={},
                       | 
                    |
| 1890 | 
                        -                    extras=frozenset({"export"}),
                       | 
                    |
| 1891 | 
                        - ),  | 
                    |
| 1892 | 
                        - id="derivepassphrase-0.4.0-export",  | 
                    |
| 1893 | 
                        - ),  | 
                    |
| 1894 | 
                        - pytest.param(  | 
                    |
| 1895 | 
                        - """\  | 
                    |
| 1896 | 
                        -derivepassphrase 0.5  | 
                    |
| 1897 | 
                        -  | 
                    |
| 1898 | 
                        -Supported derivation schemes: vault.  | 
                    |
| 1899 | 
                        -Known foreign configuration formats: vault storeroom, vault v0.2, vault v0.3.  | 
                    |
| 1900 | 
                        -Supported subcommands: export, vault.  | 
                    |
| 1901 | 
                        -No PEP 508 extras are active.  | 
                    |
| 1902 | 
                        -""",  | 
                    |
| 1903 | 
                        - "derivepassphrase",  | 
                    |
| 1904 | 
                        - "0.5",  | 
                    |
| 1905 | 
                        - VersionOutputData(  | 
                    |
| 1906 | 
                        -                    derivation_schemes={"vault": True},
                       | 
                    |
| 1907 | 
                        -                    foreign_configuration_formats={
                       | 
                    |
| 1908 | 
                        - "vault storeroom": False,  | 
                    |
| 1909 | 
                        - "vault v0.2": False,  | 
                    |
| 1910 | 
                        - "vault v0.3": False,  | 
                    |
| 1911 | 
                        - },  | 
                    |
| 1912 | 
                        -                    subcommands=frozenset({"export", "vault"}),
                       | 
                    |
| 1913 | 
                        -                    features={},
                       | 
                    |
| 1914 | 
                        -                    extras=frozenset({}),
                       | 
                    |
| 1915 | 
                        - ),  | 
                    |
| 1916 | 
                        - id="derivepassphrase-0.5-plain",  | 
                    |
| 1917 | 
                        - ),  | 
                    |
| 1918 | 
                        - pytest.param(  | 
                    |
| 1919 | 
                        - """\  | 
                    |
| 1920 | 
                        -  | 
                    |
| 1921 | 
                        -  | 
                    |
| 1922 | 
                        -  | 
                    |
| 1923 | 
                        -inventpassphrase -1.3  | 
                    |
| 1924 | 
                        -Using not-a-library 7.12  | 
                    |
| 1925 | 
                        -Copyright 2025 Nobody. All rights reserved.  | 
                    |
| 1926 | 
                        -  | 
                    |
| 1927 | 
                        -Supported derivation schemes: nonsense.  | 
                    |
| 1928 | 
                        -Known derivation schemes: divination, /dev/random,  | 
                    |
| 1929 | 
                        - geiger counter,  | 
                    |
| 1930 | 
                        - crossword solver.  | 
                    |
| 1931 | 
                        -Supported foreign configuration formats: derivepassphrase, nonsense.  | 
                    |
| 1932 | 
                        -Known foreign configuration formats: divination v3.141592,  | 
                    |
| 1933 | 
                        - /dev/random.  | 
                    |
| 1934 | 
                        -Supported subcommands: delete-all-files, dump-core.  | 
                    |
| 1935 | 
                        -Supported features: delete-while-open.  | 
                    |
| 1936 | 
                        -Known features: backups-are-nice-to-have.  | 
                    |
| 1937 | 
                        -PEP 508 extras: annoying-popups, delete-all-files,  | 
                    |
| 1938 | 
                        - dump-core-depending-on-the-phase-of-the-moon.  | 
                    |
| 1939 | 
                        -  | 
                    |
| 1940 | 
                        -  | 
                    |
| 1941 | 
                        -  | 
                    |
| 1942 | 
                        -""",  | 
                    |
| 1943 | 
                        - "inventpassphrase",  | 
                    |
| 1944 | 
                        - "-1.3",  | 
                    |
| 1945 | 
                        - VersionOutputData(  | 
                    |
| 1946 | 
                        -                    derivation_schemes={
                       | 
                    |
| 1947 | 
                        - "nonsense": True,  | 
                    |
| 1948 | 
                        - "divination": False,  | 
                    |
| 1949 | 
                        - "/dev/random": False,  | 
                    |
| 1950 | 
                        - "geiger counter": False,  | 
                    |
| 1951 | 
                        - "crossword solver": False,  | 
                    |
| 1952 | 
                        - },  | 
                    |
| 1953 | 
                        -                    foreign_configuration_formats={
                       | 
                    |
| 1954 | 
                        - "derivepassphrase": True,  | 
                    |
| 1955 | 
                        - "nonsense": True,  | 
                    |
| 1956 | 
                        - "divination v3.141592": False,  | 
                    |
| 1957 | 
                        - "/dev/random": False,  | 
                    |
| 1958 | 
                        - },  | 
                    |
| 1959 | 
                        -                    subcommands=frozenset({"delete-all-files", "dump-core"}),
                       | 
                    |
| 1960 | 
                        -                    features={
                       | 
                    |
| 1961 | 
                        - "delete-while-open": True,  | 
                    |
| 1962 | 
                        - "backups-are-nice-to-have": False,  | 
                    |
| 1963 | 
                        - },  | 
                    |
| 1964 | 
                        -                    extras=frozenset({
                       | 
                    |
| 1965 | 
                        - "annoying-popups",  | 
                    |
| 1966 | 
                        - "delete-all-files",  | 
                    |
| 1967 | 
                        - "dump-core-depending-on-the-phase-of-the-moon",  | 
                    |
| 1968 | 
                        - }),  | 
                    |
| 1969 | 
                        - ),  | 
                    |
| 1970 | 
                        - id="inventpassphrase",  | 
                    |
| 1971 | 
                        - ),  | 
                    |
| 1972 | 
                        - ],  | 
                    |
| 1973 | 
                        - )  | 
                    |
| 1974 | 
                        - """Sample data for [`parse_version_output`][]."""  | 
                    |
| 1975 | 
                        - VALIDATION_FUNCTION_INPUT = pytest.mark.parametrize(  | 
                    |
| 1976 | 
                        - ["vfunc", "input"],  | 
                    |
| 1977 | 
                        - [  | 
                    |
| 1978 | 
                        - (cli_machinery.validate_occurrence_constraint, 20),  | 
                    |
| 1979 | 
                        - (cli_machinery.validate_length, 20),  | 
                    |
| 1980 | 
                        - ],  | 
                    |
| 1981 | 
                        - )  | 
                    |
| 1982 | 672 | 
                         | 
                    
| 1983 | 673 | 
                         | 
                    
| 1984 | 
                        -class TestAllCLI:  | 
                    |
| 1985 | 
                        - """Tests uniformly for all command-line interfaces."""  | 
                    |
| 674 | 
                        +class TestCLI:  | 
                    |
| 675 | 
                        + """Tests for the `derivepassphrase vault` command-line interface."""  | 
                    |
| 1986 | 676 | 
                         | 
                    
| 1987 | 
                        - @Parametrize.MASK_PROG_NAME  | 
                    |
| 1988 | 
                        - @Parametrize.MASK_VERSION  | 
                    |
| 1989 | 
                        - @Parametrize.VERSION_OUTPUT_DATA  | 
                    |
| 1990 | 
                        - def test_001_parse_version_output(  | 
                    |
| 677 | 
                        + def test_200_help_output(  | 
                    |
| 1991 | 678 | 
                        self,  | 
                    
| 1992 | 
                        - version_output: str,  | 
                    |
| 1993 | 
                        - prog_name: str | None,  | 
                    |
| 1994 | 
                        - version: str | None,  | 
                    |
| 1995 | 
                        - mask_prog_name: bool,  | 
                    |
| 1996 | 
                        - mask_version: bool,  | 
                    |
| 1997 | 
                        - expected_parse: VersionOutputData,  | 
                    |
| 1998 | 679 | 
                        ) -> None:  | 
                    
| 1999 | 
                        - """The parsing machinery for expected version output data works."""  | 
                    |
| 2000 | 
                        - prog_name = None if mask_prog_name else prog_name  | 
                    |
| 2001 | 
                        - version = None if mask_version else version  | 
                    |
| 2002 | 
                        - assert (  | 
                    |
| 2003 | 
                        - parse_version_output(  | 
                    |
| 2004 | 
                        - version_output, prog_name=prog_name, version=version  | 
                    |
| 2005 | 
                        - )  | 
                    |
| 2006 | 
                        - == expected_parse  | 
                    |
| 2007 | 
                        - )  | 
                    |
| 2008 | 
                        -  | 
                    |
| 2009 | 
                        - # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 2010 | 
                        - # check for?  | 
                    |
| 2011 | 
                        - def test_100_help_output(self) -> None:  | 
                    |
| 2012 | 
                        - """The top-level help text mentions subcommands.  | 
                    |
| 2013 | 
                        -  | 
                    |
| 2014 | 
                        - TODO: Do we actually need this? What should we check for?  | 
                    |
| 2015 | 
                        -  | 
                    |
| 2016 | 
                        - """  | 
                    |
| 680 | 
                        + """The `--help` option emits help text."""  | 
                    |
| 2017 | 681 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2018 | 682 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2019 | 683 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2027,22 +691,23 @@ class TestAllCLI:  | 
                  
| 2027 | 691 | 
                        )  | 
                    
| 2028 | 692 | 
                        )  | 
                    
| 2029 | 693 | 
                        result = runner.invoke(  | 
                    
| 2030 | 
                        - cli.derivepassphrase, ["--help"], catch_exceptions=False  | 
                    |
| 694 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 695 | 
                        + ["--help"],  | 
                    |
| 696 | 
                        + catch_exceptions=False,  | 
                    |
| 2031 | 697 | 
                        )  | 
                    
| 2032 | 698 | 
                        assert result.clean_exit(  | 
                    
| 2033 | 
                        - empty_stderr=True, output="currently implemented subcommands"  | 
                    |
| 2034 | 
                        - ), "expected clean exit, and known help text"  | 
                    |
| 699 | 
                        + empty_stderr=True, output="Passphrase generation:\n"  | 
                    |
| 700 | 
                        + ), "expected clean exit, and option groups in help text"  | 
                    |
| 701 | 
                        + assert result.clean_exit(  | 
                    |
| 702 | 
                        + empty_stderr=True, output="Use $VISUAL or $EDITOR to configure"  | 
                    |
| 703 | 
                        + ), "expected clean exit, and option group epilog in help text"  | 
                    |
| 2035 | 704 | 
                         | 
                    
| 2036 | 
                        - # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 2037 | 
                        - # check for?  | 
                    |
| 2038 | 
                        - def test_101_help_output_export(  | 
                    |
| 705 | 
                        + # TODO(the-13th-letter): Remove this test once  | 
                    |
| 706 | 
                        + # TestAllCLI.test_202_version_option_output no longer xfails.  | 
                    |
| 707 | 
                        + def test_200a_version_output(  | 
                    |
| 2039 | 708 | 
                        self,  | 
                    
| 2040 | 709 | 
                        ) -> None:  | 
                    
| 2041 | 
                        - """The "export" subcommand help text mentions subcommands.  | 
                    |
| 2042 | 
                        -  | 
                    |
| 2043 | 
                        - TODO: Do we actually need this? What should we check for?  | 
                    |
| 2044 | 
                        -  | 
                    |
| 2045 | 
                        - """  | 
                    |
| 710 | 
                        + """The `--version` option emits version information."""  | 
                    |
| 2046 | 711 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2047 | 712 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2048 | 713 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2056,24 +721,25 @@ class TestAllCLI:  | 
                  
| 2056 | 721 | 
                        )  | 
                    
| 2057 | 722 | 
                        )  | 
                    
| 2058 | 723 | 
                        result = runner.invoke(  | 
                    
| 2059 | 
                        - cli.derivepassphrase,  | 
                    |
| 2060 | 
                        - ["export", "--help"],  | 
                    |
| 724 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 725 | 
                        + ["--version"],  | 
                    |
| 2061 | 726 | 
                        catch_exceptions=False,  | 
                    
| 2062 | 727 | 
                        )  | 
                    
| 2063 | 
                        - assert result.clean_exit(  | 
                    |
| 2064 | 
                        - empty_stderr=True, output="only available subcommand"  | 
                    |
| 2065 | 
                        - ), "expected clean exit, and known help text"  | 
                    |
| 728 | 
                        + assert result.clean_exit(empty_stderr=True, output=cli.PROG_NAME), (  | 
                    |
| 729 | 
                        + "expected clean exit, and program name in version text"  | 
                    |
| 730 | 
                        + )  | 
                    |
| 731 | 
                        + assert result.clean_exit(empty_stderr=True, output=cli.VERSION), (  | 
                    |
| 732 | 
                        + "expected clean exit, and version in help text"  | 
                    |
| 733 | 
                        + )  | 
                    |
| 2066 | 734 | 
                         | 
                    
| 2067 | 
                        - # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 2068 | 
                        - # check for?  | 
                    |
| 2069 | 
                        - def test_102_help_output_export_vault(  | 
                    |
| 735 | 
                        + @Parametrize.CHARSET_NAME  | 
                    |
| 736 | 
                        + def test_201_disable_character_set(  | 
                    |
| 2070 | 737 | 
                        self,  | 
                    
| 738 | 
                        + charset_name: str,  | 
                    |
| 2071 | 739 | 
                        ) -> None:  | 
                    
| 2072 | 
                        - """The "export vault" subcommand help text has known content.  | 
                    |
| 2073 | 
                        -  | 
                    |
| 2074 | 
                        - TODO: Do we actually need this? What should we check for?  | 
                    |
| 2075 | 
                        -  | 
                    |
| 2076 | 
                        - """  | 
                    |
| 740 | 
                        + """Named character classes can be disabled on the command-line."""  | 
                    |
| 741 | 
                        +        option = f"--{charset_name}"
                       | 
                    |
| 742 | 
                        +        charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
                       | 
                    |
| 2077 | 743 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2078 | 744 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2079 | 745 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2086,25 +752,27 @@ class TestAllCLI:  | 
                  
| 2086 | 752 | 
                        runner=runner,  | 
                    
| 2087 | 753 | 
                        )  | 
                    
| 2088 | 754 | 
                        )  | 
                    
| 755 | 
                        + monkeypatch.setattr(  | 
                    |
| 756 | 
                        + cli_helpers,  | 
                    |
| 757 | 
                        + "prompt_for_passphrase",  | 
                    |
| 758 | 
                        + callables.auto_prompt,  | 
                    |
| 759 | 
                        + )  | 
                    |
| 2089 | 760 | 
                        result = runner.invoke(  | 
                    
| 2090 | 
                        - cli.derivepassphrase,  | 
                    |
| 2091 | 
                        - ["export", "vault", "--help"],  | 
                    |
| 761 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 762 | 
                        + [option, "0", "-p", "--", DUMMY_SERVICE],  | 
                    |
| 763 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 2092 | 764 | 
                        catch_exceptions=False,  | 
                    
| 2093 | 765 | 
                        )  | 
                    
| 2094 | 
                        - assert result.clean_exit(  | 
                    |
| 2095 | 
                        - empty_stderr=True, output="Export a vault-native configuration"  | 
                    |
| 2096 | 
                        - ), "expected clean exit, and known help text"  | 
                    |
| 766 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit:"  | 
                    |
| 767 | 
                        + for c in charset:  | 
                    |
| 768 | 
                        + assert c not in result.stdout, (  | 
                    |
| 769 | 
                        +                f"derived password contains forbidden character {c!r}"
                       | 
                    |
| 770 | 
                        + )  | 
                    |
| 2097 | 771 | 
                         | 
                    
| 2098 | 
                        - # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 2099 | 
                        - # check for?  | 
                    |
| 2100 | 
                        - def test_103_help_output_vault(  | 
                    |
| 772 | 
                        + def test_202_disable_repetition(  | 
                    |
| 2101 | 773 | 
                        self,  | 
                    
| 2102 | 774 | 
                        ) -> None:  | 
                    
| 2103 | 
                        - """The "vault" subcommand help text has known content.  | 
                    |
| 2104 | 
                        -  | 
                    |
| 2105 | 
                        - TODO: Do we actually need this? What should we check for?  | 
                    |
| 2106 | 
                        -  | 
                    |
| 2107 | 
                        - """  | 
                    |
| 775 | 
                        + """Character repetition can be disabled on the command-line."""  | 
                    |
| 2108 | 776 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2109 | 777 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2110 | 778 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2117,27 +785,35 @@ class TestAllCLI:  | 
                  
| 2117 | 785 | 
                        runner=runner,  | 
                    
| 2118 | 786 | 
                        )  | 
                    
| 2119 | 787 | 
                        )  | 
                    
| 788 | 
                        + monkeypatch.setattr(  | 
                    |
| 789 | 
                        + cli_helpers,  | 
                    |
| 790 | 
                        + "prompt_for_passphrase",  | 
                    |
| 791 | 
                        + callables.auto_prompt,  | 
                    |
| 792 | 
                        + )  | 
                    |
| 2120 | 793 | 
                        result = runner.invoke(  | 
                    
| 2121 | 
                        - cli.derivepassphrase,  | 
                    |
| 2122 | 
                        - ["vault", "--help"],  | 
                    |
| 794 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 795 | 
                        + ["--repeat", "0", "-p", "--", DUMMY_SERVICE],  | 
                    |
| 796 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 2123 | 797 | 
                        catch_exceptions=False,  | 
                    
| 2124 | 798 | 
                        )  | 
                    
| 2125 | 
                        - assert result.clean_exit(  | 
                    |
| 2126 | 
                        - empty_stderr=True, output="Passphrase generation:\n"  | 
                    |
| 2127 | 
                        - ), "expected clean exit, and option groups in help text"  | 
                    |
| 2128 | 
                        - assert result.clean_exit(  | 
                    |
| 2129 | 
                        - empty_stderr=True, output="Use $VISUAL or $EDITOR to configure"  | 
                    |
| 2130 | 
                        - ), "expected clean exit, and option group epilog in help text"  | 
                    |
| 799 | 
                        + assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 800 | 
                        + "expected clean exit and empty stderr"  | 
                    |
| 801 | 
                        + )  | 
                    |
| 802 | 
                        +        passphrase = result.stdout.rstrip("\r\n")
                       | 
                    |
| 803 | 
                        + for i in range(len(passphrase) - 1):  | 
                    |
| 804 | 
                        + assert passphrase[i : i + 1] != passphrase[i + 1 : i + 2], (  | 
                    |
| 805 | 
                        + f"derived password contains repeated character "  | 
                    |
| 806 | 
                        +                f"at position {i}: {result.stdout!r}"
                       | 
                    |
| 807 | 
                        + )  | 
                    |
| 2131 | 808 | 
                         | 
                    
| 2132 | 
                        - @Parametrize.COMMAND_NON_EAGER_ARGUMENTS  | 
                    |
| 2133 | 
                        - @Parametrize.EAGER_ARGUMENTS  | 
                    |
| 2134 | 
                        - def test_200_eager_options(  | 
                    |
| 809 | 
                        + @Parametrize.CONFIG_WITH_KEY  | 
                    |
| 810 | 
                        + def test_204a_key_from_config(  | 
                    |
| 2135 | 811 | 
                        self,  | 
                    
| 2136 | 
                        - command: list[str],  | 
                    |
| 2137 | 
                        - arguments: list[str],  | 
                    |
| 2138 | 
                        - non_eager_arguments: list[str],  | 
                    |
| 812 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 813 | 
                        + config: _types.VaultConfig,  | 
                    |
| 2139 | 814 | 
                        ) -> None:  | 
                    
| 2140 | 
                        - """Eager options terminate option and argument processing."""  | 
                    |
| 815 | 
                        + """A stored configured SSH key will be used."""  | 
                    |
| 816 | 
                        + del running_ssh_agent  | 
                    |
| 2141 | 817 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2142 | 818 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2143 | 819 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2145,34 +821,40 @@ class TestAllCLI:  | 
                  
| 2145 | 821 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2146 | 822 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2147 | 823 | 
                        stack.enter_context(  | 
                    
| 2148 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 824 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2149 | 825 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2150 | 826 | 
                        runner=runner,  | 
                    
| 827 | 
                        + vault_config=config,  | 
                    |
| 2151 | 828 | 
                        )  | 
                    
| 2152 | 829 | 
                        )  | 
                    
| 830 | 
                        + monkeypatch.setattr(  | 
                    |
| 831 | 
                        + vault.Vault,  | 
                    |
| 832 | 
                        + "phrase_from_key",  | 
                    |
| 833 | 
                        + callables.phrase_from_key,  | 
                    |
| 834 | 
                        + )  | 
                    |
| 2153 | 835 | 
                        result = runner.invoke(  | 
                    
| 2154 | 
                        - cli.derivepassphrase,  | 
                    |
| 2155 | 
                        - [*command, *arguments, *non_eager_arguments],  | 
                    |
| 836 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 837 | 
                        + ["--", DUMMY_SERVICE],  | 
                    |
| 2156 | 838 | 
                        catch_exceptions=False,  | 
                    
| 2157 | 839 | 
                        )  | 
                    
| 2158 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 840 | 
                        + assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 841 | 
                        + "expected clean exit and empty stderr"  | 
                    |
| 842 | 
                        + )  | 
                    |
| 843 | 
                        + assert result.stdout  | 
                    |
| 844 | 
                        + assert (  | 
                    |
| 845 | 
                        +            result.stdout.rstrip("\n").encode("UTF-8")
                       | 
                    |
| 846 | 
                        + != DUMMY_RESULT_PASSPHRASE  | 
                    |
| 847 | 
                        + ), "known false output: phrase-based instead of key-based"  | 
                    |
| 848 | 
                        + assert (  | 
                    |
| 849 | 
                        +            result.stdout.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1
                       | 
                    |
| 850 | 
                        + ), "expected known output"  | 
                    |
| 2159 | 851 | 
                         | 
                    
| 2160 | 
                        - @Parametrize.ISATTY  | 
                    |
| 2161 | 
                        - @Parametrize.COLORFUL_COMMAND_INPUT  | 
                    |
| 2162 | 
                        - def test_201_automatic_color_mode(  | 
                    |
| 852 | 
                        + def test_204b_key_from_command_line(  | 
                    |
| 2163 | 853 | 
                        self,  | 
                    
| 2164 | 
                        - isatty: bool,  | 
                    |
| 2165 | 
                        - command_line: list[str],  | 
                    |
| 2166 | 
                        - input: str | None,  | 
                    |
| 854 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 2167 | 855 | 
                        ) -> None:  | 
                    
| 2168 | 
                        - """Auto-detect if color should be used.  | 
                    |
| 2169 | 
                        -  | 
                    |
| 2170 | 
                        - (The answer currently is always no. See the  | 
                    |
| 2171 | 
                        - [`conventional-configurable-text-styling` wishlist  | 
                    |
| 2172 | 
                        - entry](../wishlist/conventional-configurable-text-styling.md).)  | 
                    |
| 2173 | 
                        -  | 
                    |
| 2174 | 
                        - """  | 
                    |
| 2175 | 
                        - color = False  | 
                    |
| 856 | 
                        + """An SSH key requested on the command-line will be used."""  | 
                    |
| 857 | 
                        + del running_ssh_agent  | 
                    |
| 2176 | 858 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2177 | 859 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2178 | 860 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2180,40 +862,50 @@ class TestAllCLI:  | 
                  
| 2180 | 862 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2181 | 863 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2182 | 864 | 
                        stack.enter_context(  | 
                    
| 2183 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 865 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2184 | 866 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2185 | 867 | 
                        runner=runner,  | 
                    
| 868 | 
                        +                    vault_config={
                       | 
                    |
| 869 | 
                        +                        "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 870 | 
                        + },  | 
                    |
| 871 | 
                        + )  | 
                    |
| 872 | 
                        + )  | 
                    |
| 873 | 
                        + monkeypatch.setattr(  | 
                    |
| 874 | 
                        + cli_helpers,  | 
                    |
| 875 | 
                        + "get_suitable_ssh_keys",  | 
                    |
| 876 | 
                        + callables.suitable_ssh_keys,  | 
                    |
| 2186 | 877 | 
                        )  | 
                    
| 878 | 
                        + monkeypatch.setattr(  | 
                    |
| 879 | 
                        + vault.Vault,  | 
                    |
| 880 | 
                        + "phrase_from_key",  | 
                    |
| 881 | 
                        + callables.phrase_from_key,  | 
                    |
| 2187 | 882 | 
                        )  | 
                    
| 2188 | 883 | 
                        result = runner.invoke(  | 
                    
| 2189 | 
                        - cli.derivepassphrase,  | 
                    |
| 2190 | 
                        - command_line,  | 
                    |
| 2191 | 
                        - input=input,  | 
                    |
| 884 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 885 | 
                        + ["-k", "--", DUMMY_SERVICE],  | 
                    |
| 886 | 
                        + input="1\n",  | 
                    |
| 2192 | 887 | 
                        catch_exceptions=False,  | 
                    
| 2193 | 
                        - color=isatty,  | 
                    |
| 2194 | 888 | 
                        )  | 
                    
| 889 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 890 | 
                        + assert result.stdout, "expected program output"  | 
                    |
| 891 | 
                        + last_line = result.stdout.splitlines(True)[-1]  | 
                    |
| 2195 | 892 | 
                        assert (  | 
                    
| 2196 | 
                        - not color  | 
                    |
| 2197 | 
                        - or "\x1b[0m" in result.stderr  | 
                    |
| 2198 | 
                        - or "\x1b[m" in result.stderr  | 
                    |
| 2199 | 
                        - ), "Expected color, but found no ANSI reset sequence"  | 
                    |
| 2200 | 
                        - assert color or "\x1b[" not in result.stderr, (  | 
                    |
| 2201 | 
                        - "Expected no color, but found an ANSI control sequence"  | 
                    |
| 893 | 
                        +            last_line.rstrip("\n").encode("UTF-8") != DUMMY_RESULT_PASSPHRASE
                       | 
                    |
| 894 | 
                        + ), "known false output: phrase-based instead of key-based"  | 
                    |
| 895 | 
                        +        assert last_line.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1, (
                       | 
                    |
| 896 | 
                        + "expected known output"  | 
                    |
| 2202 | 897 | 
                        )  | 
                    
| 2203 | 898 | 
                         | 
                    
| 2204 | 
                        - def test_202a_derivepassphrase_version_option_output(  | 
                    |
| 899 | 
                        + @Parametrize.BASE_CONFIG_WITH_KEY_VARIATIONS  | 
                    |
| 900 | 
                        + @Parametrize.KEY_INDEX  | 
                    |
| 901 | 
                        + def test_204c_key_override_on_command_line(  | 
                    |
| 2205 | 902 | 
                        self,  | 
                    
| 903 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 904 | 
                        + config: dict[str, Any],  | 
                    |
| 905 | 
                        + key_index: int,  | 
                    |
| 2206 | 906 | 
                        ) -> None:  | 
                    
| 2207 | 
                        - """The version output states supported features.  | 
                    |
| 2208 | 
                        -  | 
                    |
| 2209 | 
                        - The version output is parsed using [`parse_version_output`][].  | 
                    |
| 2210 | 
                        - Format examples can be found in  | 
                    |
| 2211 | 
                        - [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 2212 | 
                        - top-level `derivepassphrase` command, the output should contain  | 
                    |
| 2213 | 
                        - the known and supported derivation schemes, and a list of  | 
                    |
| 2214 | 
                        - subcommands.  | 
                    |
| 2215 | 
                        -  | 
                    |
| 2216 | 
                        - """  | 
                    |
| 907 | 
                        + """A command-line SSH key will override the configured key."""  | 
                    |
| 908 | 
                        + del running_ssh_agent  | 
                    |
| 2217 | 909 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2218 | 910 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2219 | 911 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2221,40 +913,38 @@ class TestAllCLI:  | 
                  
| 2221 | 913 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2222 | 914 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2223 | 915 | 
                        stack.enter_context(  | 
                    
| 2224 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 916 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2225 | 917 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2226 | 918 | 
                        runner=runner,  | 
                    
| 919 | 
                        + vault_config=config,  | 
                    |
| 920 | 
                        + )  | 
                    |
| 2227 | 921 | 
                        )  | 
                    
| 922 | 
                        + monkeypatch.setattr(  | 
                    |
| 923 | 
                        + ssh_agent.SSHAgentClient,  | 
                    |
| 924 | 
                        + "list_keys",  | 
                    |
| 925 | 
                        + callables.list_keys,  | 
                    |
| 926 | 
                        + )  | 
                    |
| 927 | 
                        + monkeypatch.setattr(  | 
                    |
| 928 | 
                        + ssh_agent.SSHAgentClient, "sign", callables.sign  | 
                    |
| 2228 | 929 | 
                        )  | 
                    
| 2229 | 930 | 
                        result = runner.invoke(  | 
                    
| 2230 | 
                        - cli.derivepassphrase,  | 
                    |
| 2231 | 
                        - ["--version"],  | 
                    |
| 2232 | 
                        - catch_exceptions=False,  | 
                    |
| 931 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 932 | 
                        + ["-k", "--", DUMMY_SERVICE],  | 
                    |
| 933 | 
                        +                input=f"{key_index}\n",
                       | 
                    |
| 2233 | 934 | 
                        )  | 
                    
| 2234 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 2235 | 
                        - assert result.stdout.strip(), "expected version output"  | 
                    |
| 2236 | 
                        - version_data = parse_version_output(result.stdout)  | 
                    |
| 2237 | 
                        - actually_known_schemes = dict.fromkeys(_types.DerivationScheme, True)  | 
                    |
| 2238 | 
                        - subcommands = set(_types.Subcommand)  | 
                    |
| 2239 | 
                        - assert version_data.derivation_schemes == actually_known_schemes  | 
                    |
| 2240 | 
                        - assert not version_data.foreign_configuration_formats  | 
                    |
| 2241 | 
                        - assert version_data.subcommands == subcommands  | 
                    |
| 2242 | 
                        - assert not version_data.features  | 
                    |
| 2243 | 
                        - assert not version_data.extras  | 
                    |
| 2244 | 
                        -  | 
                    |
| 2245 | 
                        - def test_202b_export_version_option_output(  | 
                    |
| 935 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 936 | 
                        + assert result.stdout, "expected program output"  | 
                    |
| 937 | 
                        + assert result.stderr, "expected stderr"  | 
                    |
| 938 | 
                        + assert "Error:" not in result.stderr, (  | 
                    |
| 939 | 
                        + "expected no error messages on stderr"  | 
                    |
| 940 | 
                        + )  | 
                    |
| 941 | 
                        +  | 
                    |
| 942 | 
                        + def test_205_service_phrase_if_key_in_global_config(  | 
                    |
| 2246 | 943 | 
                        self,  | 
                    
| 944 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 2247 | 945 | 
                        ) -> None:  | 
                    
| 2248 | 
                        - """The version output states supported features.  | 
                    |
| 2249 | 
                        -  | 
                    |
| 2250 | 
                        - The version output is parsed using [`parse_version_output`][].  | 
                    |
| 2251 | 
                        - Format examples can be found in  | 
                    |
| 2252 | 
                        - [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 2253 | 
                        - `export` command, the output should contain the known foreign  | 
                    |
| 2254 | 
                        - configuration formats (but not marked as supported), and a list  | 
                    |
| 2255 | 
                        - of subcommands.  | 
                    |
| 2256 | 
                        -  | 
                    |
| 2257 | 
                        - """  | 
                    |
| 946 | 
                        + """A command-line passphrase will override the configured key."""  | 
                    |
| 947 | 
                        + del running_ssh_agent  | 
                    |
| 2258 | 948 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2259 | 949 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2260 | 950 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2262,47 +952,53 @@ class TestAllCLI:  | 
                  
| 2262 | 952 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2263 | 953 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2264 | 954 | 
                        stack.enter_context(  | 
                    
| 2265 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 955 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2266 | 956 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2267 | 957 | 
                        runner=runner,  | 
                    
| 958 | 
                        +                    vault_config={
                       | 
                    |
| 959 | 
                        +                        "global": {"key": DUMMY_KEY1_B64},
                       | 
                    |
| 960 | 
                        +                        "services": {
                       | 
                    |
| 961 | 
                        +                            DUMMY_SERVICE: {
                       | 
                    |
| 962 | 
                        +                                "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
                       | 
                    |
| 963 | 
                        + **DUMMY_CONFIG_SETTINGS,  | 
                    |
| 964 | 
                        + }  | 
                    |
| 965 | 
                        + },  | 
                    |
| 966 | 
                        + },  | 
                    |
| 967 | 
                        + )  | 
                    |
| 2268 | 968 | 
                        )  | 
                    
| 969 | 
                        + monkeypatch.setattr(  | 
                    |
| 970 | 
                        + ssh_agent.SSHAgentClient,  | 
                    |
| 971 | 
                        + "list_keys",  | 
                    |
| 972 | 
                        + callables.list_keys,  | 
                    |
| 973 | 
                        + )  | 
                    |
| 974 | 
                        + monkeypatch.setattr(  | 
                    |
| 975 | 
                        + ssh_agent.SSHAgentClient, "sign", callables.sign  | 
                    |
| 2269 | 976 | 
                        )  | 
                    
| 2270 | 977 | 
                        result = runner.invoke(  | 
                    
| 2271 | 
                        - cli.derivepassphrase,  | 
                    |
| 2272 | 
                        - ["export", "--version"],  | 
                    |
| 978 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 979 | 
                        + ["--", DUMMY_SERVICE],  | 
                    |
| 2273 | 980 | 
                        catch_exceptions=False,  | 
                    
| 2274 | 981 | 
                        )  | 
                    
| 2275 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 2276 | 
                        - assert result.stdout.strip(), "expected version output"  | 
                    |
| 2277 | 
                        - version_data = parse_version_output(result.stdout)  | 
                    |
| 2278 | 
                        -        actually_known_formats: dict[str, bool] = {
                       | 
                    |
| 2279 | 
                        - _types.ForeignConfigurationFormat.VAULT_STOREROOM: False,  | 
                    |
| 2280 | 
                        - _types.ForeignConfigurationFormat.VAULT_V02: False,  | 
                    |
| 2281 | 
                        - _types.ForeignConfigurationFormat.VAULT_V03: False,  | 
                    |
| 2282 | 
                        - }  | 
                    |
| 2283 | 
                        - subcommands = set(_types.ExportSubcommand)  | 
                    |
| 2284 | 
                        - assert not version_data.derivation_schemes  | 
                    |
| 982 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 983 | 
                        + assert result.stdout, "expected program output"  | 
                    |
| 984 | 
                        + last_line = result.stdout.splitlines(True)[-1]  | 
                    |
| 2285 | 985 | 
                        assert (  | 
                    
| 2286 | 
                        - version_data.foreign_configuration_formats  | 
                    |
| 2287 | 
                        - == actually_known_formats  | 
                    |
| 986 | 
                        +            last_line.rstrip("\n").encode("UTF-8") != DUMMY_RESULT_PASSPHRASE
                       | 
                    |
| 987 | 
                        + ), "known false output: phrase-based instead of key-based"  | 
                    |
| 988 | 
                        +        assert last_line.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1, (
                       | 
                    |
| 989 | 
                        + "expected known output"  | 
                    |
| 2288 | 990 | 
                        )  | 
                    
| 2289 | 
                        - assert version_data.subcommands == subcommands  | 
                    |
| 2290 | 
                        - assert not version_data.features  | 
                    |
| 2291 | 
                        - assert not version_data.extras  | 
                    |
| 2292 | 991 | 
                         | 
                    
| 2293 | 
                        - def test_202c_export_vault_version_option_output(  | 
                    |
| 992 | 
                        + @Parametrize.KEY_OVERRIDING_IN_CONFIG  | 
                    |
| 993 | 
                        + def test_206_setting_phrase_thus_overriding_key_in_config(  | 
                    |
| 2294 | 994 | 
                        self,  | 
                    
| 995 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 996 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 997 | 
                        + config: _types.VaultConfig,  | 
                    |
| 998 | 
                        + command_line: list[str],  | 
                    |
| 2295 | 999 | 
                        ) -> None:  | 
                    
| 2296 | 
                        - """The version output states supported features.  | 
                    |
| 2297 | 
                        -  | 
                    |
| 2298 | 
                        - The version output is parsed using [`parse_version_output`][].  | 
                    |
| 2299 | 
                        - Format examples can be found in  | 
                    |
| 2300 | 
                        - [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 2301 | 
                        - `export vault` subcommand, the output should contain the  | 
                    |
| 2302 | 
                        - vault-specific subset of the known or supported foreign  | 
                    |
| 2303 | 
                        - configuration formats, and a list of available PEP 508 extras.  | 
                    |
| 2304 | 
                        -  | 
                    |
| 2305 | 
                        - """  | 
                    |
| 1000 | 
                        + """Configuring a passphrase atop an SSH key works, but warns."""  | 
                    |
| 1001 | 
                        + del running_ssh_agent  | 
                    |
| 2306 | 1002 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2307 | 1003 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2308 | 1004 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2310,54 +1006,59 @@ class TestAllCLI:  | 
                  
| 2310 | 1006 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2311 | 1007 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2312 | 1008 | 
                        stack.enter_context(  | 
                    
| 2313 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1009 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2314 | 1010 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2315 | 1011 | 
                        runner=runner,  | 
                    
| 1012 | 
                        + vault_config=config,  | 
                    |
| 1013 | 
                        + )  | 
                    |
| 1014 | 
                        + )  | 
                    |
| 1015 | 
                        + monkeypatch.setattr(  | 
                    |
| 1016 | 
                        + ssh_agent.SSHAgentClient,  | 
                    |
| 1017 | 
                        + "list_keys",  | 
                    |
| 1018 | 
                        + callables.list_keys,  | 
                    |
| 2316 | 1019 | 
                        )  | 
                    
| 1020 | 
                        + monkeypatch.setattr(  | 
                    |
| 1021 | 
                        + ssh_agent.SSHAgentClient, "sign", callables.sign  | 
                    |
| 2317 | 1022 | 
                        )  | 
                    
| 2318 | 1023 | 
                        result = runner.invoke(  | 
                    
| 2319 | 
                        - cli.derivepassphrase,  | 
                    |
| 2320 | 
                        - ["export", "vault", "--version"],  | 
                    |
| 1024 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 1025 | 
                        + command_line,  | 
                    |
| 1026 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 2321 | 1027 | 
                        catch_exceptions=False,  | 
                    
| 2322 | 1028 | 
                        )  | 
                    
| 2323 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 2324 | 
                        - assert result.stdout.strip(), "expected version output"  | 
                    |
| 2325 | 
                        - version_data = parse_version_output(result.stdout)  | 
                    |
| 2326 | 
                        -        actually_known_formats: dict[str, bool] = {}
                       | 
                    |
| 2327 | 
                        - actually_enabled_extras: set[str] = set()  | 
                    |
| 2328 | 
                        - with contextlib.suppress(ModuleNotFoundError):  | 
                    |
| 2329 | 
                        - from derivepassphrase.exporter import storeroom, vault_native # noqa: I001,PLC0415  | 
                    |
| 2330 | 
                        -  | 
                    |
| 2331 | 
                        -            actually_known_formats.update({
                       | 
                    |
| 2332 | 
                        - _types.ForeignConfigurationFormat.VAULT_STOREROOM: not storeroom.STUBBED,  | 
                    |
| 2333 | 
                        - _types.ForeignConfigurationFormat.VAULT_V02: not vault_native.STUBBED,  | 
                    |
| 2334 | 
                        - _types.ForeignConfigurationFormat.VAULT_V03: not vault_native.STUBBED,  | 
                    |
| 2335 | 
                        - })  | 
                    |
| 2336 | 
                        - with contextlib.suppress(ModuleNotFoundError):  | 
                    |
| 2337 | 
                        - import cryptography # noqa: F401,PLC0415  | 
                    |
| 2338 | 
                        -  | 
                    |
| 2339 | 
                        - actually_enabled_extras.add(_types.PEP508Extra.EXPORT)  | 
                    |
| 2340 | 
                        - assert not version_data.derivation_schemes  | 
                    |
| 2341 | 
                        - assert (  | 
                    |
| 2342 | 
                        - version_data.foreign_configuration_formats  | 
                    |
| 2343 | 
                        - == actually_known_formats  | 
                    |
| 2344 | 
                        - )  | 
                    |
| 2345 | 
                        - assert not version_data.subcommands  | 
                    |
| 2346 | 
                        - assert not version_data.features  | 
                    |
| 2347 | 
                        - assert version_data.extras == actually_enabled_extras  | 
                    |
| 1029 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 1030 | 
                        + assert not result.stdout.strip(), "expected no program output"  | 
                    |
| 1031 | 
                        + assert result.stderr, "expected known error output"  | 
                    |
| 1032 | 
                        + err_lines = result.stderr.splitlines(False)  | 
                    |
| 1033 | 
                        +        assert err_lines[0].startswith("Passphrase:")
                       | 
                    |
| 1034 | 
                        + assert machinery.warning_emitted(  | 
                    |
| 1035 | 
                        + "Setting a service passphrase is ineffective ",  | 
                    |
| 1036 | 
                        + caplog.record_tuples,  | 
                    |
| 1037 | 
                        + ) or machinery.warning_emitted(  | 
                    |
| 1038 | 
                        + "Setting a global passphrase is ineffective ",  | 
                    |
| 1039 | 
                        + caplog.record_tuples,  | 
                    |
| 1040 | 
                        + ), "expected known warning message"  | 
                    |
| 1041 | 
                        + assert all(map(is_warning_line, result.stderr.splitlines(True)))  | 
                    |
| 1042 | 
                        + assert all(  | 
                    |
| 1043 | 
                        + map(is_harmless_config_import_warning, caplog.record_tuples)  | 
                    |
| 1044 | 
                        + ), "unexpected error output"  | 
                    |
| 2348 | 1045 | 
                         | 
                    
| 2349 | 
                        - def test_202d_vault_version_option_output(  | 
                    |
| 1046 | 
                        + @hypothesis.given(  | 
                    |
| 1047 | 
                        + notes=strategies.text(  | 
                    |
| 1048 | 
                        + strategies.characters(  | 
                    |
| 1049 | 
                        + min_codepoint=32,  | 
                    |
| 1050 | 
                        + max_codepoint=126,  | 
                    |
| 1051 | 
                        + include_characters="\n",  | 
                    |
| 1052 | 
                        + ),  | 
                    |
| 1053 | 
                        + max_size=256,  | 
                    |
| 1054 | 
                        + ),  | 
                    |
| 1055 | 
                        + )  | 
                    |
| 1056 | 
                        + def test_207_service_with_notes_actually_prints_notes(  | 
                    |
| 2350 | 1057 | 
                        self,  | 
                    
| 1058 | 
                        + notes: str,  | 
                    |
| 2351 | 1059 | 
                        ) -> None:  | 
                    
| 2352 | 
                        - """The version output states supported features.  | 
                    |
| 2353 | 
                        -  | 
                    |
| 2354 | 
                        - The version output is parsed using [`parse_version_output`][].  | 
                    |
| 2355 | 
                        - Format examples can be found in  | 
                    |
| 2356 | 
                        - [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 2357 | 
                        - vault command, the output should not contain anything beyond the  | 
                    |
| 2358 | 
                        - first paragraph.  | 
                    |
| 2359 | 
                        -  | 
                    |
| 2360 | 
                        - """  | 
                    |
| 1060 | 
                        + """Service notes are printed, if they exist."""  | 
                    |
| 1061 | 
                        +        hypothesis.assume("Error:" not in notes)
                       | 
                    |
| 2361 | 1062 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2362 | 1063 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2363 | 1064 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2365,81 +1066,45 @@ class TestAllCLI:  | 
                  
| 2365 | 1066 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2366 | 1067 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2367 | 1068 | 
                        stack.enter_context(  | 
                    
| 2368 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 2369 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 2370 | 
                        - runner=runner,  | 
                    |
| 2371 | 
                        - )  | 
                    |
| 2372 | 
                        - )  | 
                    |
| 2373 | 
                        - result = runner.invoke(  | 
                    |
| 2374 | 
                        - cli.derivepassphrase,  | 
                    |
| 2375 | 
                        - ["vault", "--version"],  | 
                    |
| 2376 | 
                        - catch_exceptions=False,  | 
                    |
| 2377 | 
                        - )  | 
                    |
| 2378 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 2379 | 
                        - assert result.stdout.strip(), "expected version output"  | 
                    |
| 2380 | 
                        - version_data = parse_version_output(result.stdout)  | 
                    |
| 2381 | 
                        -  | 
                    |
| 2382 | 
                        - ssh_key_supported = True  | 
                    |
| 2383 | 
                        -  | 
                    |
| 2384 | 
                        - def react_to_notimplementederror(  | 
                    |
| 2385 | 
                        - _exc: BaseException,  | 
                    |
| 2386 | 
                        - ) -> None: # pragma: no cover[unused]  | 
                    |
| 2387 | 
                        - nonlocal ssh_key_supported  | 
                    |
| 2388 | 
                        - ssh_key_supported = False  | 
                    |
| 2389 | 
                        -  | 
                    |
| 2390 | 
                        -        with exceptiongroup.catch({  # noqa: SIM117
                       | 
                    |
| 2391 | 
                        - NotImplementedError: react_to_notimplementederror,  | 
                    |
| 2392 | 
                        - Exception: lambda *_args: None,  | 
                    |
| 2393 | 
                        - }):  | 
                    |
| 2394 | 
                        - with ssh_agent.SSHAgentClient.ensure_agent_subcontext():  | 
                    |
| 2395 | 
                        - pass  | 
                    |
| 2396 | 
                        -        features: dict[str, bool] = {
                       | 
                    |
| 2397 | 
                        - _types.Feature.SSH_KEY: ssh_key_supported,  | 
                    |
| 2398 | 
                        - }  | 
                    |
| 2399 | 
                        - assert not version_data.derivation_schemes  | 
                    |
| 2400 | 
                        - assert not version_data.foreign_configuration_formats  | 
                    |
| 2401 | 
                        - assert not version_data.subcommands  | 
                    |
| 2402 | 
                        - assert version_data.features == features  | 
                    |
| 2403 | 
                        - assert not version_data.extras  | 
                    |
| 2404 | 
                        -  | 
                    |
| 2405 | 
                        -  | 
                    |
| 2406 | 
                        -class TestCLI:  | 
                    |
| 2407 | 
                        - """Tests for the `derivepassphrase vault` command-line interface."""  | 
                    |
| 2408 | 
                        -  | 
                    |
| 2409 | 
                        - def test_200_help_output(  | 
                    |
| 2410 | 
                        - self,  | 
                    |
| 2411 | 
                        - ) -> None:  | 
                    |
| 2412 | 
                        - """The `--help` option emits help text."""  | 
                    |
| 2413 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 2414 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 2415 | 
                        - # with-statements.  | 
                    |
| 2416 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 2417 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 2418 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 2419 | 
                        - stack.enter_context(  | 
                    |
| 2420 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1069 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2421 | 1070 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2422 | 1071 | 
                        runner=runner,  | 
                    
| 1072 | 
                        +                    vault_config={
                       | 
                    |
| 1073 | 
                        +                        "global": {
                       | 
                    |
| 1074 | 
                        + "phrase": DUMMY_PASSPHRASE,  | 
                    |
| 1075 | 
                        + },  | 
                    |
| 1076 | 
                        +                        "services": {
                       | 
                    |
| 1077 | 
                        +                            DUMMY_SERVICE: {
                       | 
                    |
| 1078 | 
                        + "notes": notes,  | 
                    |
| 1079 | 
                        + **DUMMY_CONFIG_SETTINGS,  | 
                    |
| 1080 | 
                        + },  | 
                    |
| 1081 | 
                        + },  | 
                    |
| 1082 | 
                        + },  | 
                    |
| 2423 | 1083 | 
                        )  | 
                    
| 2424 | 1084 | 
                        )  | 
                    
| 2425 | 1085 | 
                        result = runner.invoke(  | 
                    
| 2426 | 1086 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2427 | 
                        - ["--help"],  | 
                    |
| 2428 | 
                        - catch_exceptions=False,  | 
                    |
| 1087 | 
                        + ["--", DUMMY_SERVICE],  | 
                    |
| 1088 | 
                        + )  | 
                    |
| 1089 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 1090 | 
                        + assert result.stdout, "expected program output"  | 
                    |
| 1091 | 
                        + assert result.stdout.strip() == DUMMY_RESULT_PASSPHRASE.decode(  | 
                    |
| 1092 | 
                        + "ascii"  | 
                    |
| 1093 | 
                        + ), "expected known program output"  | 
                    |
| 1094 | 
                        + assert result.stderr or not notes.strip(), "expected stderr"  | 
                    |
| 1095 | 
                        + assert "Error:" not in result.stderr, (  | 
                    |
| 1096 | 
                        + "expected no error messages on stderr"  | 
                    |
| 1097 | 
                        + )  | 
                    |
| 1098 | 
                        + assert result.stderr.strip() == notes.strip(), (  | 
                    |
| 1099 | 
                        + "expected known stderr contents"  | 
                    |
| 2429 | 1100 | 
                        )  | 
                    
| 2430 | 
                        - assert result.clean_exit(  | 
                    |
| 2431 | 
                        - empty_stderr=True, output="Passphrase generation:\n"  | 
                    |
| 2432 | 
                        - ), "expected clean exit, and option groups in help text"  | 
                    |
| 2433 | 
                        - assert result.clean_exit(  | 
                    |
| 2434 | 
                        - empty_stderr=True, output="Use $VISUAL or $EDITOR to configure"  | 
                    |
| 2435 | 
                        - ), "expected clean exit, and option group epilog in help text"  | 
                    |
| 2436 | 1101 | 
                         | 
                    
| 2437 | 
                        - # TODO(the-13th-letter): Remove this test once  | 
                    |
| 2438 | 
                        - # TestAllCLI.test_202_version_option_output no longer xfails.  | 
                    |
| 2439 | 
                        - def test_200a_version_output(  | 
                    |
| 1102 | 
                        + @Parametrize.VAULT_CHARSET_OPTION  | 
                    |
| 1103 | 
                        + def test_210_invalid_argument_range(  | 
                    |
| 2440 | 1104 | 
                        self,  | 
                    
| 1105 | 
                        + option: str,  | 
                    |
| 2441 | 1106 | 
                        ) -> None:  | 
                    
| 2442 | 
                        - """The `--version` option emits version information."""  | 
                    |
| 1107 | 
                        + """Requesting invalidly many characters from a class fails."""  | 
                    |
| 2443 | 1108 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2444 | 1109 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2445 | 1110 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2452,26 +1117,26 @@ class TestCLI:  | 
                  
| 2452 | 1117 | 
                        runner=runner,  | 
                    
| 2453 | 1118 | 
                        )  | 
                    
| 2454 | 1119 | 
                        )  | 
                    
| 1120 | 
                        + for value in "-42", "invalid":  | 
                    |
| 2455 | 1121 | 
                        result = runner.invoke(  | 
                    
| 2456 | 1122 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2457 | 
                        - ["--version"],  | 
                    |
| 1123 | 
                        + [option, value, "-p", "--", DUMMY_SERVICE],  | 
                    |
| 1124 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 2458 | 1125 | 
                        catch_exceptions=False,  | 
                    
| 2459 | 1126 | 
                        )  | 
                    
| 2460 | 
                        - assert result.clean_exit(empty_stderr=True, output=cli.PROG_NAME), (  | 
                    |
| 2461 | 
                        - "expected clean exit, and program name in version text"  | 
                    |
| 2462 | 
                        - )  | 
                    |
| 2463 | 
                        - assert result.clean_exit(empty_stderr=True, output=cli.VERSION), (  | 
                    |
| 2464 | 
                        - "expected clean exit, and version in help text"  | 
                    |
| 1127 | 
                        + assert result.error_exit(error="Invalid value"), (  | 
                    |
| 1128 | 
                        + "expected error exit and known error message"  | 
                    |
| 2465 | 1129 | 
                        )  | 
                    
| 2466 | 1130 | 
                         | 
                    
| 2467 | 
                        - @Parametrize.CHARSET_NAME  | 
                    |
| 2468 | 
                        - def test_201_disable_character_set(  | 
                    |
| 1131 | 
                        + @Parametrize.OPTION_COMBINATIONS_SERVICE_NEEDED  | 
                    |
| 1132 | 
                        + def test_211_service_needed(  | 
                    |
| 2469 | 1133 | 
                        self,  | 
                    
| 2470 | 
                        - charset_name: str,  | 
                    |
| 1134 | 
                        + options: list[str],  | 
                    |
| 1135 | 
                        + service: bool | None,  | 
                    |
| 1136 | 
                        + input: str | None,  | 
                    |
| 1137 | 
                        + check_success: bool,  | 
                    |
| 2471 | 1138 | 
                        ) -> None:  | 
                    
| 2472 | 
                        - """Named character classes can be disabled on the command-line."""  | 
                    |
| 2473 | 
                        -        option = f"--{charset_name}"
                       | 
                    |
| 2474 | 
                        -        charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
                       | 
                    |
| 1139 | 
                        + """We require or forbid a service argument, depending on options."""  | 
                    |
| 2475 | 1140 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2476 | 1141 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2477 | 1142 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2479,9 +1144,10 @@ class TestCLI:  | 
                  
| 2479 | 1144 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2480 | 1145 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2481 | 1146 | 
                        stack.enter_context(  | 
                    
| 2482 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1147 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2483 | 1148 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2484 | 1149 | 
                        runner=runner,  | 
                    
| 1150 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 2485 | 1151 | 
                        )  | 
                    
| 2486 | 1152 | 
                        )  | 
                    
| 2487 | 1153 | 
                        monkeypatch.setattr(  | 
                    
| ... | ... | 
                      @@ -2491,30 +1157,37 @@ class TestCLI:  | 
                  
| 2491 | 1157 | 
                        )  | 
                    
| 2492 | 1158 | 
                        result = runner.invoke(  | 
                    
| 2493 | 1159 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2494 | 
                        - [option, "0", "-p", "--", DUMMY_SERVICE],  | 
                    |
| 2495 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 1160 | 
                        + options if service else [*options, "--", DUMMY_SERVICE],  | 
                    |
| 1161 | 
                        + input=input,  | 
                    |
| 2496 | 1162 | 
                        catch_exceptions=False,  | 
                    
| 2497 | 1163 | 
                        )  | 
                    
| 2498 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit:"  | 
                    |
| 2499 | 
                        - for c in charset:  | 
                    |
| 2500 | 
                        - assert c not in result.stdout, (  | 
                    |
| 2501 | 
                        -                f"derived password contains forbidden character {c!r}"
                       | 
                    |
| 1164 | 
                        + if service is not None:  | 
                    |
| 1165 | 
                        + err_msg = (  | 
                    |
| 1166 | 
                        + " requires a SERVICE"  | 
                    |
| 1167 | 
                        + if service  | 
                    |
| 1168 | 
                        + else " does not take a SERVICE argument"  | 
                    |
| 2502 | 1169 | 
                        )  | 
                    
| 2503 | 
                        -  | 
                    |
| 2504 | 
                        - def test_202_disable_repetition(  | 
                    |
| 2505 | 
                        - self,  | 
                    |
| 2506 | 
                        - ) -> None:  | 
                    |
| 2507 | 
                        - """Character repetition can be disabled on the command-line."""  | 
                    |
| 2508 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 1170 | 
                        + assert result.error_exit(error=err_msg), (  | 
                    |
| 1171 | 
                        + "expected error exit and known error message"  | 
                    |
| 1172 | 
                        + )  | 
                    |
| 1173 | 
                        + else:  | 
                    |
| 1174 | 
                        + assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 1175 | 
                        + "expected clean exit"  | 
                    |
| 1176 | 
                        + )  | 
                    |
| 1177 | 
                        + if check_success:  | 
                    |
| 2509 | 1178 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2510 | 1179 | 
                        # with-statements.  | 
                    
| 2511 | 1180 | 
                        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    
| 2512 | 1181 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2513 | 1182 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2514 | 1183 | 
                        stack.enter_context(  | 
                    
| 2515 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1184 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 2516 | 1185 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2517 | 1186 | 
                        runner=runner,  | 
                    
| 1187 | 
                        +                        vault_config={
                       | 
                    |
| 1188 | 
                        +                            "global": {"phrase": "abc"},
                       | 
                    |
| 1189 | 
                        +                            "services": {},
                       | 
                    |
| 1190 | 
                        + },  | 
                    |
| 2518 | 1191 | 
                        )  | 
                    
| 2519 | 1192 | 
                        )  | 
                    
| 2520 | 1193 | 
                        monkeypatch.setattr(  | 
                    
| ... | ... | 
                      @@ -2524,28 +1197,29 @@ class TestCLI:  | 
                  
| 2524 | 1197 | 
                        )  | 
                    
| 2525 | 1198 | 
                        result = runner.invoke(  | 
                    
| 2526 | 1199 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2527 | 
                        - ["--repeat", "0", "-p", "--", DUMMY_SERVICE],  | 
                    |
| 2528 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 1200 | 
                        + [*options, "--", DUMMY_SERVICE] if service else options,  | 
                    |
| 1201 | 
                        + input=input,  | 
                    |
| 2529 | 1202 | 
                        catch_exceptions=False,  | 
                    
| 2530 | 1203 | 
                        )  | 
                    
| 2531 | 
                        - assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 2532 | 
                        - "expected clean exit and empty stderr"  | 
                    |
| 2533 | 
                        - )  | 
                    |
| 2534 | 
                        -        passphrase = result.stdout.rstrip("\r\n")
                       | 
                    |
| 2535 | 
                        - for i in range(len(passphrase) - 1):  | 
                    |
| 2536 | 
                        - assert passphrase[i : i + 1] != passphrase[i + 1 : i + 2], (  | 
                    |
| 2537 | 
                        - f"derived password contains repeated character "  | 
                    |
| 2538 | 
                        -                f"at position {i}: {result.stdout!r}"
                       | 
                    |
| 2539 | 
                        - )  | 
                    |
| 1204 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 2540 | 1205 | 
                         | 
                    
| 2541 | 
                        - @Parametrize.CONFIG_WITH_KEY  | 
                    |
| 2542 | 
                        - def test_204a_key_from_config(  | 
                    |
| 1206 | 
                        + def test_211a_empty_service_name_causes_warning(  | 
                    |
| 2543 | 1207 | 
                        self,  | 
                    
| 2544 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 2545 | 
                        - config: _types.VaultConfig,  | 
                    |
| 1208 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 2546 | 1209 | 
                        ) -> None:  | 
                    
| 2547 | 
                        - """A stored configured SSH key will be used."""  | 
                    |
| 2548 | 
                        - del running_ssh_agent  | 
                    |
| 1210 | 
                        + """Using an empty service name (where permissible) warns.  | 
                    |
| 1211 | 
                        +  | 
                    |
| 1212 | 
                        + Only the `--config` option can optionally take a service name.  | 
                    |
| 1213 | 
                        +  | 
                    |
| 1214 | 
                        + """  | 
                    |
| 1215 | 
                        +  | 
                    |
| 1216 | 
                        + def is_expected_warning(record: tuple[str, int, str]) -> bool:  | 
                    |
| 1217 | 
                        + return is_harmless_config_import_warning(  | 
                    |
| 1218 | 
                        + record  | 
                    |
| 1219 | 
                        + ) or machinery.warning_emitted(  | 
                    |
| 1220 | 
                        + "An empty SERVICE is not supported by vault(1)", [record]  | 
                    |
| 1221 | 
                        + )  | 
                    |
| 1222 | 
                        +  | 
                    |
| 2549 | 1223 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2550 | 1224 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2551 | 1225 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2556,37 +1230,52 @@ class TestCLI:  | 
                  
| 2556 | 1230 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 2557 | 1231 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2558 | 1232 | 
                        runner=runner,  | 
                    
| 2559 | 
                        - vault_config=config,  | 
                    |
| 1233 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 2560 | 1234 | 
                        )  | 
                    
| 2561 | 1235 | 
                        )  | 
                    
| 2562 | 1236 | 
                        monkeypatch.setattr(  | 
                    
| 2563 | 
                        - vault.Vault,  | 
                    |
| 2564 | 
                        - "phrase_from_key",  | 
                    |
| 2565 | 
                        - callables.phrase_from_key,  | 
                    |
| 1237 | 
                        + cli_helpers,  | 
                    |
| 1238 | 
                        + "prompt_for_passphrase",  | 
                    |
| 1239 | 
                        + callables.auto_prompt,  | 
                    |
| 2566 | 1240 | 
                        )  | 
                    
| 2567 | 1241 | 
                        result = runner.invoke(  | 
                    
| 2568 | 1242 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2569 | 
                        - ["--", DUMMY_SERVICE],  | 
                    |
| 1243 | 
                        + ["--config", "--length=30", "--", ""],  | 
                    |
| 2570 | 1244 | 
                        catch_exceptions=False,  | 
                    
| 2571 | 1245 | 
                        )  | 
                    
| 2572 | 
                        - assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 2573 | 
                        - "expected clean exit and empty stderr"  | 
                    |
| 1246 | 
                        + assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 1247 | 
                        + assert result.stderr is not None, "expected known error output"  | 
                    |
| 1248 | 
                        + assert all(map(is_expected_warning, caplog.record_tuples)), (  | 
                    |
| 1249 | 
                        + "expected known error output"  | 
                    |
| 2574 | 1250 | 
                        )  | 
                    
| 2575 | 
                        - assert result.stdout  | 
                    |
| 2576 | 
                        - assert (  | 
                    |
| 2577 | 
                        -            result.stdout.rstrip("\n").encode("UTF-8")
                       | 
                    |
| 2578 | 
                        - != DUMMY_RESULT_PASSPHRASE  | 
                    |
| 2579 | 
                        - ), "known false output: phrase-based instead of key-based"  | 
                    |
| 2580 | 
                        - assert (  | 
                    |
| 2581 | 
                        -            result.stdout.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1
                       | 
                    |
| 2582 | 
                        - ), "expected known output"  | 
                    |
| 1251 | 
                        +            assert cli_helpers.load_config() == {
                       | 
                    |
| 1252 | 
                        +                "global": {"length": 30},
                       | 
                    |
| 1253 | 
                        +                "services": {},
                       | 
                    |
| 1254 | 
                        + }, "requested configuration change was not applied"  | 
                    |
| 1255 | 
                        + caplog.clear()  | 
                    |
| 1256 | 
                        + result = runner.invoke(  | 
                    |
| 1257 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 1258 | 
                        + ["--import", "-"],  | 
                    |
| 1259 | 
                        +                input=json.dumps({"services": {"": {"length": 40}}}),
                       | 
                    |
| 1260 | 
                        + catch_exceptions=False,  | 
                    |
| 1261 | 
                        + )  | 
                    |
| 1262 | 
                        + assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 1263 | 
                        + assert result.stderr is not None, "expected known error output"  | 
                    |
| 1264 | 
                        + assert all(map(is_expected_warning, caplog.record_tuples)), (  | 
                    |
| 1265 | 
                        + "expected known error output"  | 
                    |
| 1266 | 
                        + )  | 
                    |
| 1267 | 
                        +            assert cli_helpers.load_config() == {
                       | 
                    |
| 1268 | 
                        +                "global": {"length": 30},
                       | 
                    |
| 1269 | 
                        +                "services": {"": {"length": 40}},
                       | 
                    |
| 1270 | 
                        + }, "requested configuration change was not applied"  | 
                    |
| 2583 | 1271 | 
                         | 
                    
| 2584 | 
                        - def test_204b_key_from_command_line(  | 
                    |
| 1272 | 
                        + @Parametrize.OPTION_COMBINATIONS_INCOMPATIBLE  | 
                    |
| 1273 | 
                        + def test_212_incompatible_options(  | 
                    |
| 2585 | 1274 | 
                        self,  | 
                    
| 2586 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 1275 | 
                        + options: list[str],  | 
                    |
| 1276 | 
                        + service: bool | None,  | 
                    |
| 2587 | 1277 | 
                        ) -> None:  | 
                    
| 2588 | 
                        - """An SSH key requested on the command-line will be used."""  | 
                    |
| 2589 | 
                        - del running_ssh_agent  | 
                    |
| 1278 | 
                        + """Incompatible options are detected."""  | 
                    |
| 2590 | 1279 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2591 | 1280 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2592 | 1281 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2594,50 +1283,28 @@ class TestCLI:  | 
                  
| 2594 | 1283 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2595 | 1284 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2596 | 1285 | 
                        stack.enter_context(  | 
                    
| 2597 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 1286 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 2598 | 1287 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2599 | 1288 | 
                        runner=runner,  | 
                    
| 2600 | 
                        -                    vault_config={
                       | 
                    |
| 2601 | 
                        -                        "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 2602 | 
                        - },  | 
                    |
| 2603 | 
                        - )  | 
                    |
| 2604 | 1289 | 
                        )  | 
                    
| 2605 | 
                        - monkeypatch.setattr(  | 
                    |
| 2606 | 
                        - cli_helpers,  | 
                    |
| 2607 | 
                        - "get_suitable_ssh_keys",  | 
                    |
| 2608 | 
                        - callables.suitable_ssh_keys,  | 
                    |
| 2609 | 
                        - )  | 
                    |
| 2610 | 
                        - monkeypatch.setattr(  | 
                    |
| 2611 | 
                        - vault.Vault,  | 
                    |
| 2612 | 
                        - "phrase_from_key",  | 
                    |
| 2613 | 
                        - callables.phrase_from_key,  | 
                    |
| 2614 | 1290 | 
                        )  | 
                    
| 2615 | 1291 | 
                        result = runner.invoke(  | 
                    
| 2616 | 1292 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2617 | 
                        - ["-k", "--", DUMMY_SERVICE],  | 
                    |
| 2618 | 
                        - input="1\n",  | 
                    |
| 1293 | 
                        + [*options, "--", DUMMY_SERVICE] if service else options,  | 
                    |
| 1294 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 2619 | 1295 | 
                        catch_exceptions=False,  | 
                    
| 2620 | 1296 | 
                        )  | 
                    
| 2621 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 2622 | 
                        - assert result.stdout, "expected program output"  | 
                    |
| 2623 | 
                        - last_line = result.stdout.splitlines(True)[-1]  | 
                    |
| 2624 | 
                        - assert (  | 
                    |
| 2625 | 
                        -            last_line.rstrip("\n").encode("UTF-8") != DUMMY_RESULT_PASSPHRASE
                       | 
                    |
| 2626 | 
                        - ), "known false output: phrase-based instead of key-based"  | 
                    |
| 2627 | 
                        -        assert last_line.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1, (
                       | 
                    |
| 2628 | 
                        - "expected known output"  | 
                    |
| 1297 | 
                        + assert result.error_exit(error="mutually exclusive with "), (  | 
                    |
| 1298 | 
                        + "expected error exit and known error message"  | 
                    |
| 2629 | 1299 | 
                        )  | 
                    
| 2630 | 1300 | 
                         | 
                    
| 2631 | 
                        - @Parametrize.BASE_CONFIG_WITH_KEY_VARIATIONS  | 
                    |
| 2632 | 
                        - @Parametrize.KEY_INDEX  | 
                    |
| 2633 | 
                        - def test_204c_key_override_on_command_line(  | 
                    |
| 1301 | 
                        + @Parametrize.VALID_TEST_CONFIGS  | 
                    |
| 1302 | 
                        + def test_213_import_config_success(  | 
                    |
| 2634 | 1303 | 
                        self,  | 
                    
| 2635 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 2636 | 
                        - config: dict[str, Any],  | 
                    |
| 2637 | 
                        - key_index: int,  | 
                    |
| 1304 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 1305 | 
                        + config: Any,  | 
                    |
| 2638 | 1306 | 
                        ) -> None:  | 
                    
| 2639 | 
                        - """A command-line SSH key will override the configured key."""  | 
                    |
| 2640 | 
                        - del running_ssh_agent  | 
                    |
| 1307 | 
                        + """Importing a configuration works."""  | 
                    |
| 2641 | 1308 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2642 | 1309 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2643 | 1310 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2648,89 +1315,54 @@ class TestCLI:  | 
                  
| 2648 | 1315 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 2649 | 1316 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2650 | 1317 | 
                        runner=runner,  | 
                    
| 2651 | 
                        - vault_config=config,  | 
                    |
| 1318 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 2652 | 1319 | 
                        )  | 
                    
| 2653 | 1320 | 
                        )  | 
                    
| 2654 | 
                        - monkeypatch.setattr(  | 
                    |
| 2655 | 
                        - ssh_agent.SSHAgentClient,  | 
                    |
| 2656 | 
                        - "list_keys",  | 
                    |
| 2657 | 
                        - callables.list_keys,  | 
                    |
| 2658 | 
                        - )  | 
                    |
| 2659 | 
                        - monkeypatch.setattr(  | 
                    |
| 2660 | 
                        - ssh_agent.SSHAgentClient, "sign", callables.sign  | 
                    |
| 2661 | 
                        - )  | 
                    |
| 2662 | 1321 | 
                        result = runner.invoke(  | 
                    
| 2663 | 1322 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2664 | 
                        - ["-k", "--", DUMMY_SERVICE],  | 
                    |
| 2665 | 
                        -                input=f"{key_index}\n",
                       | 
                    |
| 2666 | 
                        - )  | 
                    |
| 2667 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 2668 | 
                        - assert result.stdout, "expected program output"  | 
                    |
| 2669 | 
                        - assert result.stderr, "expected stderr"  | 
                    |
| 2670 | 
                        - assert "Error:" not in result.stderr, (  | 
                    |
| 2671 | 
                        - "expected no error messages on stderr"  | 
                    |
| 1323 | 
                        + ["--import", "-"],  | 
                    |
| 1324 | 
                        + input=json.dumps(config),  | 
                    |
| 1325 | 
                        + catch_exceptions=False,  | 
                    |
| 2672 | 1326 | 
                        )  | 
                    
| 1327 | 
                        + config_txt = cli_helpers.config_filename(  | 
                    |
| 1328 | 
                        + subsystem="vault"  | 
                    |
| 1329 | 
                        + ).read_text(encoding="UTF-8")  | 
                    |
| 1330 | 
                        + config2 = json.loads(config_txt)  | 
                    |
| 1331 | 
                        + assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 1332 | 
                        + assert config2 == config, "config not imported correctly"  | 
                    |
| 1333 | 
                        + assert not result.stderr or all( # pragma: no branch  | 
                    |
| 1334 | 
                        + map(is_harmless_config_import_warning, caplog.record_tuples)  | 
                    |
| 1335 | 
                        + ), "unexpected error output"  | 
                    |
| 1336 | 
                        + assert_vault_config_is_indented_and_line_broken(config_txt)  | 
                    |
| 2673 | 1337 | 
                         | 
                    
| 2674 | 
                        - def test_205_service_phrase_if_key_in_global_config(  | 
                    |
| 2675 | 
                        - self,  | 
                    |
| 2676 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 2677 | 
                        - ) -> None:  | 
                    |
| 2678 | 
                        - """A command-line passphrase will override the configured key."""  | 
                    |
| 2679 | 
                        - del running_ssh_agent  | 
                    |
| 2680 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 2681 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 2682 | 
                        - # with-statements.  | 
                    |
| 2683 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 2684 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 2685 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 2686 | 
                        - stack.enter_context(  | 
                    |
| 2687 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 2688 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 2689 | 
                        - runner=runner,  | 
                    |
| 2690 | 
                        -                    vault_config={
                       | 
                    |
| 2691 | 
                        -                        "global": {"key": DUMMY_KEY1_B64},
                       | 
                    |
| 2692 | 
                        -                        "services": {
                       | 
                    |
| 2693 | 
                        -                            DUMMY_SERVICE: {
                       | 
                    |
| 2694 | 
                        -                                "phrase": DUMMY_PASSPHRASE.rstrip("\n"),
                       | 
                    |
| 2695 | 
                        - **DUMMY_CONFIG_SETTINGS,  | 
                    |
| 2696 | 
                        - }  | 
                    |
| 2697 | 
                        - },  | 
                    |
| 2698 | 
                        - },  | 
                    |
| 2699 | 
                        - )  | 
                    |
| 2700 | 
                        - )  | 
                    |
| 2701 | 
                        - monkeypatch.setattr(  | 
                    |
| 2702 | 
                        - ssh_agent.SSHAgentClient,  | 
                    |
| 2703 | 
                        - "list_keys",  | 
                    |
| 2704 | 
                        - callables.list_keys,  | 
                    |
| 2705 | 
                        - )  | 
                    |
| 2706 | 
                        - monkeypatch.setattr(  | 
                    |
| 2707 | 
                        - ssh_agent.SSHAgentClient, "sign", callables.sign  | 
                    |
| 1338 | 
                        + @hypothesis.settings(  | 
                    |
| 1339 | 
                        + suppress_health_check=[  | 
                    |
| 1340 | 
                        + *hypothesis.settings().suppress_health_check,  | 
                    |
| 1341 | 
                        + hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 1342 | 
                        + ],  | 
                    |
| 2708 | 1343 | 
                        )  | 
                    
| 2709 | 
                        - result = runner.invoke(  | 
                    |
| 2710 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 2711 | 
                        - ["--", DUMMY_SERVICE],  | 
                    |
| 2712 | 
                        - catch_exceptions=False,  | 
                    |
| 1344 | 
                        + @hypothesis.given(  | 
                    |
| 1345 | 
                        + conf=hypothesis_machinery.smudged_vault_test_config(  | 
                    |
| 1346 | 
                        + strategies.sampled_from([  | 
                    |
| 1347 | 
                        + conf for conf in data.TEST_CONFIGS if conf.is_valid()  | 
                    |
| 1348 | 
                        + ])  | 
                    |
| 2713 | 1349 | 
                        )  | 
                    
| 2714 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 2715 | 
                        - assert result.stdout, "expected program output"  | 
                    |
| 2716 | 
                        - last_line = result.stdout.splitlines(True)[-1]  | 
                    |
| 2717 | 
                        - assert (  | 
                    |
| 2718 | 
                        -            last_line.rstrip("\n").encode("UTF-8") != DUMMY_RESULT_PASSPHRASE
                       | 
                    |
| 2719 | 
                        - ), "known false output: phrase-based instead of key-based"  | 
                    |
| 2720 | 
                        -        assert last_line.rstrip("\n").encode("UTF-8") == DUMMY_RESULT_KEY1, (
                       | 
                    |
| 2721 | 
                        - "expected known output"  | 
                    |
| 2722 | 1350 | 
                        )  | 
                    
| 2723 | 
                        -  | 
                    |
| 2724 | 
                        - @Parametrize.KEY_OVERRIDING_IN_CONFIG  | 
                    |
| 2725 | 
                        - def test_206_setting_phrase_thus_overriding_key_in_config(  | 
                    |
| 1351 | 
                        + def test_213a_import_config_success(  | 
                    |
| 2726 | 1352 | 
                        self,  | 
                    
| 2727 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 2728 | 1353 | 
                        caplog: pytest.LogCaptureFixture,  | 
                    
| 2729 | 
                        - config: _types.VaultConfig,  | 
                    |
| 2730 | 
                        - command_line: list[str],  | 
                    |
| 1354 | 
                        + conf: data.VaultTestConfig,  | 
                    |
| 2731 | 1355 | 
                        ) -> None:  | 
                    
| 2732 | 
                        - """Configuring a passphrase atop an SSH key works, but warns."""  | 
                    |
| 2733 | 
                        - del running_ssh_agent  | 
                    |
| 1356 | 
                        + """Importing a smudged configuration works.  | 
                    |
| 1357 | 
                        +  | 
                    |
| 1358 | 
                        + Tested via hypothesis.  | 
                    |
| 1359 | 
                        +  | 
                    |
| 1360 | 
                        + """  | 
                    |
| 1361 | 
                        + config = conf.config  | 
                    |
| 1362 | 
                        + config2 = copy.deepcopy(config)  | 
                    |
| 1363 | 
                        + _types.clean_up_falsy_vault_config_values(config2)  | 
                    |
| 1364 | 
                        + # Reset caplog between hypothesis runs.  | 
                    |
| 1365 | 
                        + caplog.clear()  | 
                    |
| 2734 | 1366 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2735 | 1367 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2736 | 1368 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2741,56 +1373,30 @@ class TestCLI:  | 
                  
| 2741 | 1373 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 2742 | 1374 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2743 | 1375 | 
                        runner=runner,  | 
                    
| 2744 | 
                        - vault_config=config,  | 
                    |
| 2745 | 
                        - )  | 
                    |
| 2746 | 
                        - )  | 
                    |
| 2747 | 
                        - monkeypatch.setattr(  | 
                    |
| 2748 | 
                        - ssh_agent.SSHAgentClient,  | 
                    |
| 2749 | 
                        - "list_keys",  | 
                    |
| 2750 | 
                        - callables.list_keys,  | 
                    |
| 1376 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 2751 | 1377 | 
                        )  | 
                    
| 2752 | 
                        - monkeypatch.setattr(  | 
                    |
| 2753 | 
                        - ssh_agent.SSHAgentClient, "sign", callables.sign  | 
                    |
| 2754 | 1378 | 
                        )  | 
                    
| 2755 | 1379 | 
                        result = runner.invoke(  | 
                    
| 2756 | 1380 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2757 | 
                        - command_line,  | 
                    |
| 2758 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 1381 | 
                        + ["--import", "-"],  | 
                    |
| 1382 | 
                        + input=json.dumps(config),  | 
                    |
| 2759 | 1383 | 
                        catch_exceptions=False,  | 
                    
| 2760 | 1384 | 
                        )  | 
                    
| 2761 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 2762 | 
                        - assert not result.stdout.strip(), "expected no program output"  | 
                    |
| 2763 | 
                        - assert result.stderr, "expected known error output"  | 
                    |
| 2764 | 
                        - err_lines = result.stderr.splitlines(False)  | 
                    |
| 2765 | 
                        -        assert err_lines[0].startswith("Passphrase:")
                       | 
                    |
| 2766 | 
                        - assert machinery.warning_emitted(  | 
                    |
| 2767 | 
                        - "Setting a service passphrase is ineffective ",  | 
                    |
| 2768 | 
                        - caplog.record_tuples,  | 
                    |
| 2769 | 
                        - ) or machinery.warning_emitted(  | 
                    |
| 2770 | 
                        - "Setting a global passphrase is ineffective ",  | 
                    |
| 2771 | 
                        - caplog.record_tuples,  | 
                    |
| 2772 | 
                        - ), "expected known warning message"  | 
                    |
| 2773 | 
                        - assert all(map(is_warning_line, result.stderr.splitlines(True)))  | 
                    |
| 2774 | 
                        - assert all(  | 
                    |
| 1385 | 
                        + config_txt = cli_helpers.config_filename(  | 
                    |
| 1386 | 
                        + subsystem="vault"  | 
                    |
| 1387 | 
                        + ).read_text(encoding="UTF-8")  | 
                    |
| 1388 | 
                        + config3 = json.loads(config_txt)  | 
                    |
| 1389 | 
                        + assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 1390 | 
                        + assert config3 == config2, "config not imported correctly"  | 
                    |
| 1391 | 
                        + assert not result.stderr or all(  | 
                    |
| 2775 | 1392 | 
                        map(is_harmless_config_import_warning, caplog.record_tuples)  | 
                    
| 2776 | 1393 | 
                        ), "unexpected error output"  | 
                    
| 1394 | 
                        + assert_vault_config_is_indented_and_line_broken(config_txt)  | 
                    |
| 2777 | 1395 | 
                         | 
                    
| 2778 | 
                        - @hypothesis.given(  | 
                    |
| 2779 | 
                        - notes=strategies.text(  | 
                    |
| 2780 | 
                        - strategies.characters(  | 
                    |
| 2781 | 
                        - min_codepoint=32,  | 
                    |
| 2782 | 
                        - max_codepoint=126,  | 
                    |
| 2783 | 
                        - include_characters="\n",  | 
                    |
| 2784 | 
                        - ),  | 
                    |
| 2785 | 
                        - max_size=256,  | 
                    |
| 2786 | 
                        - ),  | 
                    |
| 2787 | 
                        - )  | 
                    |
| 2788 | 
                        - def test_207_service_with_notes_actually_prints_notes(  | 
                    |
| 1396 | 
                        + def test_213b_import_bad_config_not_vault_config(  | 
                    |
| 2789 | 1397 | 
                        self,  | 
                    
| 2790 | 
                        - notes: str,  | 
                    |
| 2791 | 1398 | 
                        ) -> None:  | 
                    
| 2792 | 
                        - """Service notes are printed, if they exist."""  | 
                    |
| 2793 | 
                        -        hypothesis.assume("Error:" not in notes)
                       | 
                    |
| 1399 | 
                        + """Importing an invalid config fails."""  | 
                    |
| 2794 | 1400 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2795 | 1401 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2796 | 1402 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2798,45 +1404,25 @@ class TestCLI:  | 
                  
| 2798 | 1404 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2799 | 1405 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2800 | 1406 | 
                        stack.enter_context(  | 
                    
| 2801 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 1407 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 2802 | 1408 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2803 | 1409 | 
                        runner=runner,  | 
                    
| 2804 | 
                        -                    vault_config={
                       | 
                    |
| 2805 | 
                        -                        "global": {
                       | 
                    |
| 2806 | 
                        - "phrase": DUMMY_PASSPHRASE,  | 
                    |
| 2807 | 
                        - },  | 
                    |
| 2808 | 
                        -                        "services": {
                       | 
                    |
| 2809 | 
                        -                            DUMMY_SERVICE: {
                       | 
                    |
| 2810 | 
                        - "notes": notes,  | 
                    |
| 2811 | 
                        - **DUMMY_CONFIG_SETTINGS,  | 
                    |
| 2812 | 
                        - },  | 
                    |
| 2813 | 
                        - },  | 
                    |
| 2814 | 
                        - },  | 
                    |
| 2815 | 1410 | 
                        )  | 
                    
| 2816 | 1411 | 
                        )  | 
                    
| 2817 | 1412 | 
                        result = runner.invoke(  | 
                    
| 2818 | 1413 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2819 | 
                        - ["--", DUMMY_SERVICE],  | 
                    |
| 2820 | 
                        - )  | 
                    |
| 2821 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 2822 | 
                        - assert result.stdout, "expected program output"  | 
                    |
| 2823 | 
                        - assert result.stdout.strip() == DUMMY_RESULT_PASSPHRASE.decode(  | 
                    |
| 2824 | 
                        - "ascii"  | 
                    |
| 2825 | 
                        - ), "expected known program output"  | 
                    |
| 2826 | 
                        - assert result.stderr or not notes.strip(), "expected stderr"  | 
                    |
| 2827 | 
                        - assert "Error:" not in result.stderr, (  | 
                    |
| 2828 | 
                        - "expected no error messages on stderr"  | 
                    |
| 1414 | 
                        + ["--import", "-"],  | 
                    |
| 1415 | 
                        + input="null",  | 
                    |
| 1416 | 
                        + catch_exceptions=False,  | 
                    |
| 2829 | 1417 | 
                        )  | 
                    
| 2830 | 
                        - assert result.stderr.strip() == notes.strip(), (  | 
                    |
| 2831 | 
                        - "expected known stderr contents"  | 
                    |
| 1418 | 
                        + assert result.error_exit(error="Invalid vault config"), (  | 
                    |
| 1419 | 
                        + "expected error exit and known error message"  | 
                    |
| 2832 | 1420 | 
                        )  | 
                    
| 2833 | 1421 | 
                         | 
                    
| 2834 | 
                        - @Parametrize.VAULT_CHARSET_OPTION  | 
                    |
| 2835 | 
                        - def test_210_invalid_argument_range(  | 
                    |
| 1422 | 
                        + def test_213c_import_bad_config_not_json_data(  | 
                    |
| 2836 | 1423 | 
                        self,  | 
                    
| 2837 | 
                        - option: str,  | 
                    |
| 2838 | 1424 | 
                        ) -> None:  | 
                    
| 2839 | 
                        - """Requesting invalidly many characters from a class fails."""  | 
                    |
| 1425 | 
                        + """Importing an invalid config fails."""  | 
                    |
| 2840 | 1426 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2841 | 1427 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2842 | 1428 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2849,27 +1435,26 @@ class TestCLI:  | 
                  
| 2849 | 1435 | 
                        runner=runner,  | 
                    
| 2850 | 1436 | 
                        )  | 
                    
| 2851 | 1437 | 
                        )  | 
                    
| 2852 | 
                        - for value in "-42", "invalid":  | 
                    |
| 2853 | 1438 | 
                        result = runner.invoke(  | 
                    
| 2854 | 1439 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2855 | 
                        - [option, value, "-p", "--", DUMMY_SERVICE],  | 
                    |
| 2856 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 1440 | 
                        + ["--import", "-"],  | 
                    |
| 1441 | 
                        + input="This string is not valid JSON.",  | 
                    |
| 2857 | 1442 | 
                        catch_exceptions=False,  | 
                    
| 2858 | 1443 | 
                        )  | 
                    
| 2859 | 
                        - assert result.error_exit(error="Invalid value"), (  | 
                    |
| 1444 | 
                        + assert result.error_exit(error="cannot decode JSON"), (  | 
                    |
| 2860 | 1445 | 
                        "expected error exit and known error message"  | 
                    
| 2861 | 1446 | 
                        )  | 
                    
| 2862 | 1447 | 
                         | 
                    
| 2863 | 
                        - @Parametrize.OPTION_COMBINATIONS_SERVICE_NEEDED  | 
                    |
| 2864 | 
                        - def test_211_service_needed(  | 
                    |
| 1448 | 
                        + def test_213d_import_bad_config_not_a_file(  | 
                    |
| 2865 | 1449 | 
                        self,  | 
                    
| 2866 | 
                        - options: list[str],  | 
                    |
| 2867 | 
                        - service: bool | None,  | 
                    |
| 2868 | 
                        - input: str | None,  | 
                    |
| 2869 | 
                        - check_success: bool,  | 
                    |
| 2870 | 1450 | 
                        ) -> None:  | 
                    
| 2871 | 
                        - """We require or forbid a service argument, depending on options."""  | 
                    |
| 1451 | 
                        + """Importing an invalid config fails."""  | 
                    |
| 2872 | 1452 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 1453 | 
                        + # `isolated_vault_config` ensures the configuration is valid  | 
                    |
| 1454 | 
                        + # JSON. So, to pass an actual broken configuration, we must  | 
                    |
| 1455 | 
                        + # open the configuration file ourselves afterwards, inside the  | 
                    |
| 1456 | 
                        + # context.  | 
                    |
| 1457 | 
                        + #  | 
                    |
| 2873 | 1458 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2874 | 1459 | 
                        # with-statements.  | 
                    
| 2875 | 1460 | 
                        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    
| ... | ... | 
                      @@ -2879,34 +1464,33 @@ class TestCLI:  | 
                  
| 2879 | 1464 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 2880 | 1465 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2881 | 1466 | 
                        runner=runner,  | 
                    
| 2882 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 1467 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 2883 | 1468 | 
                        )  | 
                    
| 2884 | 1469 | 
                        )  | 
                    
| 2885 | 
                        - monkeypatch.setattr(  | 
                    |
| 2886 | 
                        - cli_helpers,  | 
                    |
| 2887 | 
                        - "prompt_for_passphrase",  | 
                    |
| 2888 | 
                        - callables.auto_prompt,  | 
                    |
| 1470 | 
                        + cli_helpers.config_filename(subsystem="vault").write_text(  | 
                    |
| 1471 | 
                        + "This string is not valid JSON.\n", encoding="UTF-8"  | 
                    |
| 2889 | 1472 | 
                        )  | 
                    
| 1473 | 
                        + dname = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 2890 | 1474 | 
                        result = runner.invoke(  | 
                    
| 2891 | 1475 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2892 | 
                        - options if service else [*options, "--", DUMMY_SERVICE],  | 
                    |
| 2893 | 
                        - input=input,  | 
                    |
| 1476 | 
                        + ["--import", os.fsdecode(dname)],  | 
                    |
| 2894 | 1477 | 
                        catch_exceptions=False,  | 
                    
| 2895 | 1478 | 
                        )  | 
                    
| 2896 | 
                        - if service is not None:  | 
                    |
| 2897 | 
                        - err_msg = (  | 
                    |
| 2898 | 
                        - " requires a SERVICE"  | 
                    |
| 2899 | 
                        - if service  | 
                    |
| 2900 | 
                        - else " does not take a SERVICE argument"  | 
                    |
| 2901 | 
                        - )  | 
                    |
| 2902 | 
                        - assert result.error_exit(error=err_msg), (  | 
                    |
| 1479 | 
                        + # The Annoying OS uses EACCES, other OSes use EISDIR.  | 
                    |
| 1480 | 
                        + assert result.error_exit(  | 
                    |
| 1481 | 
                        + error=os.strerror(errno.EISDIR)  | 
                    |
| 1482 | 
                        + ) or result.error_exit(error=os.strerror(errno.EACCES)), (  | 
                    |
| 2903 | 1483 | 
                        "expected error exit and known error message"  | 
                    
| 2904 | 1484 | 
                        )  | 
                    
| 2905 | 
                        - else:  | 
                    |
| 2906 | 
                        - assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 2907 | 
                        - "expected clean exit"  | 
                    |
| 2908 | 
                        - )  | 
                    |
| 2909 | 
                        - if check_success:  | 
                    |
| 1485 | 
                        +  | 
                    |
| 1486 | 
                        + @Parametrize.VALID_TEST_CONFIGS  | 
                    |
| 1487 | 
                        + def test_214_export_config_success(  | 
                    |
| 1488 | 
                        + self,  | 
                    |
| 1489 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 1490 | 
                        + config: Any,  | 
                    |
| 1491 | 
                        + ) -> None:  | 
                    |
| 1492 | 
                        + """Exporting a configuration works."""  | 
                    |
| 1493 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 2910 | 1494 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2911 | 1495 | 
                        # with-statements.  | 
                    
| 2912 | 1496 | 
                        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    
| ... | ... | 
                      @@ -2916,42 +1500,36 @@ class TestCLI:  | 
                  
| 2916 | 1500 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 2917 | 1501 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2918 | 1502 | 
                        runner=runner,  | 
                    
| 2919 | 
                        -                        vault_config={
                       | 
                    |
| 2920 | 
                        -                            "global": {"phrase": "abc"},
                       | 
                    |
| 2921 | 
                        -                            "services": {},
                       | 
                    |
| 2922 | 
                        - },  | 
                    |
| 1503 | 
                        + vault_config=config,  | 
                    |
| 2923 | 1504 | 
                        )  | 
                    
| 2924 | 1505 | 
                        )  | 
                    
| 2925 | 
                        - monkeypatch.setattr(  | 
                    |
| 2926 | 
                        - cli_helpers,  | 
                    |
| 2927 | 
                        - "prompt_for_passphrase",  | 
                    |
| 2928 | 
                        - callables.auto_prompt,  | 
                    |
| 2929 | 
                        - )  | 
                    |
| 1506 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 1507 | 
                        + "w", encoding="UTF-8"  | 
                    |
| 1508 | 
                        + ) as outfile:  | 
                    |
| 1509 | 
                        + # Ensure the config is written on one line.  | 
                    |
| 1510 | 
                        + json.dump(config, outfile, indent=None)  | 
                    |
| 2930 | 1511 | 
                        result = runner.invoke(  | 
                    
| 2931 | 1512 | 
                        cli.derivepassphrase_vault,  | 
                    
| 2932 | 
                        - [*options, "--", DUMMY_SERVICE] if service else options,  | 
                    |
| 2933 | 
                        - input=input,  | 
                    |
| 1513 | 
                        + ["--export", "-"],  | 
                    |
| 2934 | 1514 | 
                        catch_exceptions=False,  | 
                    
| 2935 | 1515 | 
                        )  | 
                    
| 2936 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 1516 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 1517 | 
                        + encoding="UTF-8"  | 
                    |
| 1518 | 
                        + ) as infile:  | 
                    |
| 1519 | 
                        + config2 = json.load(infile)  | 
                    |
| 1520 | 
                        + assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 1521 | 
                        + assert config2 == config, "config not imported correctly"  | 
                    |
| 1522 | 
                        + assert not result.stderr or all( # pragma: no branch  | 
                    |
| 1523 | 
                        + map(is_harmless_config_import_warning, caplog.record_tuples)  | 
                    |
| 1524 | 
                        + ), "unexpected error output"  | 
                    |
| 1525 | 
                        + assert_vault_config_is_indented_and_line_broken(result.stdout)  | 
                    |
| 2937 | 1526 | 
                         | 
                    
| 2938 | 
                        - def test_211a_empty_service_name_causes_warning(  | 
                    |
| 1527 | 
                        + @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 1528 | 
                        + def test_214a_export_settings_no_stored_settings(  | 
                    |
| 2939 | 1529 | 
                        self,  | 
                    
| 2940 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 1530 | 
                        + export_options: list[str],  | 
                    |
| 2941 | 1531 | 
                        ) -> None:  | 
                    
| 2942 | 
                        - """Using an empty service name (where permissible) warns.  | 
                    |
| 2943 | 
                        -  | 
                    |
| 2944 | 
                        - Only the `--config` option can optionally take a service name.  | 
                    |
| 2945 | 
                        -  | 
                    |
| 2946 | 
                        - """  | 
                    |
| 2947 | 
                        -  | 
                    |
| 2948 | 
                        - def is_expected_warning(record: tuple[str, int, str]) -> bool:  | 
                    |
| 2949 | 
                        - return is_harmless_config_import_warning(  | 
                    |
| 2950 | 
                        - record  | 
                    |
| 2951 | 
                        - ) or machinery.warning_emitted(  | 
                    |
| 2952 | 
                        - "An empty SERVICE is not supported by vault(1)", [record]  | 
                    |
| 2953 | 
                        - )  | 
                    |
| 2954 | 
                        -  | 
                    |
| 1532 | 
                        + """Exporting the default, empty config works."""  | 
                    |
| 2955 | 1533 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 2956 | 1534 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 2957 | 1535 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -2959,55 +1537,31 @@ class TestCLI:  | 
                  
| 2959 | 1537 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 2960 | 1538 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 2961 | 1539 | 
                        stack.enter_context(  | 
                    
| 2962 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 1540 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 2963 | 1541 | 
                        monkeypatch=monkeypatch,  | 
                    
| 2964 | 1542 | 
                        runner=runner,  | 
                    
| 2965 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 2966 | 
                        - )  | 
                    |
| 2967 | 
                        - )  | 
                    |
| 2968 | 
                        - monkeypatch.setattr(  | 
                    |
| 2969 | 
                        - cli_helpers,  | 
                    |
| 2970 | 
                        - "prompt_for_passphrase",  | 
                    |
| 2971 | 
                        - callables.auto_prompt,  | 
                    |
| 2972 | 1543 | 
                        )  | 
                    
| 2973 | 
                        - result = runner.invoke(  | 
                    |
| 2974 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 2975 | 
                        - ["--config", "--length=30", "--", ""],  | 
                    |
| 2976 | 
                        - catch_exceptions=False,  | 
                    |
| 2977 | 1544 | 
                        )  | 
                    
| 2978 | 
                        - assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 2979 | 
                        - assert result.stderr is not None, "expected known error output"  | 
                    |
| 2980 | 
                        - assert all(map(is_expected_warning, caplog.record_tuples)), (  | 
                    |
| 2981 | 
                        - "expected known error output"  | 
                    |
| 1545 | 
                        + cli_helpers.config_filename(subsystem="vault").unlink(  | 
                    |
| 1546 | 
                        + missing_ok=True  | 
                    |
| 2982 | 1547 | 
                        )  | 
                    
| 2983 | 
                        -            assert cli_helpers.load_config() == {
                       | 
                    |
| 2984 | 
                        -                "global": {"length": 30},
                       | 
                    |
| 2985 | 
                        -                "services": {},
                       | 
                    |
| 2986 | 
                        - }, "requested configuration change was not applied"  | 
                    |
| 2987 | 
                        - caplog.clear()  | 
                    |
| 2988 | 1548 | 
                        result = runner.invoke(  | 
                    
| 2989 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 2990 | 
                        - ["--import", "-"],  | 
                    |
| 2991 | 
                        -                input=json.dumps({"services": {"": {"length": 40}}}),
                       | 
                    |
| 1549 | 
                        + # Test parent context navigation by not calling  | 
                    |
| 1550 | 
                        + # `cli.derivepassphrase_vault` directly. Used e.g. in  | 
                    |
| 1551 | 
                        + # the `--export-as=sh` section to autoconstruct the  | 
                    |
| 1552 | 
                        + # program name correctly.  | 
                    |
| 1553 | 
                        + cli.derivepassphrase,  | 
                    |
| 1554 | 
                        + ["vault", "--export", "-", *export_options],  | 
                    |
| 2992 | 1555 | 
                        catch_exceptions=False,  | 
                    
| 2993 | 1556 | 
                        )  | 
                    
| 2994 | 
                        - assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 2995 | 
                        - assert result.stderr is not None, "expected known error output"  | 
                    |
| 2996 | 
                        - assert all(map(is_expected_warning, caplog.record_tuples)), (  | 
                    |
| 2997 | 
                        - "expected known error output"  | 
                    |
| 2998 | 
                        - )  | 
                    |
| 2999 | 
                        -            assert cli_helpers.load_config() == {
                       | 
                    |
| 3000 | 
                        -                "global": {"length": 30},
                       | 
                    |
| 3001 | 
                        -                "services": {"": {"length": 40}},
                       | 
                    |
| 3002 | 
                        - }, "requested configuration change was not applied"  | 
                    |
| 1557 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 3003 | 1558 | 
                         | 
                    
| 3004 | 
                        - @Parametrize.OPTION_COMBINATIONS_INCOMPATIBLE  | 
                    |
| 3005 | 
                        - def test_212_incompatible_options(  | 
                    |
| 1559 | 
                        + @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 1560 | 
                        + def test_214b_export_settings_bad_stored_config(  | 
                    |
| 3006 | 1561 | 
                        self,  | 
                    
| 3007 | 
                        - options: list[str],  | 
                    |
| 3008 | 
                        - service: bool | None,  | 
                    |
| 1562 | 
                        + export_options: list[str],  | 
                    |
| 3009 | 1563 | 
                        ) -> None:  | 
                    
| 3010 | 
                        - """Incompatible options are detected."""  | 
                    |
| 1564 | 
                        + """Exporting an invalid config fails."""  | 
                    |
| 3011 | 1565 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 3012 | 1566 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 3013 | 1567 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -3015,28 +1569,28 @@ class TestCLI:  | 
                  
| 3015 | 1569 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 3016 | 1570 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 3017 | 1571 | 
                        stack.enter_context(  | 
                    
| 3018 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1572 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 3019 | 1573 | 
                        monkeypatch=monkeypatch,  | 
                    
| 3020 | 1574 | 
                        runner=runner,  | 
                    
| 1575 | 
                        +                    vault_config={},
                       | 
                    |
| 3021 | 1576 | 
                        )  | 
                    
| 3022 | 1577 | 
                        )  | 
                    
| 3023 | 1578 | 
                        result = runner.invoke(  | 
                    
| 3024 | 1579 | 
                        cli.derivepassphrase_vault,  | 
                    
| 3025 | 
                        - [*options, "--", DUMMY_SERVICE] if service else options,  | 
                    |
| 3026 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 1580 | 
                        + ["--export", "-", *export_options],  | 
                    |
| 1581 | 
                        + input="null",  | 
                    |
| 3027 | 1582 | 
                        catch_exceptions=False,  | 
                    
| 3028 | 1583 | 
                        )  | 
                    
| 3029 | 
                        - assert result.error_exit(error="mutually exclusive with "), (  | 
                    |
| 1584 | 
                        + assert result.error_exit(error="Cannot load vault settings:"), (  | 
                    |
| 3030 | 1585 | 
                        "expected error exit and known error message"  | 
                    
| 3031 | 1586 | 
                        )  | 
                    
| 3032 | 1587 | 
                         | 
                    
| 3033 | 
                        - @Parametrize.VALID_TEST_CONFIGS  | 
                    |
| 3034 | 
                        - def test_213_import_config_success(  | 
                    |
| 1588 | 
                        + @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 1589 | 
                        + def test_214c_export_settings_not_a_file(  | 
                    |
| 3035 | 1590 | 
                        self,  | 
                    
| 3036 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 3037 | 
                        - config: Any,  | 
                    |
| 1591 | 
                        + export_options: list[str],  | 
                    |
| 3038 | 1592 | 
                        ) -> None:  | 
                    
| 3039 | 
                        - """Importing a configuration works."""  | 
                    |
| 1593 | 
                        + """Exporting an invalid config fails."""  | 
                    |
| 3040 | 1594 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 3041 | 1595 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 3042 | 1596 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -3044,57 +1598,30 @@ class TestCLI:  | 
                  
| 3044 | 1598 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 3045 | 1599 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 3046 | 1600 | 
                        stack.enter_context(  | 
                    
| 3047 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 1601 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 3048 | 1602 | 
                        monkeypatch=monkeypatch,  | 
                    
| 3049 | 1603 | 
                        runner=runner,  | 
                    
| 3050 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 3051 | 1604 | 
                        )  | 
                    
| 3052 | 1605 | 
                        )  | 
                    
| 1606 | 
                        + config_file = cli_helpers.config_filename(subsystem="vault")  | 
                    |
| 1607 | 
                        + config_file.unlink(missing_ok=True)  | 
                    |
| 1608 | 
                        + config_file.mkdir(parents=True, exist_ok=True)  | 
                    |
| 3053 | 1609 | 
                        result = runner.invoke(  | 
                    
| 3054 | 1610 | 
                        cli.derivepassphrase_vault,  | 
                    
| 3055 | 
                        - ["--import", "-"],  | 
                    |
| 3056 | 
                        - input=json.dumps(config),  | 
                    |
| 1611 | 
                        + ["--export", "-", *export_options],  | 
                    |
| 1612 | 
                        + input="null",  | 
                    |
| 3057 | 1613 | 
                        catch_exceptions=False,  | 
                    
| 3058 | 1614 | 
                        )  | 
                    
| 3059 | 
                        - config_txt = cli_helpers.config_filename(  | 
                    |
| 3060 | 
                        - subsystem="vault"  | 
                    |
| 3061 | 
                        - ).read_text(encoding="UTF-8")  | 
                    |
| 3062 | 
                        - config2 = json.loads(config_txt)  | 
                    |
| 3063 | 
                        - assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 3064 | 
                        - assert config2 == config, "config not imported correctly"  | 
                    |
| 3065 | 
                        - assert not result.stderr or all( # pragma: no branch  | 
                    |
| 3066 | 
                        - map(is_harmless_config_import_warning, caplog.record_tuples)  | 
                    |
| 3067 | 
                        - ), "unexpected error output"  | 
                    |
| 3068 | 
                        - assert_vault_config_is_indented_and_line_broken(config_txt)  | 
                    |
| 3069 | 
                        -  | 
                    |
| 3070 | 
                        - @hypothesis.settings(  | 
                    |
| 3071 | 
                        - suppress_health_check=[  | 
                    |
| 3072 | 
                        - *hypothesis.settings().suppress_health_check,  | 
                    |
| 3073 | 
                        - hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 3074 | 
                        - ],  | 
                    |
| 3075 | 
                        - )  | 
                    |
| 3076 | 
                        - @hypothesis.given(  | 
                    |
| 3077 | 
                        - conf=hypothesis_machinery.smudged_vault_test_config(  | 
                    |
| 3078 | 
                        - strategies.sampled_from([  | 
                    |
| 3079 | 
                        - conf for conf in data.TEST_CONFIGS if conf.is_valid()  | 
                    |
| 3080 | 
                        - ])  | 
                    |
| 3081 | 
                        - )  | 
                    |
| 1615 | 
                        + assert result.error_exit(error="Cannot load vault settings:"), (  | 
                    |
| 1616 | 
                        + "expected error exit and known error message"  | 
                    |
| 3082 | 1617 | 
                        )  | 
                    
| 3083 | 
                        - def test_213a_import_config_success(  | 
                    |
| 1618 | 
                        +  | 
                    |
| 1619 | 
                        + @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 1620 | 
                        + def test_214d_export_settings_target_not_a_file(  | 
                    |
| 3084 | 1621 | 
                        self,  | 
                    
| 3085 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 3086 | 
                        - conf: data.VaultTestConfig,  | 
                    |
| 1622 | 
                        + export_options: list[str],  | 
                    |
| 3087 | 1623 | 
                        ) -> None:  | 
                    
| 3088 | 
                        - """Importing a smudged configuration works.  | 
                    |
| 3089 | 
                        -  | 
                    |
| 3090 | 
                        - Tested via hypothesis.  | 
                    |
| 3091 | 
                        -  | 
                    |
| 3092 | 
                        - """  | 
                    |
| 3093 | 
                        - config = conf.config  | 
                    |
| 3094 | 
                        - config2 = copy.deepcopy(config)  | 
                    |
| 3095 | 
                        - _types.clean_up_falsy_vault_config_values(config2)  | 
                    |
| 3096 | 
                        - # Reset caplog between hypothesis runs.  | 
                    |
| 3097 | 
                        - caplog.clear()  | 
                    |
| 1624 | 
                        + """Exporting an invalid config fails."""  | 
                    |
| 3098 | 1625 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 3099 | 1626 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 3100 | 1627 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -3102,33 +1629,29 @@ class TestCLI:  | 
                  
| 3102 | 1629 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 3103 | 1630 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 3104 | 1631 | 
                        stack.enter_context(  | 
                    
| 3105 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 1632 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 3106 | 1633 | 
                        monkeypatch=monkeypatch,  | 
                    
| 3107 | 1634 | 
                        runner=runner,  | 
                    
| 3108 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 3109 | 1635 | 
                        )  | 
                    
| 3110 | 1636 | 
                        )  | 
                    
| 1637 | 
                        + dname = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 3111 | 1638 | 
                        result = runner.invoke(  | 
                    
| 3112 | 1639 | 
                        cli.derivepassphrase_vault,  | 
                    
| 3113 | 
                        - ["--import", "-"],  | 
                    |
| 3114 | 
                        - input=json.dumps(config),  | 
                    |
| 1640 | 
                        + ["--export", os.fsdecode(dname), *export_options],  | 
                    |
| 1641 | 
                        + input="null",  | 
                    |
| 3115 | 1642 | 
                        catch_exceptions=False,  | 
                    
| 3116 | 1643 | 
                        )  | 
                    
| 3117 | 
                        - config_txt = cli_helpers.config_filename(  | 
                    |
| 3118 | 
                        - subsystem="vault"  | 
                    |
| 3119 | 
                        - ).read_text(encoding="UTF-8")  | 
                    |
| 3120 | 
                        - config3 = json.loads(config_txt)  | 
                    |
| 3121 | 
                        - assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 3122 | 
                        - assert config3 == config2, "config not imported correctly"  | 
                    |
| 3123 | 
                        - assert not result.stderr or all(  | 
                    |
| 3124 | 
                        - map(is_harmless_config_import_warning, caplog.record_tuples)  | 
                    |
| 3125 | 
                        - ), "unexpected error output"  | 
                    |
| 3126 | 
                        - assert_vault_config_is_indented_and_line_broken(config_txt)  | 
                    |
| 1644 | 
                        + assert result.error_exit(error="Cannot export vault settings:"), (  | 
                    |
| 1645 | 
                        + "expected error exit and known error message"  | 
                    |
| 1646 | 
                        + )  | 
                    |
| 3127 | 1647 | 
                         | 
                    
| 3128 | 
                        - def test_213b_import_bad_config_not_vault_config(  | 
                    |
| 1648 | 
                        + @pytest_machinery.skip_if_on_the_annoying_os  | 
                    |
| 1649 | 
                        + @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 1650 | 
                        + def test_214e_export_settings_settings_directory_not_a_directory(  | 
                    |
| 3129 | 1651 | 
                        self,  | 
                    
| 1652 | 
                        + export_options: list[str],  | 
                    |
| 3130 | 1653 | 
                        ) -> None:  | 
                    
| 3131 | 
                        - """Importing an invalid config fails."""  | 
                    |
| 1654 | 
                        + """Exporting an invalid config fails."""  | 
                    |
| 3132 | 1655 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 3133 | 1656 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 3134 | 1657 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -3141,1240 +1664,105 @@ class TestCLI:  | 
                  
| 3141 | 1664 | 
                        runner=runner,  | 
                    
| 3142 | 1665 | 
                        )  | 
                    
| 3143 | 1666 | 
                        )  | 
                    
| 1667 | 
                        + config_dir = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 1668 | 
                        + with contextlib.suppress(FileNotFoundError):  | 
                    |
| 1669 | 
                        + shutil.rmtree(config_dir)  | 
                    |
| 1670 | 
                        +            config_dir.write_text("Obstruction!!\n")
                       | 
                    |
| 3144 | 1671 | 
                        result = runner.invoke(  | 
                    
| 3145 | 1672 | 
                        cli.derivepassphrase_vault,  | 
                    
| 3146 | 
                        - ["--import", "-"],  | 
                    |
| 1673 | 
                        + ["--export", "-", *export_options],  | 
                    |
| 3147 | 1674 | 
                        input="null",  | 
                    
| 3148 | 1675 | 
                        catch_exceptions=False,  | 
                    
| 3149 | 1676 | 
                        )  | 
                    
| 3150 | 
                        - assert result.error_exit(error="Invalid vault config"), (  | 
                    |
| 1677 | 
                        + assert result.error_exit(  | 
                    |
| 1678 | 
                        + error="Cannot load vault settings:"  | 
                    |
| 1679 | 
                        + ) or result.error_exit(error="Cannot load user config:"), (  | 
                    |
| 3151 | 1680 | 
                        "expected error exit and known error message"  | 
                    
| 3152 | 1681 | 
                        )  | 
                    
| 3153 | 1682 | 
                         | 
                    
| 3154 | 
                        - def test_213c_import_bad_config_not_json_data(  | 
                    |
| 1683 | 
                        + @Parametrize.NOTES_PLACEMENT  | 
                    |
| 1684 | 
                        + @hypothesis.given(  | 
                    |
| 1685 | 
                        + notes=strategies.text(  | 
                    |
| 1686 | 
                        + strategies.characters(  | 
                    |
| 1687 | 
                        + min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 1688 | 
                        + ),  | 
                    |
| 1689 | 
                        + min_size=1,  | 
                    |
| 1690 | 
                        + max_size=512,  | 
                    |
| 1691 | 
                        + ).filter(str.strip),  | 
                    |
| 1692 | 
                        + )  | 
                    |
| 1693 | 
                        + def test_215_notes_placement(  | 
                    |
| 3155 | 1694 | 
                        self,  | 
                    
| 1695 | 
                        + notes_placement: Literal["before", "after"],  | 
                    |
| 1696 | 
                        + placement_args: list[str],  | 
                    |
| 1697 | 
                        + notes: str,  | 
                    |
| 3156 | 1698 | 
                        ) -> None:  | 
                    
| 3157 | 
                        - """Importing an invalid config fails."""  | 
                    |
| 3158 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 1699 | 
                        + notes = notes.strip()  | 
                    |
| 1700 | 
                        +        maybe_notes = {"notes": notes} if notes else {}
                       | 
                    |
| 1701 | 
                        +        vault_config = {
                       | 
                    |
| 1702 | 
                        +            "global": {"phrase": DUMMY_PASSPHRASE},
                       | 
                    |
| 1703 | 
                        +            "services": {
                       | 
                    |
| 1704 | 
                        +                DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 1705 | 
                        + },  | 
                    |
| 1706 | 
                        + }  | 
                    |
| 1707 | 
                        +        result_phrase = DUMMY_RESULT_PASSPHRASE.decode("ascii")
                       | 
                    |
| 1708 | 
                        + expected = (  | 
                    |
| 1709 | 
                        +            f"{notes}\n\n{result_phrase}\n"
                       | 
                    |
| 1710 | 
                        + if notes_placement == "before"  | 
                    |
| 1711 | 
                        +            else f"{result_phrase}\n\n{notes}\n\n"
                       | 
                    |
| 1712 | 
                        + )  | 
                    |
| 1713 | 
                        + runner = machinery.CliRunner(mix_stderr=True)  | 
                    |
| 3159 | 1714 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 3160 | 1715 | 
                        # with-statements.  | 
                    
| 3161 | 1716 | 
                        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    
| 3162 | 1717 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 3163 | 1718 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 3164 | 1719 | 
                        stack.enter_context(  | 
                    
| 3165 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1720 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 3166 | 1721 | 
                        monkeypatch=monkeypatch,  | 
                    
| 3167 | 1722 | 
                        runner=runner,  | 
                    
| 1723 | 
                        + vault_config=vault_config,  | 
                    |
| 3168 | 1724 | 
                        )  | 
                    
| 3169 | 1725 | 
                        )  | 
                    
| 3170 | 1726 | 
                        result = runner.invoke(  | 
                    
| 3171 | 1727 | 
                        cli.derivepassphrase_vault,  | 
                    
| 3172 | 
                        - ["--import", "-"],  | 
                    |
| 3173 | 
                        - input="This string is not valid JSON.",  | 
                    |
| 1728 | 
                        + [*placement_args, "--", DUMMY_SERVICE],  | 
                    |
| 3174 | 1729 | 
                        catch_exceptions=False,  | 
                    
| 3175 | 1730 | 
                        )  | 
                    
| 3176 | 
                        - assert result.error_exit(error="cannot decode JSON"), (  | 
                    |
| 3177 | 
                        - "expected error exit and known error message"  | 
                    |
| 3178 | 
                        - )  | 
                    |
| 1731 | 
                        + assert result.clean_exit(output=expected), "expected clean exit"  | 
                    |
| 3179 | 1732 | 
                         | 
                    
| 3180 | 
                        - def test_213d_import_bad_config_not_a_file(  | 
                    |
| 3181 | 
                        - self,  | 
                    |
| 3182 | 
                        - ) -> None:  | 
                    |
| 3183 | 
                        - """Importing an invalid config fails."""  | 
                    |
| 3184 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3185 | 
                        - # `isolated_vault_config` ensures the configuration is valid  | 
                    |
| 3186 | 
                        - # JSON. So, to pass an actual broken configuration, we must  | 
                    |
| 3187 | 
                        - # open the configuration file ourselves afterwards, inside the  | 
                    |
| 3188 | 
                        - # context.  | 
                    |
| 3189 | 
                        - #  | 
                    |
| 3190 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3191 | 
                        - # with-statements.  | 
                    |
| 3192 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3193 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3194 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3195 | 
                        - stack.enter_context(  | 
                    |
| 3196 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3197 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3198 | 
                        - runner=runner,  | 
                    |
| 3199 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 3200 | 
                        - )  | 
                    |
| 3201 | 
                        - )  | 
                    |
| 3202 | 
                        - cli_helpers.config_filename(subsystem="vault").write_text(  | 
                    |
| 3203 | 
                        - "This string is not valid JSON.\n", encoding="UTF-8"  | 
                    |
| 3204 | 
                        - )  | 
                    |
| 3205 | 
                        - dname = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 3206 | 
                        - result = runner.invoke(  | 
                    |
| 3207 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3208 | 
                        - ["--import", os.fsdecode(dname)],  | 
                    |
| 3209 | 
                        - catch_exceptions=False,  | 
                    |
| 3210 | 
                        - )  | 
                    |
| 3211 | 
                        - # The Annoying OS uses EACCES, other OSes use EISDIR.  | 
                    |
| 3212 | 
                        - assert result.error_exit(  | 
                    |
| 3213 | 
                        - error=os.strerror(errno.EISDIR)  | 
                    |
| 3214 | 
                        - ) or result.error_exit(error=os.strerror(errno.EACCES)), (  | 
                    |
| 3215 | 
                        - "expected error exit and known error message"  | 
                    |
| 3216 | 
                        - )  | 
                    |
| 3217 | 
                        -  | 
                    |
| 3218 | 
                        - @Parametrize.VALID_TEST_CONFIGS  | 
                    |
| 3219 | 
                        - def test_214_export_config_success(  | 
                    |
| 3220 | 
                        - self,  | 
                    |
| 3221 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 3222 | 
                        - config: Any,  | 
                    |
| 3223 | 
                        - ) -> None:  | 
                    |
| 3224 | 
                        - """Exporting a configuration works."""  | 
                    |
| 3225 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3226 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3227 | 
                        - # with-statements.  | 
                    |
| 3228 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3229 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3230 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3231 | 
                        - stack.enter_context(  | 
                    |
| 3232 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3233 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3234 | 
                        - runner=runner,  | 
                    |
| 3235 | 
                        - vault_config=config,  | 
                    |
| 3236 | 
                        - )  | 
                    |
| 3237 | 
                        - )  | 
                    |
| 3238 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3239 | 
                        - "w", encoding="UTF-8"  | 
                    |
| 3240 | 
                        - ) as outfile:  | 
                    |
| 3241 | 
                        - # Ensure the config is written on one line.  | 
                    |
| 3242 | 
                        - json.dump(config, outfile, indent=None)  | 
                    |
| 3243 | 
                        - result = runner.invoke(  | 
                    |
| 3244 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3245 | 
                        - ["--export", "-"],  | 
                    |
| 3246 | 
                        - catch_exceptions=False,  | 
                    |
| 3247 | 
                        - )  | 
                    |
| 3248 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3249 | 
                        - encoding="UTF-8"  | 
                    |
| 3250 | 
                        - ) as infile:  | 
                    |
| 3251 | 
                        - config2 = json.load(infile)  | 
                    |
| 3252 | 
                        - assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 3253 | 
                        - assert config2 == config, "config not imported correctly"  | 
                    |
| 3254 | 
                        - assert not result.stderr or all( # pragma: no branch  | 
                    |
| 3255 | 
                        - map(is_harmless_config_import_warning, caplog.record_tuples)  | 
                    |
| 3256 | 
                        - ), "unexpected error output"  | 
                    |
| 3257 | 
                        - assert_vault_config_is_indented_and_line_broken(result.stdout)  | 
                    |
| 3258 | 
                        -  | 
                    |
| 3259 | 
                        - @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 3260 | 
                        - def test_214a_export_settings_no_stored_settings(  | 
                    |
| 3261 | 
                        - self,  | 
                    |
| 3262 | 
                        - export_options: list[str],  | 
                    |
| 3263 | 
                        - ) -> None:  | 
                    |
| 3264 | 
                        - """Exporting the default, empty config works."""  | 
                    |
| 3265 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3266 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3267 | 
                        - # with-statements.  | 
                    |
| 3268 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3269 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3270 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3271 | 
                        - stack.enter_context(  | 
                    |
| 3272 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 3273 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3274 | 
                        - runner=runner,  | 
                    |
| 3275 | 
                        - )  | 
                    |
| 3276 | 
                        - )  | 
                    |
| 3277 | 
                        - cli_helpers.config_filename(subsystem="vault").unlink(  | 
                    |
| 3278 | 
                        - missing_ok=True  | 
                    |
| 3279 | 
                        - )  | 
                    |
| 3280 | 
                        - result = runner.invoke(  | 
                    |
| 3281 | 
                        - # Test parent context navigation by not calling  | 
                    |
| 3282 | 
                        - # `cli.derivepassphrase_vault` directly. Used e.g. in  | 
                    |
| 3283 | 
                        - # the `--export-as=sh` section to autoconstruct the  | 
                    |
| 3284 | 
                        - # program name correctly.  | 
                    |
| 3285 | 
                        - cli.derivepassphrase,  | 
                    |
| 3286 | 
                        - ["vault", "--export", "-", *export_options],  | 
                    |
| 3287 | 
                        - catch_exceptions=False,  | 
                    |
| 3288 | 
                        - )  | 
                    |
| 3289 | 
                        - assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 3290 | 
                        -  | 
                    |
| 3291 | 
                        - @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 3292 | 
                        - def test_214b_export_settings_bad_stored_config(  | 
                    |
| 3293 | 
                        - self,  | 
                    |
| 3294 | 
                        - export_options: list[str],  | 
                    |
| 3295 | 
                        - ) -> None:  | 
                    |
| 3296 | 
                        - """Exporting an invalid config fails."""  | 
                    |
| 3297 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3298 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3299 | 
                        - # with-statements.  | 
                    |
| 3300 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3301 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3302 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3303 | 
                        - stack.enter_context(  | 
                    |
| 3304 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3305 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3306 | 
                        - runner=runner,  | 
                    |
| 3307 | 
                        -                    vault_config={},
                       | 
                    |
| 3308 | 
                        - )  | 
                    |
| 3309 | 
                        - )  | 
                    |
| 3310 | 
                        - result = runner.invoke(  | 
                    |
| 3311 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3312 | 
                        - ["--export", "-", *export_options],  | 
                    |
| 3313 | 
                        - input="null",  | 
                    |
| 3314 | 
                        - catch_exceptions=False,  | 
                    |
| 3315 | 
                        - )  | 
                    |
| 3316 | 
                        - assert result.error_exit(error="Cannot load vault settings:"), (  | 
                    |
| 3317 | 
                        - "expected error exit and known error message"  | 
                    |
| 3318 | 
                        - )  | 
                    |
| 3319 | 
                        -  | 
                    |
| 3320 | 
                        - @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 3321 | 
                        - def test_214c_export_settings_not_a_file(  | 
                    |
| 3322 | 
                        - self,  | 
                    |
| 3323 | 
                        - export_options: list[str],  | 
                    |
| 3324 | 
                        - ) -> None:  | 
                    |
| 3325 | 
                        - """Exporting an invalid config fails."""  | 
                    |
| 3326 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3327 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3328 | 
                        - # with-statements.  | 
                    |
| 3329 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3330 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3331 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3332 | 
                        - stack.enter_context(  | 
                    |
| 3333 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 3334 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3335 | 
                        - runner=runner,  | 
                    |
| 3336 | 
                        - )  | 
                    |
| 3337 | 
                        - )  | 
                    |
| 3338 | 
                        - config_file = cli_helpers.config_filename(subsystem="vault")  | 
                    |
| 3339 | 
                        - config_file.unlink(missing_ok=True)  | 
                    |
| 3340 | 
                        - config_file.mkdir(parents=True, exist_ok=True)  | 
                    |
| 3341 | 
                        - result = runner.invoke(  | 
                    |
| 3342 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3343 | 
                        - ["--export", "-", *export_options],  | 
                    |
| 3344 | 
                        - input="null",  | 
                    |
| 3345 | 
                        - catch_exceptions=False,  | 
                    |
| 3346 | 
                        - )  | 
                    |
| 3347 | 
                        - assert result.error_exit(error="Cannot load vault settings:"), (  | 
                    |
| 3348 | 
                        - "expected error exit and known error message"  | 
                    |
| 3349 | 
                        - )  | 
                    |
| 3350 | 
                        -  | 
                    |
| 3351 | 
                        - @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 3352 | 
                        - def test_214d_export_settings_target_not_a_file(  | 
                    |
| 3353 | 
                        - self,  | 
                    |
| 3354 | 
                        - export_options: list[str],  | 
                    |
| 3355 | 
                        - ) -> None:  | 
                    |
| 3356 | 
                        - """Exporting an invalid config fails."""  | 
                    |
| 3357 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3358 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3359 | 
                        - # with-statements.  | 
                    |
| 3360 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3361 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3362 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3363 | 
                        - stack.enter_context(  | 
                    |
| 3364 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 3365 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3366 | 
                        - runner=runner,  | 
                    |
| 3367 | 
                        - )  | 
                    |
| 3368 | 
                        - )  | 
                    |
| 3369 | 
                        - dname = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 3370 | 
                        - result = runner.invoke(  | 
                    |
| 3371 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3372 | 
                        - ["--export", os.fsdecode(dname), *export_options],  | 
                    |
| 3373 | 
                        - input="null",  | 
                    |
| 3374 | 
                        - catch_exceptions=False,  | 
                    |
| 3375 | 
                        - )  | 
                    |
| 3376 | 
                        - assert result.error_exit(error="Cannot export vault settings:"), (  | 
                    |
| 3377 | 
                        - "expected error exit and known error message"  | 
                    |
| 3378 | 
                        - )  | 
                    |
| 3379 | 
                        -  | 
                    |
| 3380 | 
                        - @pytest_machinery.skip_if_on_the_annoying_os  | 
                    |
| 3381 | 
                        - @Parametrize.EXPORT_FORMAT_OPTIONS  | 
                    |
| 3382 | 
                        - def test_214e_export_settings_settings_directory_not_a_directory(  | 
                    |
| 3383 | 
                        - self,  | 
                    |
| 3384 | 
                        - export_options: list[str],  | 
                    |
| 3385 | 
                        - ) -> None:  | 
                    |
| 3386 | 
                        - """Exporting an invalid config fails."""  | 
                    |
| 3387 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3388 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3389 | 
                        - # with-statements.  | 
                    |
| 3390 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3391 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3392 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3393 | 
                        - stack.enter_context(  | 
                    |
| 3394 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 3395 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3396 | 
                        - runner=runner,  | 
                    |
| 3397 | 
                        - )  | 
                    |
| 3398 | 
                        - )  | 
                    |
| 3399 | 
                        - config_dir = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 3400 | 
                        - with contextlib.suppress(FileNotFoundError):  | 
                    |
| 3401 | 
                        - shutil.rmtree(config_dir)  | 
                    |
| 3402 | 
                        -            config_dir.write_text("Obstruction!!\n")
                       | 
                    |
| 3403 | 
                        - result = runner.invoke(  | 
                    |
| 3404 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3405 | 
                        - ["--export", "-", *export_options],  | 
                    |
| 3406 | 
                        - input="null",  | 
                    |
| 3407 | 
                        - catch_exceptions=False,  | 
                    |
| 3408 | 
                        - )  | 
                    |
| 3409 | 
                        - assert result.error_exit(  | 
                    |
| 3410 | 
                        - error="Cannot load vault settings:"  | 
                    |
| 3411 | 
                        - ) or result.error_exit(error="Cannot load user config:"), (  | 
                    |
| 3412 | 
                        - "expected error exit and known error message"  | 
                    |
| 3413 | 
                        - )  | 
                    |
| 3414 | 
                        -  | 
                    |
| 3415 | 
                        - @Parametrize.NOTES_PLACEMENT  | 
                    |
| 3416 | 
                        - @hypothesis.given(  | 
                    |
| 3417 | 
                        - notes=strategies.text(  | 
                    |
| 3418 | 
                        - strategies.characters(  | 
                    |
| 3419 | 
                        - min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 3420 | 
                        - ),  | 
                    |
| 3421 | 
                        - min_size=1,  | 
                    |
| 3422 | 
                        - max_size=512,  | 
                    |
| 3423 | 
                        - ).filter(str.strip),  | 
                    |
| 3424 | 
                        - )  | 
                    |
| 3425 | 
                        - def test_215_notes_placement(  | 
                    |
| 3426 | 
                        - self,  | 
                    |
| 3427 | 
                        - notes_placement: Literal["before", "after"],  | 
                    |
| 3428 | 
                        - placement_args: list[str],  | 
                    |
| 3429 | 
                        - notes: str,  | 
                    |
| 3430 | 
                        - ) -> None:  | 
                    |
| 3431 | 
                        - notes = notes.strip()  | 
                    |
| 3432 | 
                        -        maybe_notes = {"notes": notes} if notes else {}
                       | 
                    |
| 3433 | 
                        -        vault_config = {
                       | 
                    |
| 3434 | 
                        -            "global": {"phrase": DUMMY_PASSPHRASE},
                       | 
                    |
| 3435 | 
                        -            "services": {
                       | 
                    |
| 3436 | 
                        -                DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 3437 | 
                        - },  | 
                    |
| 3438 | 
                        - }  | 
                    |
| 3439 | 
                        -        result_phrase = DUMMY_RESULT_PASSPHRASE.decode("ascii")
                       | 
                    |
| 3440 | 
                        - expected = (  | 
                    |
| 3441 | 
                        -            f"{notes}\n\n{result_phrase}\n"
                       | 
                    |
| 3442 | 
                        - if notes_placement == "before"  | 
                    |
| 3443 | 
                        -            else f"{result_phrase}\n\n{notes}\n\n"
                       | 
                    |
| 3444 | 
                        - )  | 
                    |
| 3445 | 
                        - runner = machinery.CliRunner(mix_stderr=True)  | 
                    |
| 3446 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3447 | 
                        - # with-statements.  | 
                    |
| 3448 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3449 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3450 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3451 | 
                        - stack.enter_context(  | 
                    |
| 3452 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3453 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3454 | 
                        - runner=runner,  | 
                    |
| 3455 | 
                        - vault_config=vault_config,  | 
                    |
| 3456 | 
                        - )  | 
                    |
| 3457 | 
                        - )  | 
                    |
| 3458 | 
                        - result = runner.invoke(  | 
                    |
| 3459 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3460 | 
                        - [*placement_args, "--", DUMMY_SERVICE],  | 
                    |
| 3461 | 
                        - catch_exceptions=False,  | 
                    |
| 3462 | 
                        - )  | 
                    |
| 3463 | 
                        - assert result.clean_exit(output=expected), "expected clean exit"  | 
                    |
| 3464 | 
                        -  | 
                    |
| 3465 | 
                        - @Parametrize.MODERN_EDITOR_INTERFACE  | 
                    |
| 3466 | 
                        - @hypothesis.settings(  | 
                    |
| 3467 | 
                        - suppress_health_check=[  | 
                    |
| 3468 | 
                        - *hypothesis.settings().suppress_health_check,  | 
                    |
| 3469 | 
                        - hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 3470 | 
                        - ],  | 
                    |
| 3471 | 
                        - )  | 
                    |
| 3472 | 
                        - @hypothesis.given(  | 
                    |
| 3473 | 
                        - notes=strategies.text(  | 
                    |
| 3474 | 
                        - strategies.characters(  | 
                    |
| 3475 | 
                        - min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 3476 | 
                        - ),  | 
                    |
| 3477 | 
                        - min_size=1,  | 
                    |
| 3478 | 
                        - max_size=512,  | 
                    |
| 3479 | 
                        - ).filter(str.strip),  | 
                    |
| 3480 | 
                        - )  | 
                    |
| 3481 | 
                        - def test_220_edit_notes_successfully(  | 
                    |
| 3482 | 
                        - self,  | 
                    |
| 3483 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 3484 | 
                        - modern_editor_interface: bool,  | 
                    |
| 3485 | 
                        - notes: str,  | 
                    |
| 3486 | 
                        - ) -> None:  | 
                    |
| 3487 | 
                        - """Editing notes works."""  | 
                    |
| 3488 | 
                        - marker = cli_messages.TranslatedString(  | 
                    |
| 3489 | 
                        - cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER  | 
                    |
| 3490 | 
                        - )  | 
                    |
| 3491 | 
                        - edit_result = f"""  | 
                    |
| 3492 | 
                        -  | 
                    |
| 3493 | 
                        -{marker}
                       | 
                    |
| 3494 | 
                        -{notes}
                       | 
                    |
| 3495 | 
                        -"""  | 
                    |
| 3496 | 
                        - # Reset caplog between hypothesis runs.  | 
                    |
| 3497 | 
                        - caplog.clear()  | 
                    |
| 3498 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3499 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3500 | 
                        - # with-statements.  | 
                    |
| 3501 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3502 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3503 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3504 | 
                        - stack.enter_context(  | 
                    |
| 3505 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3506 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3507 | 
                        - runner=runner,  | 
                    |
| 3508 | 
                        -                    vault_config={
                       | 
                    |
| 3509 | 
                        -                        "global": {"phrase": "abc"},
                       | 
                    |
| 3510 | 
                        -                        "services": {"sv": {"notes": "Contents go here"}},
                       | 
                    |
| 3511 | 
                        - },  | 
                    |
| 3512 | 
                        - )  | 
                    |
| 3513 | 
                        - )  | 
                    |
| 3514 | 
                        - notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 3515 | 
                        - subsystem="notes backup"  | 
                    |
| 3516 | 
                        - )  | 
                    |
| 3517 | 
                        - notes_backup_file.write_text(  | 
                    |
| 3518 | 
                        - "These backup notes are left over from the previous session.",  | 
                    |
| 3519 | 
                        - encoding="UTF-8",  | 
                    |
| 3520 | 
                        - )  | 
                    |
| 3521 | 
                        - monkeypatch.setattr(click, "edit", lambda *_a, **_kw: edit_result)  | 
                    |
| 3522 | 
                        - result = runner.invoke(  | 
                    |
| 3523 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3524 | 
                        - [  | 
                    |
| 3525 | 
                        - "--config",  | 
                    |
| 3526 | 
                        - "--notes",  | 
                    |
| 3527 | 
                        - "--modern-editor-interface"  | 
                    |
| 3528 | 
                        - if modern_editor_interface  | 
                    |
| 3529 | 
                        - else "--vault-legacy-editor-interface",  | 
                    |
| 3530 | 
                        - "--",  | 
                    |
| 3531 | 
                        - "sv",  | 
                    |
| 3532 | 
                        - ],  | 
                    |
| 3533 | 
                        - catch_exceptions=False,  | 
                    |
| 3534 | 
                        - )  | 
                    |
| 3535 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 3536 | 
                        - assert all(map(is_warning_line, result.stderr.splitlines(True)))  | 
                    |
| 3537 | 
                        - assert modern_editor_interface or machinery.warning_emitted(  | 
                    |
| 3538 | 
                        - "A backup copy of the old notes was saved",  | 
                    |
| 3539 | 
                        - caplog.record_tuples,  | 
                    |
| 3540 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 3541 | 
                        - assert (  | 
                    |
| 3542 | 
                        - modern_editor_interface  | 
                    |
| 3543 | 
                        - or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 3544 | 
                        - == "Contents go here"  | 
                    |
| 3545 | 
                        - )  | 
                    |
| 3546 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3547 | 
                        - encoding="UTF-8"  | 
                    |
| 3548 | 
                        - ) as infile:  | 
                    |
| 3549 | 
                        - config = json.load(infile)  | 
                    |
| 3550 | 
                        -            assert config == {
                       | 
                    |
| 3551 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 3552 | 
                        -                "services": {
                       | 
                    |
| 3553 | 
                        -                    "sv": {
                       | 
                    |
| 3554 | 
                        - "notes": notes.strip()  | 
                    |
| 3555 | 
                        - if modern_editor_interface  | 
                    |
| 3556 | 
                        - else edit_result.strip()  | 
                    |
| 3557 | 
                        - }  | 
                    |
| 3558 | 
                        - },  | 
                    |
| 3559 | 
                        - }  | 
                    |
| 3560 | 
                        -  | 
                    |
| 3561 | 
                        - @Parametrize.NOOP_EDIT_FUNCS  | 
                    |
| 3562 | 
                        - @hypothesis.given(  | 
                    |
| 3563 | 
                        - notes=strategies.text(  | 
                    |
| 3564 | 
                        - strategies.characters(  | 
                    |
| 3565 | 
                        - min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 3566 | 
                        - ),  | 
                    |
| 3567 | 
                        - min_size=1,  | 
                    |
| 3568 | 
                        - max_size=512,  | 
                    |
| 3569 | 
                        - ).filter(str.strip),  | 
                    |
| 3570 | 
                        - )  | 
                    |
| 3571 | 
                        - def test_221_edit_notes_noop(  | 
                    |
| 3572 | 
                        - self,  | 
                    |
| 3573 | 
                        - edit_func_name: Literal["empty", "space"],  | 
                    |
| 3574 | 
                        - modern_editor_interface: bool,  | 
                    |
| 3575 | 
                        - notes: str,  | 
                    |
| 3576 | 
                        - ) -> None:  | 
                    |
| 3577 | 
                        - """Abandoning edited notes works."""  | 
                    |
| 3578 | 
                        -  | 
                    |
| 3579 | 
                        - def empty(text: str, *_args: Any, **_kwargs: Any) -> str:  | 
                    |
| 3580 | 
                        - del text  | 
                    |
| 3581 | 
                        - return ""  | 
                    |
| 3582 | 
                        -  | 
                    |
| 3583 | 
                        - def space(text: str, *_args: Any, **_kwargs: Any) -> str:  | 
                    |
| 3584 | 
                        - del text  | 
                    |
| 3585 | 
                        - return " " + notes.strip() + "\n\n\n\n\n\n"  | 
                    |
| 3586 | 
                        -  | 
                    |
| 3587 | 
                        -        edit_funcs = {"empty": empty, "space": space}
                       | 
                    |
| 3588 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3589 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3590 | 
                        - # with-statements.  | 
                    |
| 3591 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3592 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3593 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3594 | 
                        - stack.enter_context(  | 
                    |
| 3595 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3596 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3597 | 
                        - runner=runner,  | 
                    |
| 3598 | 
                        -                    vault_config={
                       | 
                    |
| 3599 | 
                        -                        "global": {"phrase": "abc"},
                       | 
                    |
| 3600 | 
                        -                        "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 3601 | 
                        - },  | 
                    |
| 3602 | 
                        - )  | 
                    |
| 3603 | 
                        - )  | 
                    |
| 3604 | 
                        - notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 3605 | 
                        - subsystem="notes backup"  | 
                    |
| 3606 | 
                        - )  | 
                    |
| 3607 | 
                        - notes_backup_file.write_text(  | 
                    |
| 3608 | 
                        - "These backup notes are left over from the previous session.",  | 
                    |
| 3609 | 
                        - encoding="UTF-8",  | 
                    |
| 3610 | 
                        - )  | 
                    |
| 3611 | 
                        - monkeypatch.setattr(click, "edit", edit_funcs[edit_func_name])  | 
                    |
| 3612 | 
                        - result = runner.invoke(  | 
                    |
| 3613 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3614 | 
                        - [  | 
                    |
| 3615 | 
                        - "--config",  | 
                    |
| 3616 | 
                        - "--notes",  | 
                    |
| 3617 | 
                        - "--modern-editor-interface"  | 
                    |
| 3618 | 
                        - if modern_editor_interface  | 
                    |
| 3619 | 
                        - else "--vault-legacy-editor-interface",  | 
                    |
| 3620 | 
                        - "--",  | 
                    |
| 3621 | 
                        - "sv",  | 
                    |
| 3622 | 
                        - ],  | 
                    |
| 3623 | 
                        - catch_exceptions=False,  | 
                    |
| 3624 | 
                        - )  | 
                    |
| 3625 | 
                        - assert result.clean_exit(empty_stderr=True) or result.error_exit(  | 
                    |
| 3626 | 
                        - error="the user aborted the request"  | 
                    |
| 3627 | 
                        - ), "expected clean exit"  | 
                    |
| 3628 | 
                        - assert (  | 
                    |
| 3629 | 
                        - modern_editor_interface  | 
                    |
| 3630 | 
                        - or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 3631 | 
                        - == "These backup notes are left over from the previous session."  | 
                    |
| 3632 | 
                        - )  | 
                    |
| 3633 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3634 | 
                        - encoding="UTF-8"  | 
                    |
| 3635 | 
                        - ) as infile:  | 
                    |
| 3636 | 
                        - config = json.load(infile)  | 
                    |
| 3637 | 
                        -            assert config == {
                       | 
                    |
| 3638 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 3639 | 
                        -                "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 3640 | 
                        - }  | 
                    |
| 3641 | 
                        -  | 
                    |
| 3642 | 
                        - # TODO(the-13th-letter): Keep this behavior or not, with or without  | 
                    |
| 3643 | 
                        - # warning?  | 
                    |
| 3644 | 
                        - @Parametrize.MODERN_EDITOR_INTERFACE  | 
                    |
| 3645 | 
                        - @hypothesis.settings(  | 
                    |
| 3646 | 
                        - suppress_health_check=[  | 
                    |
| 3647 | 
                        - *hypothesis.settings().suppress_health_check,  | 
                    |
| 3648 | 
                        - hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 3649 | 
                        - ],  | 
                    |
| 3650 | 
                        - )  | 
                    |
| 3651 | 
                        - @hypothesis.given(  | 
                    |
| 3652 | 
                        - notes=strategies.text(  | 
                    |
| 3653 | 
                        - strategies.characters(  | 
                    |
| 3654 | 
                        - min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 3655 | 
                        - ),  | 
                    |
| 3656 | 
                        - min_size=1,  | 
                    |
| 3657 | 
                        - max_size=512,  | 
                    |
| 3658 | 
                        - ).filter(str.strip),  | 
                    |
| 3659 | 
                        - )  | 
                    |
| 3660 | 
                        - def test_222_edit_notes_marker_removed(  | 
                    |
| 3661 | 
                        - self,  | 
                    |
| 3662 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 3663 | 
                        - modern_editor_interface: bool,  | 
                    |
| 3664 | 
                        - notes: str,  | 
                    |
| 3665 | 
                        - ) -> None:  | 
                    |
| 3666 | 
                        - """Removing the notes marker still saves the notes.  | 
                    |
| 3667 | 
                        -  | 
                    |
| 3668 | 
                        - TODO: Keep this behavior or not, with or without warning?  | 
                    |
| 3669 | 
                        -  | 
                    |
| 3670 | 
                        - """  | 
                    |
| 3671 | 
                        - notes_marker = cli_messages.TranslatedString(  | 
                    |
| 3672 | 
                        - cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER  | 
                    |
| 3673 | 
                        - )  | 
                    |
| 3674 | 
                        - hypothesis.assume(str(notes_marker) not in notes.strip())  | 
                    |
| 3675 | 
                        - # Reset caplog between hypothesis runs.  | 
                    |
| 3676 | 
                        - caplog.clear()  | 
                    |
| 3677 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3678 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3679 | 
                        - # with-statements.  | 
                    |
| 3680 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3681 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3682 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3683 | 
                        - stack.enter_context(  | 
                    |
| 3684 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3685 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3686 | 
                        - runner=runner,  | 
                    |
| 3687 | 
                        -                    vault_config={
                       | 
                    |
| 3688 | 
                        -                        "global": {"phrase": "abc"},
                       | 
                    |
| 3689 | 
                        -                        "services": {"sv": {"notes": "Contents go here"}},
                       | 
                    |
| 3690 | 
                        - },  | 
                    |
| 3691 | 
                        - )  | 
                    |
| 3692 | 
                        - )  | 
                    |
| 3693 | 
                        - notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 3694 | 
                        - subsystem="notes backup"  | 
                    |
| 3695 | 
                        - )  | 
                    |
| 3696 | 
                        - notes_backup_file.write_text(  | 
                    |
| 3697 | 
                        - "These backup notes are left over from the previous session.",  | 
                    |
| 3698 | 
                        - encoding="UTF-8",  | 
                    |
| 3699 | 
                        - )  | 
                    |
| 3700 | 
                        - monkeypatch.setattr(click, "edit", lambda *_a, **_kw: notes)  | 
                    |
| 3701 | 
                        - result = runner.invoke(  | 
                    |
| 3702 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3703 | 
                        - [  | 
                    |
| 3704 | 
                        - "--config",  | 
                    |
| 3705 | 
                        - "--notes",  | 
                    |
| 3706 | 
                        - "--modern-editor-interface"  | 
                    |
| 3707 | 
                        - if modern_editor_interface  | 
                    |
| 3708 | 
                        - else "--vault-legacy-editor-interface",  | 
                    |
| 3709 | 
                        - "--",  | 
                    |
| 3710 | 
                        - "sv",  | 
                    |
| 3711 | 
                        - ],  | 
                    |
| 3712 | 
                        - catch_exceptions=False,  | 
                    |
| 3713 | 
                        - )  | 
                    |
| 3714 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 3715 | 
                        - assert not result.stderr or all(  | 
                    |
| 3716 | 
                        - map(is_warning_line, result.stderr.splitlines(True))  | 
                    |
| 3717 | 
                        - )  | 
                    |
| 3718 | 
                        - assert not caplog.record_tuples or machinery.warning_emitted(  | 
                    |
| 3719 | 
                        - "A backup copy of the old notes was saved",  | 
                    |
| 3720 | 
                        - caplog.record_tuples,  | 
                    |
| 3721 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 3722 | 
                        - assert (  | 
                    |
| 3723 | 
                        - modern_editor_interface  | 
                    |
| 3724 | 
                        - or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 3725 | 
                        - == "Contents go here"  | 
                    |
| 3726 | 
                        - )  | 
                    |
| 3727 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3728 | 
                        - encoding="UTF-8"  | 
                    |
| 3729 | 
                        - ) as infile:  | 
                    |
| 3730 | 
                        - config = json.load(infile)  | 
                    |
| 3731 | 
                        -            assert config == {
                       | 
                    |
| 3732 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 3733 | 
                        -                "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 3734 | 
                        - }  | 
                    |
| 3735 | 
                        -  | 
                    |
| 3736 | 
                        - @hypothesis.given(  | 
                    |
| 3737 | 
                        - notes=strategies.text(  | 
                    |
| 3738 | 
                        - strategies.characters(  | 
                    |
| 3739 | 
                        - min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 3740 | 
                        - ),  | 
                    |
| 3741 | 
                        - min_size=1,  | 
                    |
| 3742 | 
                        - max_size=512,  | 
                    |
| 3743 | 
                        - ).filter(str.strip),  | 
                    |
| 3744 | 
                        - )  | 
                    |
| 3745 | 
                        - def test_223_edit_notes_abort(  | 
                    |
| 3746 | 
                        - self,  | 
                    |
| 3747 | 
                        - notes: str,  | 
                    |
| 3748 | 
                        - ) -> None:  | 
                    |
| 3749 | 
                        - """Aborting editing notes works.  | 
                    |
| 3750 | 
                        -  | 
                    |
| 3751 | 
                        - Aborting is only supported with the modern editor interface.  | 
                    |
| 3752 | 
                        -  | 
                    |
| 3753 | 
                        - """  | 
                    |
| 3754 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3755 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3756 | 
                        - # with-statements.  | 
                    |
| 3757 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3758 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3759 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3760 | 
                        - stack.enter_context(  | 
                    |
| 3761 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3762 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3763 | 
                        - runner=runner,  | 
                    |
| 3764 | 
                        -                    vault_config={
                       | 
                    |
| 3765 | 
                        -                        "global": {"phrase": "abc"},
                       | 
                    |
| 3766 | 
                        -                        "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 3767 | 
                        - },  | 
                    |
| 3768 | 
                        - )  | 
                    |
| 3769 | 
                        - )  | 
                    |
| 3770 | 
                        - monkeypatch.setattr(click, "edit", lambda *_a, **_kw: "")  | 
                    |
| 3771 | 
                        - result = runner.invoke(  | 
                    |
| 3772 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3773 | 
                        - [  | 
                    |
| 3774 | 
                        - "--config",  | 
                    |
| 3775 | 
                        - "--notes",  | 
                    |
| 3776 | 
                        - "--modern-editor-interface",  | 
                    |
| 3777 | 
                        - "--",  | 
                    |
| 3778 | 
                        - "sv",  | 
                    |
| 3779 | 
                        - ],  | 
                    |
| 3780 | 
                        - catch_exceptions=False,  | 
                    |
| 3781 | 
                        - )  | 
                    |
| 3782 | 
                        - assert result.error_exit(error="the user aborted the request"), (  | 
                    |
| 3783 | 
                        - "expected known error message"  | 
                    |
| 3784 | 
                        - )  | 
                    |
| 3785 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3786 | 
                        - encoding="UTF-8"  | 
                    |
| 3787 | 
                        - ) as infile:  | 
                    |
| 3788 | 
                        - config = json.load(infile)  | 
                    |
| 3789 | 
                        -            assert config == {
                       | 
                    |
| 3790 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 3791 | 
                        -                "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 3792 | 
                        - }  | 
                    |
| 3793 | 
                        -  | 
                    |
| 3794 | 
                        - def test_223a_edit_empty_notes_abort(  | 
                    |
| 3795 | 
                        - self,  | 
                    |
| 3796 | 
                        - ) -> None:  | 
                    |
| 3797 | 
                        - """Aborting editing notes works even if no notes are stored yet.  | 
                    |
| 3798 | 
                        -  | 
                    |
| 3799 | 
                        - Aborting is only supported with the modern editor interface.  | 
                    |
| 3800 | 
                        -  | 
                    |
| 3801 | 
                        - """  | 
                    |
| 3802 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3803 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3804 | 
                        - # with-statements.  | 
                    |
| 3805 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3806 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3807 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3808 | 
                        - stack.enter_context(  | 
                    |
| 3809 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3810 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3811 | 
                        - runner=runner,  | 
                    |
| 3812 | 
                        -                    vault_config={
                       | 
                    |
| 3813 | 
                        -                        "global": {"phrase": "abc"},
                       | 
                    |
| 3814 | 
                        -                        "services": {},
                       | 
                    |
| 3815 | 
                        - },  | 
                    |
| 3816 | 
                        - )  | 
                    |
| 3817 | 
                        - )  | 
                    |
| 3818 | 
                        - monkeypatch.setattr(click, "edit", lambda *_a, **_kw: "")  | 
                    |
| 3819 | 
                        - result = runner.invoke(  | 
                    |
| 3820 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3821 | 
                        - [  | 
                    |
| 3822 | 
                        - "--config",  | 
                    |
| 3823 | 
                        - "--notes",  | 
                    |
| 3824 | 
                        - "--modern-editor-interface",  | 
                    |
| 3825 | 
                        - "--",  | 
                    |
| 3826 | 
                        - "sv",  | 
                    |
| 3827 | 
                        - ],  | 
                    |
| 3828 | 
                        - catch_exceptions=False,  | 
                    |
| 3829 | 
                        - )  | 
                    |
| 3830 | 
                        - assert result.error_exit(error="the user aborted the request"), (  | 
                    |
| 3831 | 
                        - "expected known error message"  | 
                    |
| 3832 | 
                        - )  | 
                    |
| 3833 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3834 | 
                        - encoding="UTF-8"  | 
                    |
| 3835 | 
                        - ) as infile:  | 
                    |
| 3836 | 
                        - config = json.load(infile)  | 
                    |
| 3837 | 
                        -            assert config == {
                       | 
                    |
| 3838 | 
                        -                "global": {"phrase": "abc"},
                       | 
                    |
| 3839 | 
                        -                "services": {},
                       | 
                    |
| 3840 | 
                        - }  | 
                    |
| 3841 | 
                        -  | 
                    |
| 3842 | 
                        - @Parametrize.MODERN_EDITOR_INTERFACE  | 
                    |
| 3843 | 
                        - @hypothesis.settings(  | 
                    |
| 3844 | 
                        - suppress_health_check=[  | 
                    |
| 3845 | 
                        - *hypothesis.settings().suppress_health_check,  | 
                    |
| 3846 | 
                        - hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 3847 | 
                        - ],  | 
                    |
| 3848 | 
                        - )  | 
                    |
| 3849 | 
                        - @hypothesis.given(  | 
                    |
| 3850 | 
                        - notes=strategies.text(  | 
                    |
| 3851 | 
                        - strategies.characters(  | 
                    |
| 3852 | 
                        - min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 3853 | 
                        - ),  | 
                    |
| 3854 | 
                        - max_size=512,  | 
                    |
| 3855 | 
                        - ),  | 
                    |
| 3856 | 
                        - )  | 
                    |
| 3857 | 
                        - def test_223b_edit_notes_fail_config_option_missing(  | 
                    |
| 3858 | 
                        - self,  | 
                    |
| 3859 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 3860 | 
                        - modern_editor_interface: bool,  | 
                    |
| 3861 | 
                        - notes: str,  | 
                    |
| 3862 | 
                        - ) -> None:  | 
                    |
| 3863 | 
                        - """Editing notes fails (and warns) if `--config` is missing."""  | 
                    |
| 3864 | 
                        -        maybe_notes = {"notes": notes.strip()} if notes.strip() else {}
                       | 
                    |
| 3865 | 
                        -        vault_config = {
                       | 
                    |
| 3866 | 
                        -            "global": {"phrase": DUMMY_PASSPHRASE},
                       | 
                    |
| 3867 | 
                        -            "services": {
                       | 
                    |
| 3868 | 
                        -                DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 3869 | 
                        - },  | 
                    |
| 3870 | 
                        - }  | 
                    |
| 3871 | 
                        - # Reset caplog between hypothesis runs.  | 
                    |
| 3872 | 
                        - caplog.clear()  | 
                    |
| 3873 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3874 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3875 | 
                        - # with-statements.  | 
                    |
| 3876 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3877 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3878 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3879 | 
                        - stack.enter_context(  | 
                    |
| 3880 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3881 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3882 | 
                        - runner=runner,  | 
                    |
| 3883 | 
                        - vault_config=vault_config,  | 
                    |
| 3884 | 
                        - )  | 
                    |
| 3885 | 
                        - )  | 
                    |
| 3886 | 
                        - EDIT_ATTEMPTED = "edit attempted!" # noqa: N806  | 
                    |
| 3887 | 
                        -  | 
                    |
| 3888 | 
                        - def raiser(*_args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 3889 | 
                        - pytest.fail(EDIT_ATTEMPTED)  | 
                    |
| 3890 | 
                        -  | 
                    |
| 3891 | 
                        - notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 3892 | 
                        - subsystem="notes backup"  | 
                    |
| 3893 | 
                        - )  | 
                    |
| 3894 | 
                        - notes_backup_file.write_text(  | 
                    |
| 3895 | 
                        - "These backup notes are left over from the previous session.",  | 
                    |
| 3896 | 
                        - encoding="UTF-8",  | 
                    |
| 3897 | 
                        - )  | 
                    |
| 3898 | 
                        - monkeypatch.setattr(click, "edit", raiser)  | 
                    |
| 3899 | 
                        - result = runner.invoke(  | 
                    |
| 3900 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3901 | 
                        - [  | 
                    |
| 3902 | 
                        - "--notes",  | 
                    |
| 3903 | 
                        - "--modern-editor-interface"  | 
                    |
| 3904 | 
                        - if modern_editor_interface  | 
                    |
| 3905 | 
                        - else "--vault-legacy-editor-interface",  | 
                    |
| 3906 | 
                        - "--",  | 
                    |
| 3907 | 
                        - DUMMY_SERVICE,  | 
                    |
| 3908 | 
                        - ],  | 
                    |
| 3909 | 
                        - catch_exceptions=False,  | 
                    |
| 3910 | 
                        - )  | 
                    |
| 3911 | 
                        - assert result.clean_exit(  | 
                    |
| 3912 | 
                        -                output=DUMMY_RESULT_PASSPHRASE.decode("ascii")
                       | 
                    |
| 3913 | 
                        - ), "expected clean exit"  | 
                    |
| 3914 | 
                        - assert result.stderr  | 
                    |
| 3915 | 
                        - assert notes.strip() in result.stderr  | 
                    |
| 3916 | 
                        - assert all(  | 
                    |
| 3917 | 
                        - is_warning_line(line)  | 
                    |
| 3918 | 
                        - for line in result.stderr.splitlines(True)  | 
                    |
| 3919 | 
                        -                if line.startswith(f"{cli.PROG_NAME}: ")
                       | 
                    |
| 3920 | 
                        - )  | 
                    |
| 3921 | 
                        - assert machinery.warning_emitted(  | 
                    |
| 3922 | 
                        - "Specifying --notes without --config is ineffective. "  | 
                    |
| 3923 | 
                        - "No notes will be edited.",  | 
                    |
| 3924 | 
                        - caplog.record_tuples,  | 
                    |
| 3925 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 3926 | 
                        - assert (  | 
                    |
| 3927 | 
                        - modern_editor_interface  | 
                    |
| 3928 | 
                        - or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 3929 | 
                        - == "These backup notes are left over from the previous session."  | 
                    |
| 3930 | 
                        - )  | 
                    |
| 3931 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 3932 | 
                        - encoding="UTF-8"  | 
                    |
| 3933 | 
                        - ) as infile:  | 
                    |
| 3934 | 
                        - config = json.load(infile)  | 
                    |
| 3935 | 
                        - assert config == vault_config  | 
                    |
| 3936 | 
                        -  | 
                    |
| 3937 | 
                        - @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG  | 
                    |
| 3938 | 
                        - def test_224_store_config_good(  | 
                    |
| 3939 | 
                        - self,  | 
                    |
| 3940 | 
                        - command_line: list[str],  | 
                    |
| 3941 | 
                        - input: str,  | 
                    |
| 3942 | 
                        - result_config: Any,  | 
                    |
| 3943 | 
                        - ) -> None:  | 
                    |
| 3944 | 
                        - """Storing valid settings via `--config` works.  | 
                    |
| 3945 | 
                        -  | 
                    |
| 3946 | 
                        - The format also contains embedded newlines and indentation to make  | 
                    |
| 3947 | 
                        - the config more readable.  | 
                    |
| 3948 | 
                        -  | 
                    |
| 3949 | 
                        - """  | 
                    |
| 3950 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3951 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3952 | 
                        - # with-statements.  | 
                    |
| 3953 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3954 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3955 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3956 | 
                        - stack.enter_context(  | 
                    |
| 3957 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 3958 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 3959 | 
                        - runner=runner,  | 
                    |
| 3960 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 3961 | 
                        - )  | 
                    |
| 3962 | 
                        - )  | 
                    |
| 3963 | 
                        - monkeypatch.setattr(  | 
                    |
| 3964 | 
                        - cli_helpers,  | 
                    |
| 3965 | 
                        - "get_suitable_ssh_keys",  | 
                    |
| 3966 | 
                        - callables.suitable_ssh_keys,  | 
                    |
| 3967 | 
                        - )  | 
                    |
| 3968 | 
                        - result = runner.invoke(  | 
                    |
| 3969 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 3970 | 
                        - ["--config", *command_line],  | 
                    |
| 3971 | 
                        - catch_exceptions=False,  | 
                    |
| 3972 | 
                        - input=input,  | 
                    |
| 3973 | 
                        - )  | 
                    |
| 3974 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 3975 | 
                        - config_txt = cli_helpers.config_filename(  | 
                    |
| 3976 | 
                        - subsystem="vault"  | 
                    |
| 3977 | 
                        - ).read_text(encoding="UTF-8")  | 
                    |
| 3978 | 
                        - config = json.loads(config_txt)  | 
                    |
| 3979 | 
                        - assert config == result_config, (  | 
                    |
| 3980 | 
                        - "stored config does not match expectation"  | 
                    |
| 3981 | 
                        - )  | 
                    |
| 3982 | 
                        - assert_vault_config_is_indented_and_line_broken(config_txt)  | 
                    |
| 3983 | 
                        -  | 
                    |
| 3984 | 
                        - @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES  | 
                    |
| 3985 | 
                        - def test_225_store_config_fail(  | 
                    |
| 3986 | 
                        - self,  | 
                    |
| 3987 | 
                        - command_line: list[str],  | 
                    |
| 3988 | 
                        - input: str,  | 
                    |
| 3989 | 
                        - err_text: str,  | 
                    |
| 3990 | 
                        - ) -> None:  | 
                    |
| 3991 | 
                        - """Storing invalid settings via `--config` fails."""  | 
                    |
| 3992 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 3993 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 3994 | 
                        - # with-statements.  | 
                    |
| 3995 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 3996 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 3997 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 3998 | 
                        - stack.enter_context(  | 
                    |
| 3999 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4000 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4001 | 
                        - runner=runner,  | 
                    |
| 4002 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4003 | 
                        - )  | 
                    |
| 4004 | 
                        - )  | 
                    |
| 4005 | 
                        - monkeypatch.setattr(  | 
                    |
| 4006 | 
                        - cli_helpers,  | 
                    |
| 4007 | 
                        - "get_suitable_ssh_keys",  | 
                    |
| 4008 | 
                        - callables.suitable_ssh_keys,  | 
                    |
| 4009 | 
                        - )  | 
                    |
| 4010 | 
                        - result = runner.invoke(  | 
                    |
| 4011 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4012 | 
                        - ["--config", *command_line],  | 
                    |
| 4013 | 
                        - catch_exceptions=False,  | 
                    |
| 4014 | 
                        - input=input,  | 
                    |
| 4015 | 
                        - )  | 
                    |
| 4016 | 
                        - assert result.error_exit(error=err_text), (  | 
                    |
| 4017 | 
                        - "expected error exit and known error message"  | 
                    |
| 4018 | 
                        - )  | 
                    |
| 4019 | 
                        -  | 
                    |
| 4020 | 
                        - def test_225a_store_config_fail_manual_no_ssh_key_selection(  | 
                    |
| 4021 | 
                        - self,  | 
                    |
| 4022 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 4023 | 
                        - ) -> None:  | 
                    |
| 4024 | 
                        - """Not selecting an SSH key during `--config --key` fails."""  | 
                    |
| 4025 | 
                        - del running_ssh_agent  | 
                    |
| 4026 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4027 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4028 | 
                        - # with-statements.  | 
                    |
| 4029 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4030 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4031 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4032 | 
                        - stack.enter_context(  | 
                    |
| 4033 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4034 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4035 | 
                        - runner=runner,  | 
                    |
| 4036 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4037 | 
                        - )  | 
                    |
| 4038 | 
                        - )  | 
                    |
| 4039 | 
                        -  | 
                    |
| 4040 | 
                        - def prompt_for_selection(*_args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 4041 | 
                        - raise IndexError(cli_helpers.EMPTY_SELECTION)  | 
                    |
| 4042 | 
                        -  | 
                    |
| 4043 | 
                        - monkeypatch.setattr(  | 
                    |
| 4044 | 
                        - cli_helpers, "prompt_for_selection", prompt_for_selection  | 
                    |
| 4045 | 
                        - )  | 
                    |
| 4046 | 
                        - # Also patch the list of suitable SSH keys, lest we be at  | 
                    |
| 4047 | 
                        - # the mercy of whatever SSH agent may be running.  | 
                    |
| 4048 | 
                        - monkeypatch.setattr(  | 
                    |
| 4049 | 
                        - cli_helpers,  | 
                    |
| 4050 | 
                        - "get_suitable_ssh_keys",  | 
                    |
| 4051 | 
                        - callables.suitable_ssh_keys,  | 
                    |
| 4052 | 
                        - )  | 
                    |
| 4053 | 
                        - result = runner.invoke(  | 
                    |
| 4054 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4055 | 
                        - ["--key", "--config"],  | 
                    |
| 4056 | 
                        - catch_exceptions=False,  | 
                    |
| 4057 | 
                        - )  | 
                    |
| 4058 | 
                        - assert result.error_exit(error="the user aborted the request"), (  | 
                    |
| 4059 | 
                        - "expected error exit and known error message"  | 
                    |
| 4060 | 
                        - )  | 
                    |
| 4061 | 
                        -  | 
                    |
| 4062 | 
                        - def test_225b_store_config_fail_manual_no_ssh_agent(  | 
                    |
| 4063 | 
                        - self,  | 
                    |
| 4064 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 4065 | 
                        - ) -> None:  | 
                    |
| 4066 | 
                        - """Not running an SSH agent during `--config --key` fails."""  | 
                    |
| 4067 | 
                        - del running_ssh_agent  | 
                    |
| 4068 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4069 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4070 | 
                        - # with-statements.  | 
                    |
| 4071 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4072 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4073 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4074 | 
                        - stack.enter_context(  | 
                    |
| 4075 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4076 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4077 | 
                        - runner=runner,  | 
                    |
| 4078 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4079 | 
                        - )  | 
                    |
| 4080 | 
                        - )  | 
                    |
| 4081 | 
                        -            monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
                       | 
                    |
| 4082 | 
                        - result = runner.invoke(  | 
                    |
| 4083 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4084 | 
                        - ["--key", "--config"],  | 
                    |
| 4085 | 
                        - catch_exceptions=False,  | 
                    |
| 4086 | 
                        - )  | 
                    |
| 4087 | 
                        - assert result.error_exit(error="Cannot find any running SSH agent"), (  | 
                    |
| 4088 | 
                        - "expected error exit and known error message"  | 
                    |
| 4089 | 
                        - )  | 
                    |
| 4090 | 
                        -  | 
                    |
| 4091 | 
                        - def test_225c_store_config_fail_manual_bad_ssh_agent_connection(  | 
                    |
| 4092 | 
                        - self,  | 
                    |
| 4093 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 4094 | 
                        - ) -> None:  | 
                    |
| 4095 | 
                        - """Not running a reachable SSH agent during `--config --key` fails."""  | 
                    |
| 4096 | 
                        - running_ssh_agent.require_external_address()  | 
                    |
| 4097 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4098 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4099 | 
                        - # with-statements.  | 
                    |
| 4100 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4101 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4102 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4103 | 
                        - stack.enter_context(  | 
                    |
| 4104 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4105 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4106 | 
                        - runner=runner,  | 
                    |
| 4107 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4108 | 
                        - )  | 
                    |
| 4109 | 
                        - )  | 
                    |
| 4110 | 
                        - cwd = pathlib.Path.cwd().resolve()  | 
                    |
| 4111 | 
                        -            monkeypatch.setenv("SSH_AUTH_SOCK", str(cwd))
                       | 
                    |
| 4112 | 
                        - result = runner.invoke(  | 
                    |
| 4113 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4114 | 
                        - ["--key", "--config"],  | 
                    |
| 4115 | 
                        - catch_exceptions=False,  | 
                    |
| 4116 | 
                        - )  | 
                    |
| 4117 | 
                        - assert result.error_exit(error="Cannot connect to the SSH agent"), (  | 
                    |
| 4118 | 
                        - "expected error exit and known error message"  | 
                    |
| 4119 | 
                        - )  | 
                    |
| 4120 | 
                        -  | 
                    |
| 4121 | 
                        - @Parametrize.TRY_RACE_FREE_IMPLEMENTATION  | 
                    |
| 4122 | 
                        - def test_225d_store_config_fail_manual_read_only_file(  | 
                    |
| 4123 | 
                        - self,  | 
                    |
| 4124 | 
                        - try_race_free_implementation: bool,  | 
                    |
| 4125 | 
                        - ) -> None:  | 
                    |
| 4126 | 
                        - """Using a read-only configuration file with `--config` fails."""  | 
                    |
| 4127 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4128 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4129 | 
                        - # with-statements.  | 
                    |
| 4130 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4131 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4132 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4133 | 
                        - stack.enter_context(  | 
                    |
| 4134 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4135 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4136 | 
                        - runner=runner,  | 
                    |
| 4137 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4138 | 
                        - )  | 
                    |
| 4139 | 
                        - )  | 
                    |
| 4140 | 
                        - callables.make_file_readonly(  | 
                    |
| 4141 | 
                        - cli_helpers.config_filename(subsystem="vault"),  | 
                    |
| 4142 | 
                        - try_race_free_implementation=try_race_free_implementation,  | 
                    |
| 4143 | 
                        - )  | 
                    |
| 4144 | 
                        - result = runner.invoke(  | 
                    |
| 4145 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4146 | 
                        - ["--config", "--length=15", "--", DUMMY_SERVICE],  | 
                    |
| 4147 | 
                        - catch_exceptions=False,  | 
                    |
| 4148 | 
                        - )  | 
                    |
| 4149 | 
                        - assert result.error_exit(error="Cannot store vault settings:"), (  | 
                    |
| 4150 | 
                        - "expected error exit and known error message"  | 
                    |
| 4151 | 
                        - )  | 
                    |
| 4152 | 
                        -  | 
                    |
| 4153 | 
                        - def test_225e_store_config_fail_manual_custom_error(  | 
                    |
| 4154 | 
                        - self,  | 
                    |
| 4155 | 
                        - ) -> None:  | 
                    |
| 4156 | 
                        - """OS-erroring with `--config` fails."""  | 
                    |
| 4157 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4158 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4159 | 
                        - # with-statements.  | 
                    |
| 4160 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4161 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4162 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4163 | 
                        - stack.enter_context(  | 
                    |
| 4164 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4165 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4166 | 
                        - runner=runner,  | 
                    |
| 4167 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4168 | 
                        - )  | 
                    |
| 4169 | 
                        - )  | 
                    |
| 4170 | 
                        - custom_error = "custom error message"  | 
                    |
| 4171 | 
                        -  | 
                    |
| 4172 | 
                        - def raiser(config: Any) -> None:  | 
                    |
| 4173 | 
                        - del config  | 
                    |
| 4174 | 
                        - raise RuntimeError(custom_error)  | 
                    |
| 4175 | 
                        -  | 
                    |
| 4176 | 
                        - monkeypatch.setattr(cli_helpers, "save_config", raiser)  | 
                    |
| 4177 | 
                        - result = runner.invoke(  | 
                    |
| 4178 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4179 | 
                        - ["--config", "--length=15", "--", DUMMY_SERVICE],  | 
                    |
| 4180 | 
                        - catch_exceptions=False,  | 
                    |
| 4181 | 
                        - )  | 
                    |
| 4182 | 
                        - assert result.error_exit(error=custom_error), (  | 
                    |
| 4183 | 
                        - "expected error exit and known error message"  | 
                    |
| 4184 | 
                        - )  | 
                    |
| 4185 | 
                        -  | 
                    |
| 4186 | 
                        - def test_225f_store_config_fail_unset_and_set_same_settings(  | 
                    |
| 4187 | 
                        - self,  | 
                    |
| 4188 | 
                        - ) -> None:  | 
                    |
| 4189 | 
                        - """Issuing conflicting settings to `--config` fails."""  | 
                    |
| 4190 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4191 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4192 | 
                        - # with-statements.  | 
                    |
| 4193 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4194 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4195 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4196 | 
                        - stack.enter_context(  | 
                    |
| 4197 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4198 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4199 | 
                        - runner=runner,  | 
                    |
| 4200 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4201 | 
                        - )  | 
                    |
| 4202 | 
                        - )  | 
                    |
| 4203 | 
                        - result = runner.invoke(  | 
                    |
| 4204 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4205 | 
                        - [  | 
                    |
| 4206 | 
                        - "--config",  | 
                    |
| 4207 | 
                        - "--unset=length",  | 
                    |
| 4208 | 
                        - "--length=15",  | 
                    |
| 4209 | 
                        - "--",  | 
                    |
| 4210 | 
                        - DUMMY_SERVICE,  | 
                    |
| 4211 | 
                        - ],  | 
                    |
| 4212 | 
                        - catch_exceptions=False,  | 
                    |
| 4213 | 
                        - )  | 
                    |
| 4214 | 
                        - assert result.error_exit(  | 
                    |
| 4215 | 
                        - error="Attempted to unset and set --length at the same time."  | 
                    |
| 4216 | 
                        - ), "expected error exit and known error message"  | 
                    |
| 4217 | 
                        -  | 
                    |
| 4218 | 
                        - def test_225g_store_config_fail_manual_ssh_agent_no_keys_loaded(  | 
                    |
| 4219 | 
                        - self,  | 
                    |
| 4220 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 4221 | 
                        - ) -> None:  | 
                    |
| 4222 | 
                        - """Not holding any SSH keys during `--config --key` fails."""  | 
                    |
| 4223 | 
                        - del running_ssh_agent  | 
                    |
| 4224 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4225 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4226 | 
                        - # with-statements.  | 
                    |
| 4227 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4228 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4229 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4230 | 
                        - stack.enter_context(  | 
                    |
| 4231 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4232 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4233 | 
                        - runner=runner,  | 
                    |
| 4234 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4235 | 
                        - )  | 
                    |
| 4236 | 
                        - )  | 
                    |
| 4237 | 
                        -  | 
                    |
| 4238 | 
                        - def func(  | 
                    |
| 4239 | 
                        - *_args: Any,  | 
                    |
| 4240 | 
                        - **_kwargs: Any,  | 
                    |
| 4241 | 
                        - ) -> list[_types.SSHKeyCommentPair]:  | 
                    |
| 4242 | 
                        - return []  | 
                    |
| 4243 | 
                        -  | 
                    |
| 4244 | 
                        - monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", func)  | 
                    |
| 4245 | 
                        - result = runner.invoke(  | 
                    |
| 4246 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4247 | 
                        - ["--key", "--config"],  | 
                    |
| 4248 | 
                        - catch_exceptions=False,  | 
                    |
| 4249 | 
                        - )  | 
                    |
| 4250 | 
                        - assert result.error_exit(error="no keys suitable"), (  | 
                    |
| 4251 | 
                        - "expected error exit and known error message"  | 
                    |
| 4252 | 
                        - )  | 
                    |
| 4253 | 
                        -  | 
                    |
| 4254 | 
                        - def test_225h_store_config_fail_manual_ssh_agent_runtime_error(  | 
                    |
| 4255 | 
                        - self,  | 
                    |
| 4256 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 4257 | 
                        - ) -> None:  | 
                    |
| 4258 | 
                        - """The SSH agent erroring during `--config --key` fails."""  | 
                    |
| 4259 | 
                        - del running_ssh_agent  | 
                    |
| 4260 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4261 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4262 | 
                        - # with-statements.  | 
                    |
| 4263 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4264 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4265 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4266 | 
                        - stack.enter_context(  | 
                    |
| 4267 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4268 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4269 | 
                        - runner=runner,  | 
                    |
| 4270 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4271 | 
                        - )  | 
                    |
| 4272 | 
                        - )  | 
                    |
| 4273 | 
                        -  | 
                    |
| 4274 | 
                        - def raiser(*_args: Any, **_kwargs: Any) -> None:  | 
                    |
| 4275 | 
                        - raise ssh_agent.TrailingDataError()  | 
                    |
| 4276 | 
                        -  | 
                    |
| 4277 | 
                        - monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", raiser)  | 
                    |
| 4278 | 
                        - result = runner.invoke(  | 
                    |
| 4279 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4280 | 
                        - ["--key", "--config"],  | 
                    |
| 4281 | 
                        - catch_exceptions=False,  | 
                    |
| 4282 | 
                        - )  | 
                    |
| 4283 | 
                        - assert result.error_exit(  | 
                    |
| 4284 | 
                        - error="violates the communication protocol."  | 
                    |
| 4285 | 
                        - ), "expected error exit and known error message"  | 
                    |
| 4286 | 
                        -  | 
                    |
| 4287 | 
                        - def test_225i_store_config_fail_manual_ssh_agent_refuses(  | 
                    |
| 4288 | 
                        - self,  | 
                    |
| 4289 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 4290 | 
                        - ) -> None:  | 
                    |
| 4291 | 
                        - """The SSH agent refusing during `--config --key` fails."""  | 
                    |
| 4292 | 
                        - del running_ssh_agent  | 
                    |
| 4293 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4294 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4295 | 
                        - # with-statements.  | 
                    |
| 4296 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4297 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4298 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4299 | 
                        - stack.enter_context(  | 
                    |
| 4300 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4301 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4302 | 
                        - runner=runner,  | 
                    |
| 4303 | 
                        -                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4304 | 
                        - )  | 
                    |
| 4305 | 
                        - )  | 
                    |
| 4306 | 
                        -  | 
                    |
| 4307 | 
                        - def func(*_args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 4308 | 
                        - raise ssh_agent.SSHAgentFailedError(  | 
                    |
| 4309 | 
                        - _types.SSH_AGENT.FAILURE, b""  | 
                    |
| 4310 | 
                        - )  | 
                    |
| 4311 | 
                        -  | 
                    |
| 4312 | 
                        - monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", func)  | 
                    |
| 4313 | 
                        - result = runner.invoke(  | 
                    |
| 4314 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4315 | 
                        - ["--key", "--config"],  | 
                    |
| 4316 | 
                        - catch_exceptions=False,  | 
                    |
| 4317 | 
                        - )  | 
                    |
| 4318 | 
                        - assert result.error_exit(error="refused to"), (  | 
                    |
| 4319 | 
                        - "expected error exit and known error message"  | 
                    |
| 4320 | 
                        - )  | 
                    |
| 4321 | 
                        -  | 
                    |
| 4322 | 
                        - def test_226_no_arguments(self) -> None:  | 
                    |
| 4323 | 
                        - """Calling `derivepassphrase vault` without any arguments fails."""  | 
                    |
| 4324 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4325 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4326 | 
                        - # with-statements.  | 
                    |
| 4327 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4328 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4329 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4330 | 
                        - stack.enter_context(  | 
                    |
| 4331 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 4332 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4333 | 
                        - runner=runner,  | 
                    |
| 4334 | 
                        - )  | 
                    |
| 4335 | 
                        - )  | 
                    |
| 4336 | 
                        - result = runner.invoke(  | 
                    |
| 4337 | 
                        - cli.derivepassphrase_vault, [], catch_exceptions=False  | 
                    |
| 4338 | 
                        - )  | 
                    |
| 4339 | 
                        - assert result.error_exit(  | 
                    |
| 4340 | 
                        - error="Deriving a passphrase requires a SERVICE"  | 
                    |
| 4341 | 
                        - ), "expected error exit and known error message"  | 
                    |
| 4342 | 
                        -  | 
                    |
| 4343 | 
                        - def test_226a_no_passphrase_or_key(  | 
                    |
| 4344 | 
                        - self,  | 
                    |
| 4345 | 
                        - ) -> None:  | 
                    |
| 4346 | 
                        - """Deriving a passphrase without a passphrase or key fails."""  | 
                    |
| 4347 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4348 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4349 | 
                        - # with-statements.  | 
                    |
| 4350 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4351 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4352 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4353 | 
                        - stack.enter_context(  | 
                    |
| 4354 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 4355 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4356 | 
                        - runner=runner,  | 
                    |
| 4357 | 
                        - )  | 
                    |
| 4358 | 
                        - )  | 
                    |
| 4359 | 
                        - result = runner.invoke(  | 
                    |
| 4360 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4361 | 
                        - ["--", DUMMY_SERVICE],  | 
                    |
| 4362 | 
                        - catch_exceptions=False,  | 
                    |
| 1733 | 
                        + @Parametrize.MODERN_EDITOR_INTERFACE  | 
                    |
| 1734 | 
                        + @hypothesis.settings(  | 
                    |
| 1735 | 
                        + suppress_health_check=[  | 
                    |
| 1736 | 
                        + *hypothesis.settings().suppress_health_check,  | 
                    |
| 1737 | 
                        + hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 1738 | 
                        + ],  | 
                    |
| 4363 | 1739 | 
                        )  | 
                    
| 4364 | 
                        - assert result.error_exit(error="No passphrase or key was given"), (  | 
                    |
| 4365 | 
                        - "expected error exit and known error message"  | 
                    |
| 1740 | 
                        + @hypothesis.given(  | 
                    |
| 1741 | 
                        + notes=strategies.text(  | 
                    |
| 1742 | 
                        + strategies.characters(  | 
                    |
| 1743 | 
                        + min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 1744 | 
                        + ),  | 
                    |
| 1745 | 
                        + min_size=1,  | 
                    |
| 1746 | 
                        + max_size=512,  | 
                    |
| 1747 | 
                        + ).filter(str.strip),  | 
                    |
| 4366 | 1748 | 
                        )  | 
                    
| 4367 | 
                        -  | 
                    |
| 4368 | 
                        - def test_230_config_directory_nonexistant(  | 
                    |
| 1749 | 
                        + def test_220_edit_notes_successfully(  | 
                    |
| 4369 | 1750 | 
                        self,  | 
                    
| 1751 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 1752 | 
                        + modern_editor_interface: bool,  | 
                    |
| 1753 | 
                        + notes: str,  | 
                    |
| 4370 | 1754 | 
                        ) -> None:  | 
                    
| 4371 | 
                        - """Running without an existing config directory works.  | 
                    |
| 4372 | 
                        -  | 
                    |
| 4373 | 
                        - This is a regression test; see [issue\u00a0#6][] for context.  | 
                    |
| 4374 | 
                        -  | 
                    |
| 4375 | 
                        - [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6  | 
                    |
| 1755 | 
                        + """Editing notes works."""  | 
                    |
| 1756 | 
                        + marker = cli_messages.TranslatedString(  | 
                    |
| 1757 | 
                        + cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER  | 
                    |
| 1758 | 
                        + )  | 
                    |
| 1759 | 
                        + edit_result = f"""  | 
                    |
| 4376 | 1760 | 
                         | 
                    
| 1761 | 
                        +{marker}
                       | 
                    |
| 1762 | 
                        +{notes}
                       | 
                    |
| 4377 | 1763 | 
                        """  | 
                    
| 1764 | 
                        + # Reset caplog between hypothesis runs.  | 
                    |
| 1765 | 
                        + caplog.clear()  | 
                    |
| 4378 | 1766 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4379 | 1767 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4380 | 1768 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4382,45 +1770,89 @@ class TestCLI:  | 
                  
| 4382 | 1770 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 4383 | 1771 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 4384 | 1772 | 
                        stack.enter_context(  | 
                    
| 4385 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1773 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 4386 | 1774 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4387 | 1775 | 
                        runner=runner,  | 
                    
| 1776 | 
                        +                    vault_config={
                       | 
                    |
| 1777 | 
                        +                        "global": {"phrase": "abc"},
                       | 
                    |
| 1778 | 
                        +                        "services": {"sv": {"notes": "Contents go here"}},
                       | 
                    |
| 1779 | 
                        + },  | 
                    |
| 4388 | 1780 | 
                        )  | 
                    
| 4389 | 1781 | 
                        )  | 
                    
| 4390 | 
                        - with contextlib.suppress(FileNotFoundError):  | 
                    |
| 4391 | 
                        - shutil.rmtree(cli_helpers.config_filename(subsystem=None))  | 
                    |
| 1782 | 
                        + notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 1783 | 
                        + subsystem="notes backup"  | 
                    |
| 1784 | 
                        + )  | 
                    |
| 1785 | 
                        + notes_backup_file.write_text(  | 
                    |
| 1786 | 
                        + "These backup notes are left over from the previous session.",  | 
                    |
| 1787 | 
                        + encoding="UTF-8",  | 
                    |
| 1788 | 
                        + )  | 
                    |
| 1789 | 
                        + monkeypatch.setattr(click, "edit", lambda *_a, **_kw: edit_result)  | 
                    |
| 4392 | 1790 | 
                        result = runner.invoke(  | 
                    
| 4393 | 1791 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4394 | 
                        - ["--config", "-p"],  | 
                    |
| 1792 | 
                        + [  | 
                    |
| 1793 | 
                        + "--config",  | 
                    |
| 1794 | 
                        + "--notes",  | 
                    |
| 1795 | 
                        + "--modern-editor-interface"  | 
                    |
| 1796 | 
                        + if modern_editor_interface  | 
                    |
| 1797 | 
                        + else "--vault-legacy-editor-interface",  | 
                    |
| 1798 | 
                        + "--",  | 
                    |
| 1799 | 
                        + "sv",  | 
                    |
| 1800 | 
                        + ],  | 
                    |
| 4395 | 1801 | 
                        catch_exceptions=False,  | 
                    
| 4396 | 
                        - input="abc\n",  | 
                    |
| 4397 | 1802 | 
                        )  | 
                    
| 4398 | 1803 | 
                        assert result.clean_exit(), "expected clean exit"  | 
                    
| 4399 | 
                        - assert result.stderr == "Passphrase:", (  | 
                    |
| 4400 | 
                        - "program unexpectedly failed?!"  | 
                    |
| 1804 | 
                        + assert all(map(is_warning_line, result.stderr.splitlines(True)))  | 
                    |
| 1805 | 
                        + assert modern_editor_interface or machinery.warning_emitted(  | 
                    |
| 1806 | 
                        + "A backup copy of the old notes was saved",  | 
                    |
| 1807 | 
                        + caplog.record_tuples,  | 
                    |
| 1808 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 1809 | 
                        + assert (  | 
                    |
| 1810 | 
                        + modern_editor_interface  | 
                    |
| 1811 | 
                        + or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 1812 | 
                        + == "Contents go here"  | 
                    |
| 4401 | 1813 | 
                        )  | 
                    
| 4402 | 1814 | 
                        with cli_helpers.config_filename(subsystem="vault").open(  | 
                    
| 4403 | 1815 | 
                        encoding="UTF-8"  | 
                    
| 4404 | 1816 | 
                        ) as infile:  | 
                    
| 4405 | 
                        - config_readback = json.load(infile)  | 
                    |
| 4406 | 
                        -            assert config_readback == {
                       | 
                    |
| 1817 | 
                        + config = json.load(infile)  | 
                    |
| 1818 | 
                        +            assert config == {
                       | 
                    |
| 4407 | 1819 | 
                                         "global": {"phrase": "abc"},
                       | 
                    
| 4408 | 
                        -                "services": {},
                       | 
                    |
| 4409 | 
                        - }, "config mismatch"  | 
                    |
| 1820 | 
                        +                "services": {
                       | 
                    |
| 1821 | 
                        +                    "sv": {
                       | 
                    |
| 1822 | 
                        + "notes": notes.strip()  | 
                    |
| 1823 | 
                        + if modern_editor_interface  | 
                    |
| 1824 | 
                        + else edit_result.strip()  | 
                    |
| 1825 | 
                        + }  | 
                    |
| 1826 | 
                        + },  | 
                    |
| 1827 | 
                        + }  | 
                    |
| 4410 | 1828 | 
                         | 
                    
| 4411 | 
                        - def test_230a_config_directory_not_a_file(  | 
                    |
| 1829 | 
                        + @Parametrize.NOOP_EDIT_FUNCS  | 
                    |
| 1830 | 
                        + @hypothesis.given(  | 
                    |
| 1831 | 
                        + notes=strategies.text(  | 
                    |
| 1832 | 
                        + strategies.characters(  | 
                    |
| 1833 | 
                        + min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 1834 | 
                        + ),  | 
                    |
| 1835 | 
                        + min_size=1,  | 
                    |
| 1836 | 
                        + max_size=512,  | 
                    |
| 1837 | 
                        + ).filter(str.strip),  | 
                    |
| 1838 | 
                        + )  | 
                    |
| 1839 | 
                        + def test_221_edit_notes_noop(  | 
                    |
| 4412 | 1840 | 
                        self,  | 
                    
| 1841 | 
                        + edit_func_name: Literal["empty", "space"],  | 
                    |
| 1842 | 
                        + modern_editor_interface: bool,  | 
                    |
| 1843 | 
                        + notes: str,  | 
                    |
| 4413 | 1844 | 
                        ) -> None:  | 
                    
| 4414 | 
                        - """Erroring without an existing config directory errors normally.  | 
                    |
| 4415 | 
                        -  | 
                    |
| 4416 | 
                        - That is, the missing configuration directory does not cause any  | 
                    |
| 4417 | 
                        - errors by itself.  | 
                    |
| 1845 | 
                        + """Abandoning edited notes works."""  | 
                    |
| 4418 | 1846 | 
                         | 
                    
| 4419 | 
                        - This is a regression test; see [issue\u00a0#6][] for context.  | 
                    |
| 1847 | 
                        + def empty(text: str, *_args: Any, **_kwargs: Any) -> str:  | 
                    |
| 1848 | 
                        + del text  | 
                    |
| 1849 | 
                        + return ""  | 
                    |
| 4420 | 1850 | 
                         | 
                    
| 4421 | 
                        - [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6  | 
                    |
| 1851 | 
                        + def space(text: str, *_args: Any, **_kwargs: Any) -> str:  | 
                    |
| 1852 | 
                        + del text  | 
                    |
| 1853 | 
                        + return " " + notes.strip() + "\n\n\n\n\n\n"  | 
                    |
| 4422 | 1854 | 
                         | 
                    
| 4423 | 
                        - """  | 
                    |
| 1855 | 
                        +        edit_funcs = {"empty": empty, "space": space}
                       | 
                    |
| 4424 | 1856 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4425 | 1857 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4426 | 1858 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4428,77 +1860,88 @@ class TestCLI:  | 
                  
| 4428 | 1860 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 4429 | 1861 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 4430 | 1862 | 
                        stack.enter_context(  | 
                    
| 4431 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 1863 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 4432 | 1864 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4433 | 1865 | 
                        runner=runner,  | 
                    
| 1866 | 
                        +                    vault_config={
                       | 
                    |
| 1867 | 
                        +                        "global": {"phrase": "abc"},
                       | 
                    |
| 1868 | 
                        +                        "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 1869 | 
                        + },  | 
                    |
| 4434 | 1870 | 
                        )  | 
                    
| 4435 | 1871 | 
                        )  | 
                    
| 4436 | 
                        - save_config_ = cli_helpers.save_config  | 
                    |
| 4437 | 
                        -  | 
                    |
| 4438 | 
                        - def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any:  | 
                    |
| 4439 | 
                        - config_dir = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 4440 | 
                        - with contextlib.suppress(FileNotFoundError):  | 
                    |
| 4441 | 
                        - shutil.rmtree(config_dir)  | 
                    |
| 4442 | 
                        -                config_dir.write_text("Obstruction!!\n")
                       | 
                    |
| 4443 | 
                        - monkeypatch.setattr(cli_helpers, "save_config", save_config_)  | 
                    |
| 4444 | 
                        - return save_config_(*args, **kwargs)  | 
                    |
| 4445 | 
                        -  | 
                    |
| 4446 | 
                        - monkeypatch.setattr(  | 
                    |
| 4447 | 
                        - cli_helpers, "save_config", obstruct_config_saving  | 
                    |
| 1872 | 
                        + notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 1873 | 
                        + subsystem="notes backup"  | 
                    |
| 1874 | 
                        + )  | 
                    |
| 1875 | 
                        + notes_backup_file.write_text(  | 
                    |
| 1876 | 
                        + "These backup notes are left over from the previous session.",  | 
                    |
| 1877 | 
                        + encoding="UTF-8",  | 
                    |
| 4448 | 1878 | 
                        )  | 
                    
| 1879 | 
                        + monkeypatch.setattr(click, "edit", edit_funcs[edit_func_name])  | 
                    |
| 4449 | 1880 | 
                        result = runner.invoke(  | 
                    
| 4450 | 1881 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4451 | 
                        - ["--config", "-p"],  | 
                    |
| 1882 | 
                        + [  | 
                    |
| 1883 | 
                        + "--config",  | 
                    |
| 1884 | 
                        + "--notes",  | 
                    |
| 1885 | 
                        + "--modern-editor-interface"  | 
                    |
| 1886 | 
                        + if modern_editor_interface  | 
                    |
| 1887 | 
                        + else "--vault-legacy-editor-interface",  | 
                    |
| 1888 | 
                        + "--",  | 
                    |
| 1889 | 
                        + "sv",  | 
                    |
| 1890 | 
                        + ],  | 
                    |
| 4452 | 1891 | 
                        catch_exceptions=False,  | 
                    
| 4453 | 
                        - input="abc\n",  | 
                    |
| 4454 | 
                        - )  | 
                    |
| 4455 | 
                        - assert result.error_exit(error="Cannot store vault settings:"), (  | 
                    |
| 4456 | 
                        - "expected error exit and known error message"  | 
                    |
| 4457 | 
                        - )  | 
                    |
| 4458 | 
                        -  | 
                    |
| 4459 | 
                        - def test_230b_store_config_custom_error(  | 
                    |
| 4460 | 
                        - self,  | 
                    |
| 4461 | 
                        - ) -> None:  | 
                    |
| 4462 | 
                        - """Storing the configuration reacts even to weird errors."""  | 
                    |
| 4463 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4464 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4465 | 
                        - # with-statements.  | 
                    |
| 4466 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4467 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4468 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4469 | 
                        - stack.enter_context(  | 
                    |
| 4470 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 4471 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4472 | 
                        - runner=runner,  | 
                    |
| 4473 | 1892 | 
                        )  | 
                    
| 1893 | 
                        + assert result.clean_exit(empty_stderr=True) or result.error_exit(  | 
                    |
| 1894 | 
                        + error="the user aborted the request"  | 
                    |
| 1895 | 
                        + ), "expected clean exit"  | 
                    |
| 1896 | 
                        + assert (  | 
                    |
| 1897 | 
                        + modern_editor_interface  | 
                    |
| 1898 | 
                        + or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 1899 | 
                        + == "These backup notes are left over from the previous session."  | 
                    |
| 4474 | 1900 | 
                        )  | 
                    
| 4475 | 
                        - custom_error = "custom error message"  | 
                    |
| 4476 | 
                        -  | 
                    |
| 4477 | 
                        - def raiser(config: Any) -> None:  | 
                    |
| 4478 | 
                        - del config  | 
                    |
| 4479 | 
                        - raise RuntimeError(custom_error)  | 
                    |
| 1901 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 1902 | 
                        + encoding="UTF-8"  | 
                    |
| 1903 | 
                        + ) as infile:  | 
                    |
| 1904 | 
                        + config = json.load(infile)  | 
                    |
| 1905 | 
                        +            assert config == {
                       | 
                    |
| 1906 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 1907 | 
                        +                "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 1908 | 
                        + }  | 
                    |
| 4480 | 1909 | 
                         | 
                    
| 4481 | 
                        - monkeypatch.setattr(cli_helpers, "save_config", raiser)  | 
                    |
| 4482 | 
                        - result = runner.invoke(  | 
                    |
| 4483 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 4484 | 
                        - ["--config", "-p"],  | 
                    |
| 4485 | 
                        - catch_exceptions=False,  | 
                    |
| 4486 | 
                        - input="abc\n",  | 
                    |
| 1910 | 
                        + # TODO(the-13th-letter): Keep this behavior or not, with or without  | 
                    |
| 1911 | 
                        + # warning?  | 
                    |
| 1912 | 
                        + @Parametrize.MODERN_EDITOR_INTERFACE  | 
                    |
| 1913 | 
                        + @hypothesis.settings(  | 
                    |
| 1914 | 
                        + suppress_health_check=[  | 
                    |
| 1915 | 
                        + *hypothesis.settings().suppress_health_check,  | 
                    |
| 1916 | 
                        + hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 1917 | 
                        + ],  | 
                    |
| 4487 | 1918 | 
                        )  | 
                    
| 4488 | 
                        - assert result.error_exit(error=custom_error), (  | 
                    |
| 4489 | 
                        - "expected error exit and known error message"  | 
                    |
| 1919 | 
                        + @hypothesis.given(  | 
                    |
| 1920 | 
                        + notes=strategies.text(  | 
                    |
| 1921 | 
                        + strategies.characters(  | 
                    |
| 1922 | 
                        + min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 1923 | 
                        + ),  | 
                    |
| 1924 | 
                        + min_size=1,  | 
                    |
| 1925 | 
                        + max_size=512,  | 
                    |
| 1926 | 
                        + ).filter(str.strip),  | 
                    |
| 4490 | 1927 | 
                        )  | 
                    
| 4491 | 
                        -  | 
                    |
| 4492 | 
                        - @Parametrize.UNICODE_NORMALIZATION_WARNING_INPUTS  | 
                    |
| 4493 | 
                        - def test_300_unicode_normalization_form_warning(  | 
                    |
| 1928 | 
                        + def test_222_edit_notes_marker_removed(  | 
                    |
| 4494 | 1929 | 
                        self,  | 
                    
| 4495 | 1930 | 
                        caplog: pytest.LogCaptureFixture,  | 
                    
| 4496 | 
                        - main_config: str,  | 
                    |
| 4497 | 
                        - command_line: list[str],  | 
                    |
| 4498 | 
                        - input: str | None,  | 
                    |
| 4499 | 
                        - warning_message: str,  | 
                    |
| 1931 | 
                        + modern_editor_interface: bool,  | 
                    |
| 1932 | 
                        + notes: str,  | 
                    |
| 4500 | 1933 | 
                        ) -> None:  | 
                    
| 4501 | 
                        - """Using unnormalized Unicode passphrases warns."""  | 
                    |
| 1934 | 
                        + """Removing the notes marker still saves the notes.  | 
                    |
| 1935 | 
                        +  | 
                    |
| 1936 | 
                        + TODO: Keep this behavior or not, with or without warning?  | 
                    |
| 1937 | 
                        +  | 
                    |
| 1938 | 
                        + """  | 
                    |
| 1939 | 
                        + notes_marker = cli_messages.TranslatedString(  | 
                    |
| 1940 | 
                        + cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER  | 
                    |
| 1941 | 
                        + )  | 
                    |
| 1942 | 
                        + hypothesis.assume(str(notes_marker) not in notes.strip())  | 
                    |
| 1943 | 
                        + # Reset caplog between hypothesis runs.  | 
                    |
| 1944 | 
                        + caplog.clear()  | 
                    |
| 4502 | 1945 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4503 | 1946 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4504 | 1947 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4510,33 +1953,72 @@ class TestCLI:  | 
                  
| 4510 | 1953 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4511 | 1954 | 
                        runner=runner,  | 
                    
| 4512 | 1955 | 
                                             vault_config={
                       | 
                    
| 4513 | 
                        -                        "services": {
                       | 
                    |
| 4514 | 
                        - DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()  | 
                    |
| 4515 | 
                        - }  | 
                    |
| 1956 | 
                        +                        "global": {"phrase": "abc"},
                       | 
                    |
| 1957 | 
                        +                        "services": {"sv": {"notes": "Contents go here"}},
                       | 
                    |
| 4516 | 1958 | 
                        },  | 
                    
| 4517 | 
                        - main_config_str=main_config,  | 
                    |
| 4518 | 1959 | 
                        )  | 
                    
| 4519 | 1960 | 
                        )  | 
                    
| 1961 | 
                        + notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 1962 | 
                        + subsystem="notes backup"  | 
                    |
| 1963 | 
                        + )  | 
                    |
| 1964 | 
                        + notes_backup_file.write_text(  | 
                    |
| 1965 | 
                        + "These backup notes are left over from the previous session.",  | 
                    |
| 1966 | 
                        + encoding="UTF-8",  | 
                    |
| 1967 | 
                        + )  | 
                    |
| 1968 | 
                        + monkeypatch.setattr(click, "edit", lambda *_a, **_kw: notes)  | 
                    |
| 4520 | 1969 | 
                        result = runner.invoke(  | 
                    
| 4521 | 1970 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4522 | 
                        - ["--debug", *command_line],  | 
                    |
| 1971 | 
                        + [  | 
                    |
| 1972 | 
                        + "--config",  | 
                    |
| 1973 | 
                        + "--notes",  | 
                    |
| 1974 | 
                        + "--modern-editor-interface"  | 
                    |
| 1975 | 
                        + if modern_editor_interface  | 
                    |
| 1976 | 
                        + else "--vault-legacy-editor-interface",  | 
                    |
| 1977 | 
                        + "--",  | 
                    |
| 1978 | 
                        + "sv",  | 
                    |
| 1979 | 
                        + ],  | 
                    |
| 4523 | 1980 | 
                        catch_exceptions=False,  | 
                    
| 4524 | 
                        - input=input,  | 
                    |
| 4525 | 1981 | 
                        )  | 
                    
| 4526 | 1982 | 
                        assert result.clean_exit(), "expected clean exit"  | 
                    
| 4527 | 
                        - assert machinery.warning_emitted(  | 
                    |
| 4528 | 
                        - warning_message, caplog.record_tuples  | 
                    |
| 1983 | 
                        + assert not result.stderr or all(  | 
                    |
| 1984 | 
                        + map(is_warning_line, result.stderr.splitlines(True))  | 
                    |
| 1985 | 
                        + )  | 
                    |
| 1986 | 
                        + assert not caplog.record_tuples or machinery.warning_emitted(  | 
                    |
| 1987 | 
                        + "A backup copy of the old notes was saved",  | 
                    |
| 1988 | 
                        + caplog.record_tuples,  | 
                    |
| 4529 | 1989 | 
                        ), "expected known warning message in stderr"  | 
                    
| 1990 | 
                        + assert (  | 
                    |
| 1991 | 
                        + modern_editor_interface  | 
                    |
| 1992 | 
                        + or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 1993 | 
                        + == "Contents go here"  | 
                    |
| 1994 | 
                        + )  | 
                    |
| 1995 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 1996 | 
                        + encoding="UTF-8"  | 
                    |
| 1997 | 
                        + ) as infile:  | 
                    |
| 1998 | 
                        + config = json.load(infile)  | 
                    |
| 1999 | 
                        +            assert config == {
                       | 
                    |
| 2000 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 2001 | 
                        +                "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 2002 | 
                        + }  | 
                    |
| 4530 | 2003 | 
                         | 
                    
| 4531 | 
                        - @Parametrize.UNICODE_NORMALIZATION_ERROR_INPUTS  | 
                    |
| 4532 | 
                        - def test_301_unicode_normalization_form_error(  | 
                    |
| 2004 | 
                        + @hypothesis.given(  | 
                    |
| 2005 | 
                        + notes=strategies.text(  | 
                    |
| 2006 | 
                        + strategies.characters(  | 
                    |
| 2007 | 
                        + min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 2008 | 
                        + ),  | 
                    |
| 2009 | 
                        + min_size=1,  | 
                    |
| 2010 | 
                        + max_size=512,  | 
                    |
| 2011 | 
                        + ).filter(str.strip),  | 
                    |
| 2012 | 
                        + )  | 
                    |
| 2013 | 
                        + def test_223_edit_notes_abort(  | 
                    |
| 4533 | 2014 | 
                        self,  | 
                    
| 4534 | 
                        - main_config: str,  | 
                    |
| 4535 | 
                        - command_line: list[str],  | 
                    |
| 4536 | 
                        - input: str | None,  | 
                    |
| 4537 | 
                        - error_message: str,  | 
                    |
| 2015 | 
                        + notes: str,  | 
                    |
| 4538 | 2016 | 
                        ) -> None:  | 
                    
| 4539 | 
                        - """Using unknown Unicode normalization forms fails."""  | 
                    |
| 2017 | 
                        + """Aborting editing notes works.  | 
                    |
| 2018 | 
                        +  | 
                    |
| 2019 | 
                        + Aborting is only supported with the modern editor interface.  | 
                    |
| 2020 | 
                        +  | 
                    |
| 2021 | 
                        + """  | 
                    |
| 4540 | 2022 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4541 | 2023 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4542 | 2024 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4548,32 +2030,43 @@ class TestCLI:  | 
                  
| 4548 | 2030 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4549 | 2031 | 
                        runner=runner,  | 
                    
| 4550 | 2032 | 
                                             vault_config={
                       | 
                    
| 4551 | 
                        -                        "services": {
                       | 
                    |
| 4552 | 
                        - DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()  | 
                    |
| 4553 | 
                        - }  | 
                    |
| 2033 | 
                        +                        "global": {"phrase": "abc"},
                       | 
                    |
| 2034 | 
                        +                        "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 4554 | 2035 | 
                        },  | 
                    
| 4555 | 
                        - main_config_str=main_config,  | 
                    |
| 4556 | 2036 | 
                        )  | 
                    
| 4557 | 2037 | 
                        )  | 
                    
| 2038 | 
                        + monkeypatch.setattr(click, "edit", lambda *_a, **_kw: "")  | 
                    |
| 4558 | 2039 | 
                        result = runner.invoke(  | 
                    
| 4559 | 2040 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4560 | 
                        - command_line,  | 
                    |
| 2041 | 
                        + [  | 
                    |
| 2042 | 
                        + "--config",  | 
                    |
| 2043 | 
                        + "--notes",  | 
                    |
| 2044 | 
                        + "--modern-editor-interface",  | 
                    |
| 2045 | 
                        + "--",  | 
                    |
| 2046 | 
                        + "sv",  | 
                    |
| 2047 | 
                        + ],  | 
                    |
| 4561 | 2048 | 
                        catch_exceptions=False,  | 
                    
| 4562 | 
                        - input=input,  | 
                    |
| 4563 | 2049 | 
                        )  | 
                    
| 4564 | 
                        - assert result.error_exit(  | 
                    |
| 4565 | 
                        - error="The user configuration file is invalid."  | 
                    |
| 4566 | 
                        - ), "expected error exit and known error message"  | 
                    |
| 4567 | 
                        - assert result.error_exit(error=error_message), (  | 
                    |
| 4568 | 
                        - "expected error exit and known error message"  | 
                    |
| 2050 | 
                        + assert result.error_exit(error="the user aborted the request"), (  | 
                    |
| 2051 | 
                        + "expected known error message"  | 
                    |
| 4569 | 2052 | 
                        )  | 
                    
| 2053 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 2054 | 
                        + encoding="UTF-8"  | 
                    |
| 2055 | 
                        + ) as infile:  | 
                    |
| 2056 | 
                        + config = json.load(infile)  | 
                    |
| 2057 | 
                        +            assert config == {
                       | 
                    |
| 2058 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 2059 | 
                        +                "services": {"sv": {"notes": notes.strip()}},
                       | 
                    |
| 2060 | 
                        + }  | 
                    |
| 4570 | 2061 | 
                         | 
                    
| 4571 | 
                        - @Parametrize.UNICODE_NORMALIZATION_COMMAND_LINES  | 
                    |
| 4572 | 
                        - def test_301a_unicode_normalization_form_error_from_stored_config(  | 
                    |
| 2062 | 
                        + def test_223a_edit_empty_notes_abort(  | 
                    |
| 4573 | 2063 | 
                        self,  | 
                    
| 4574 | 
                        - command_line: list[str],  | 
                    |
| 4575 | 2064 | 
                        ) -> None:  | 
                    
| 4576 | 
                        - """Using unknown Unicode normalization forms in the config fails."""  | 
                    |
| 2065 | 
                        + """Aborting editing notes works even if no notes are stored yet.  | 
                    |
| 2066 | 
                        +  | 
                    |
| 2067 | 
                        + Aborting is only supported with the modern editor interface.  | 
                    |
| 2068 | 
                        +  | 
                    |
| 2069 | 
                        + """  | 
                    |
| 4577 | 2070 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4578 | 2071 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4579 | 2072 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4585,35 +2078,66 @@ class TestCLI:  | 
                  
| 4585 | 2078 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4586 | 2079 | 
                        runner=runner,  | 
                    
| 4587 | 2080 | 
                                             vault_config={
                       | 
                    
| 4588 | 
                        -                        "services": {
                       | 
                    |
| 4589 | 
                        - DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()  | 
                    |
| 4590 | 
                        - }  | 
                    |
| 2081 | 
                        +                        "global": {"phrase": "abc"},
                       | 
                    |
| 2082 | 
                        +                        "services": {},
                       | 
                    |
| 4591 | 2083 | 
                        },  | 
                    
| 4592 | 
                        - main_config_str=(  | 
                    |
| 4593 | 
                        - "[vault]\ndefault-unicode-normalization-form = 'XXX'\n"  | 
                    |
| 4594 | 
                        - ),  | 
                    |
| 4595 | 2084 | 
                        )  | 
                    
| 4596 | 2085 | 
                        )  | 
                    
| 2086 | 
                        + monkeypatch.setattr(click, "edit", lambda *_a, **_kw: "")  | 
                    |
| 4597 | 2087 | 
                        result = runner.invoke(  | 
                    
| 4598 | 2088 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4599 | 
                        - command_line,  | 
                    |
| 4600 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 2089 | 
                        + [  | 
                    |
| 2090 | 
                        + "--config",  | 
                    |
| 2091 | 
                        + "--notes",  | 
                    |
| 2092 | 
                        + "--modern-editor-interface",  | 
                    |
| 2093 | 
                        + "--",  | 
                    |
| 2094 | 
                        + "sv",  | 
                    |
| 2095 | 
                        + ],  | 
                    |
| 4601 | 2096 | 
                        catch_exceptions=False,  | 
                    
| 4602 | 2097 | 
                        )  | 
                    
| 4603 | 
                        - assert result.error_exit(  | 
                    |
| 4604 | 
                        - error="The user configuration file is invalid."  | 
                    |
| 4605 | 
                        - ), "expected error exit and known error message"  | 
                    |
| 4606 | 
                        - assert result.error_exit(  | 
                    |
| 4607 | 
                        - error=(  | 
                    |
| 4608 | 
                        - "Invalid value 'XXX' for config key "  | 
                    |
| 4609 | 
                        - "vault.default-unicode-normalization-form"  | 
                    |
| 4610 | 
                        - ),  | 
                    |
| 4611 | 
                        - ), "expected error exit and known error message"  | 
                    |
| 2098 | 
                        + assert result.error_exit(error="the user aborted the request"), (  | 
                    |
| 2099 | 
                        + "expected known error message"  | 
                    |
| 2100 | 
                        + )  | 
                    |
| 2101 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 2102 | 
                        + encoding="UTF-8"  | 
                    |
| 2103 | 
                        + ) as infile:  | 
                    |
| 2104 | 
                        + config = json.load(infile)  | 
                    |
| 2105 | 
                        +            assert config == {
                       | 
                    |
| 2106 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 2107 | 
                        +                "services": {},
                       | 
                    |
| 2108 | 
                        + }  | 
                    |
| 4612 | 2109 | 
                         | 
                    
| 4613 | 
                        - def test_310_bad_user_config_file(  | 
                    |
| 2110 | 
                        + @Parametrize.MODERN_EDITOR_INTERFACE  | 
                    |
| 2111 | 
                        + @hypothesis.settings(  | 
                    |
| 2112 | 
                        + suppress_health_check=[  | 
                    |
| 2113 | 
                        + *hypothesis.settings().suppress_health_check,  | 
                    |
| 2114 | 
                        + hypothesis.HealthCheck.function_scoped_fixture,  | 
                    |
| 2115 | 
                        + ],  | 
                    |
| 2116 | 
                        + )  | 
                    |
| 2117 | 
                        + @hypothesis.given(  | 
                    |
| 2118 | 
                        + notes=strategies.text(  | 
                    |
| 2119 | 
                        + strategies.characters(  | 
                    |
| 2120 | 
                        + min_codepoint=32, max_codepoint=126, include_characters="\n"  | 
                    |
| 2121 | 
                        + ),  | 
                    |
| 2122 | 
                        + max_size=512,  | 
                    |
| 2123 | 
                        + ),  | 
                    |
| 2124 | 
                        + )  | 
                    |
| 2125 | 
                        + def test_223b_edit_notes_fail_config_option_missing(  | 
                    |
| 4614 | 2126 | 
                        self,  | 
                    
| 2127 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 2128 | 
                        + modern_editor_interface: bool,  | 
                    |
| 2129 | 
                        + notes: str,  | 
                    |
| 4615 | 2130 | 
                        ) -> None:  | 
                    
| 4616 | 
                        - """Loading a user configuration file in an invalid format fails."""  | 
                    |
| 2131 | 
                        + """Editing notes fails (and warns) if `--config` is missing."""  | 
                    |
| 2132 | 
                        +        maybe_notes = {"notes": notes.strip()} if notes.strip() else {}
                       | 
                    |
| 2133 | 
                        +        vault_config = {
                       | 
                    |
| 2134 | 
                        +            "global": {"phrase": DUMMY_PASSPHRASE},
                       | 
                    |
| 2135 | 
                        +            "services": {
                       | 
                    |
| 2136 | 
                        +                DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 2137 | 
                        + },  | 
                    |
| 2138 | 
                        + }  | 
                    |
| 2139 | 
                        + # Reset caplog between hypothesis runs.  | 
                    |
| 2140 | 
                        + caplog.clear()  | 
                    |
| 4617 | 2141 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4618 | 2142 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4619 | 2143 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4624,24 +2148,73 @@ class TestCLI:  | 
                  
| 4624 | 2148 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 4625 | 2149 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4626 | 2150 | 
                        runner=runner,  | 
                    
| 4627 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 4628 | 
                        - main_config_str="This file is not valid TOML.\n",  | 
                    |
| 2151 | 
                        + vault_config=vault_config,  | 
                    |
| 2152 | 
                        + )  | 
                    |
| 2153 | 
                        + )  | 
                    |
| 2154 | 
                        + EDIT_ATTEMPTED = "edit attempted!" # noqa: N806  | 
                    |
| 2155 | 
                        +  | 
                    |
| 2156 | 
                        + def raiser(*_args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 2157 | 
                        + pytest.fail(EDIT_ATTEMPTED)  | 
                    |
| 2158 | 
                        +  | 
                    |
| 2159 | 
                        + notes_backup_file = cli_helpers.config_filename(  | 
                    |
| 2160 | 
                        + subsystem="notes backup"  | 
                    |
| 4629 | 2161 | 
                        )  | 
                    
| 2162 | 
                        + notes_backup_file.write_text(  | 
                    |
| 2163 | 
                        + "These backup notes are left over from the previous session.",  | 
                    |
| 2164 | 
                        + encoding="UTF-8",  | 
                    |
| 4630 | 2165 | 
                        )  | 
                    
| 2166 | 
                        + monkeypatch.setattr(click, "edit", raiser)  | 
                    |
| 4631 | 2167 | 
                        result = runner.invoke(  | 
                    
| 4632 | 2168 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4633 | 
                        - ["--phrase", "--", DUMMY_SERVICE],  | 
                    |
| 4634 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 2169 | 
                        + [  | 
                    |
| 2170 | 
                        + "--notes",  | 
                    |
| 2171 | 
                        + "--modern-editor-interface"  | 
                    |
| 2172 | 
                        + if modern_editor_interface  | 
                    |
| 2173 | 
                        + else "--vault-legacy-editor-interface",  | 
                    |
| 2174 | 
                        + "--",  | 
                    |
| 2175 | 
                        + DUMMY_SERVICE,  | 
                    |
| 2176 | 
                        + ],  | 
                    |
| 4635 | 2177 | 
                        catch_exceptions=False,  | 
                    
| 4636 | 2178 | 
                        )  | 
                    
| 4637 | 
                        - assert result.error_exit(error="Cannot load user config:"), (  | 
                    |
| 4638 | 
                        - "expected error exit and known error message"  | 
                    |
| 2179 | 
                        + assert result.clean_exit(  | 
                    |
| 2180 | 
                        +                output=DUMMY_RESULT_PASSPHRASE.decode("ascii")
                       | 
                    |
| 2181 | 
                        + ), "expected clean exit"  | 
                    |
| 2182 | 
                        + assert result.stderr  | 
                    |
| 2183 | 
                        + assert notes.strip() in result.stderr  | 
                    |
| 2184 | 
                        + assert all(  | 
                    |
| 2185 | 
                        + is_warning_line(line)  | 
                    |
| 2186 | 
                        + for line in result.stderr.splitlines(True)  | 
                    |
| 2187 | 
                        +                if line.startswith(f"{cli.PROG_NAME}: ")
                       | 
                    |
| 2188 | 
                        + )  | 
                    |
| 2189 | 
                        + assert machinery.warning_emitted(  | 
                    |
| 2190 | 
                        + "Specifying --notes without --config is ineffective. "  | 
                    |
| 2191 | 
                        + "No notes will be edited.",  | 
                    |
| 2192 | 
                        + caplog.record_tuples,  | 
                    |
| 2193 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 2194 | 
                        + assert (  | 
                    |
| 2195 | 
                        + modern_editor_interface  | 
                    |
| 2196 | 
                        + or notes_backup_file.read_text(encoding="UTF-8")  | 
                    |
| 2197 | 
                        + == "These backup notes are left over from the previous session."  | 
                    |
| 4639 | 2198 | 
                        )  | 
                    
| 2199 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 2200 | 
                        + encoding="UTF-8"  | 
                    |
| 2201 | 
                        + ) as infile:  | 
                    |
| 2202 | 
                        + config = json.load(infile)  | 
                    |
| 2203 | 
                        + assert config == vault_config  | 
                    |
| 4640 | 2204 | 
                         | 
                    
| 4641 | 
                        - def test_311_bad_user_config_is_a_directory(  | 
                    |
| 2205 | 
                        + @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG  | 
                    |
| 2206 | 
                        + def test_224_store_config_good(  | 
                    |
| 4642 | 2207 | 
                        self,  | 
                    
| 2208 | 
                        + command_line: list[str],  | 
                    |
| 2209 | 
                        + input: str,  | 
                    |
| 2210 | 
                        + result_config: Any,  | 
                    |
| 4643 | 2211 | 
                        ) -> None:  | 
                    
| 4644 | 
                        - """Loading a user configuration file in an invalid format fails."""  | 
                    |
| 2212 | 
                        + """Storing valid settings via `--config` works.  | 
                    |
| 2213 | 
                        +  | 
                    |
| 2214 | 
                        + The format also contains embedded newlines and indentation to make  | 
                    |
| 2215 | 
                        + the config more readable.  | 
                    |
| 2216 | 
                        +  | 
                    |
| 2217 | 
                        + """  | 
                    |
| 4645 | 2218 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4646 | 2219 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4647 | 2220 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4652,30 +2225,38 @@ class TestCLI:  | 
                  
| 4652 | 2225 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 4653 | 2226 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4654 | 2227 | 
                        runner=runner,  | 
                    
| 4655 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 4656 | 
                        - main_config_str="",  | 
                    |
| 2228 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4657 | 2229 | 
                        )  | 
                    
| 4658 | 2230 | 
                        )  | 
                    
| 4659 | 
                        - user_config = cli_helpers.config_filename(  | 
                    |
| 4660 | 
                        - subsystem="user configuration"  | 
                    |
| 2231 | 
                        + monkeypatch.setattr(  | 
                    |
| 2232 | 
                        + cli_helpers,  | 
                    |
| 2233 | 
                        + "get_suitable_ssh_keys",  | 
                    |
| 2234 | 
                        + callables.suitable_ssh_keys,  | 
                    |
| 4661 | 2235 | 
                        )  | 
                    
| 4662 | 
                        - user_config.unlink()  | 
                    |
| 4663 | 
                        - user_config.mkdir(parents=True, exist_ok=True)  | 
                    |
| 4664 | 2236 | 
                        result = runner.invoke(  | 
                    
| 4665 | 2237 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4666 | 
                        - ["--phrase", "--", DUMMY_SERVICE],  | 
                    |
| 4667 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 2238 | 
                        + ["--config", *command_line],  | 
                    |
| 4668 | 2239 | 
                        catch_exceptions=False,  | 
                    
| 2240 | 
                        + input=input,  | 
                    |
| 4669 | 2241 | 
                        )  | 
                    
| 4670 | 
                        - assert result.error_exit(error="Cannot load user config:"), (  | 
                    |
| 4671 | 
                        - "expected error exit and known error message"  | 
                    |
| 2242 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 2243 | 
                        + config_txt = cli_helpers.config_filename(  | 
                    |
| 2244 | 
                        + subsystem="vault"  | 
                    |
| 2245 | 
                        + ).read_text(encoding="UTF-8")  | 
                    |
| 2246 | 
                        + config = json.loads(config_txt)  | 
                    |
| 2247 | 
                        + assert config == result_config, (  | 
                    |
| 2248 | 
                        + "stored config does not match expectation"  | 
                    |
| 4672 | 2249 | 
                        )  | 
                    
| 2250 | 
                        + assert_vault_config_is_indented_and_line_broken(config_txt)  | 
                    |
| 4673 | 2251 | 
                         | 
                    
| 4674 | 
                        - def test_400_missing_af_unix_support(  | 
                    |
| 2252 | 
                        + @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES  | 
                    |
| 2253 | 
                        + def test_225_store_config_fail(  | 
                    |
| 4675 | 2254 | 
                        self,  | 
                    
| 4676 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 2255 | 
                        + command_line: list[str],  | 
                    |
| 2256 | 
                        + input: str,  | 
                    |
| 2257 | 
                        + err_text: str,  | 
                    |
| 4677 | 2258 | 
                        ) -> None:  | 
                    
| 4678 | 
                        - """Querying the SSH agent without `AF_UNIX` support fails."""  | 
                    |
| 2259 | 
                        + """Storing invalid settings via `--config` fails."""  | 
                    |
| 4679 | 2260 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4680 | 2261 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4681 | 2262 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4689,58 +2270,27 @@ class TestCLI:  | 
                  
| 4689 | 2270 | 
                                             vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    
| 4690 | 2271 | 
                        )  | 
                    
| 4691 | 2272 | 
                        )  | 
                    
| 4692 | 
                        - monkeypatch.setenv(  | 
                    |
| 4693 | 
                        - "SSH_AUTH_SOCK", "the value doesn't even matter"  | 
                    |
| 4694 | 
                        - )  | 
                    |
| 4695 | 2273 | 
                        monkeypatch.setattr(  | 
                    
| 4696 | 
                        - ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", ["posix"]  | 
                    |
| 2274 | 
                        + cli_helpers,  | 
                    |
| 2275 | 
                        + "get_suitable_ssh_keys",  | 
                    |
| 2276 | 
                        + callables.suitable_ssh_keys,  | 
                    |
| 4697 | 2277 | 
                        )  | 
                    
| 4698 | 
                        - monkeypatch.delattr(socket, "AF_UNIX", raising=False)  | 
                    |
| 4699 | 2278 | 
                        result = runner.invoke(  | 
                    
| 4700 | 2279 | 
                        cli.derivepassphrase_vault,  | 
                    
| 4701 | 
                        - ["--key", "--config"],  | 
                    |
| 2280 | 
                        + ["--config", *command_line],  | 
                    |
| 4702 | 2281 | 
                        catch_exceptions=False,  | 
                    
| 2282 | 
                        + input=input,  | 
                    |
| 4703 | 2283 | 
                        )  | 
                    
| 4704 | 
                        - assert result.error_exit(  | 
                    |
| 4705 | 
                        - error="does not support communicating with it"  | 
                    |
| 4706 | 
                        - ), "expected error exit and known error message"  | 
                    |
| 4707 | 
                        - assert machinery.warning_emitted(  | 
                    |
| 4708 | 
                        - "Cannot connect to an SSH agent via UNIX domain sockets",  | 
                    |
| 4709 | 
                        - caplog.record_tuples,  | 
                    |
| 4710 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 4711 | 
                        -  | 
                    |
| 4712 | 
                        -  | 
                    |
| 4713 | 
                        -class TestCLIUtils:  | 
                    |
| 4714 | 
                        - """Tests for command-line utility functions."""  | 
                    |
| 4715 | 
                        -  | 
                    |
| 4716 | 
                        - @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 4717 | 
                        - def test_100_load_config(  | 
                    |
| 4718 | 
                        - self,  | 
                    |
| 4719 | 
                        - config: Any,  | 
                    |
| 4720 | 
                        - ) -> None:  | 
                    |
| 4721 | 
                        - """[`cli_helpers.load_config`][] works for valid configurations."""  | 
                    |
| 4722 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 4723 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4724 | 
                        - # with-statements.  | 
                    |
| 4725 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4726 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 4727 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4728 | 
                        - stack.enter_context(  | 
                    |
| 4729 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 4730 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 4731 | 
                        - runner=runner,  | 
                    |
| 4732 | 
                        - vault_config=config,  | 
                    |
| 4733 | 
                        - )  | 
                    |
| 2284 | 
                        + assert result.error_exit(error=err_text), (  | 
                    |
| 2285 | 
                        + "expected error exit and known error message"  | 
                    |
| 4734 | 2286 | 
                        )  | 
                    
| 4735 | 
                        - config_filename = cli_helpers.config_filename(subsystem="vault")  | 
                    |
| 4736 | 
                        - with config_filename.open(encoding="UTF-8") as fileobj:  | 
                    |
| 4737 | 
                        - assert json.load(fileobj) == config  | 
                    |
| 4738 | 
                        - assert cli_helpers.load_config() == config  | 
                    |
| 4739 | 2287 | 
                         | 
                    
| 4740 | 
                        - def test_110_save_bad_config(  | 
                    |
| 2288 | 
                        + def test_225a_store_config_fail_manual_no_ssh_key_selection(  | 
                    |
| 4741 | 2289 | 
                        self,  | 
                    
| 2290 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 4742 | 2291 | 
                        ) -> None:  | 
                    
| 4743 | 
                        - """[`cli_helpers.save_config`][] fails for bad configurations."""  | 
                    |
| 2292 | 
                        + """Not selecting an SSH key during `--config --key` fails."""  | 
                    |
| 2293 | 
                        + del running_ssh_agent  | 
                    |
| 4744 | 2294 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 4745 | 2295 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 4746 | 2296 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -4751,358 +2301,38 @@ class TestCLIUtils:  | 
                  
| 4751 | 2301 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 4752 | 2302 | 
                        monkeypatch=monkeypatch,  | 
                    
| 4753 | 2303 | 
                        runner=runner,  | 
                    
| 4754 | 
                        -                    vault_config={},
                       | 
                    |
| 2304 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 4755 | 2305 | 
                        )  | 
                    
| 4756 | 2306 | 
                        )  | 
                    
| 4757 | 
                        - stack.enter_context(  | 
                    |
| 4758 | 
                        - pytest.raises(ValueError, match="Invalid vault config")  | 
                    |
| 4759 | 
                        - )  | 
                    |
| 4760 | 
                        - cli_helpers.save_config(None) # type: ignore[arg-type]  | 
                    |
| 4761 | 
                        -  | 
                    |
| 4762 | 
                        - def test_111_prompt_for_selection_multiple(self) -> None:  | 
                    |
| 4763 | 
                        - """[`cli_helpers.prompt_for_selection`][] works in the "multiple" case."""  | 
                    |
| 4764 | 
                        -  | 
                    |
| 4765 | 
                        - @click.command()  | 
                    |
| 4766 | 
                        -        @click.option("--heading", default="Our menu:")
                       | 
                    |
| 4767 | 
                        -        @click.argument("items", nargs=-1)
                       | 
                    |
| 4768 | 
                        - def driver(heading: str, items: list[str]) -> None:  | 
                    |
| 4769 | 
                        - # from https://montypython.fandom.com/wiki/Spam#The_menu  | 
                    |
| 4770 | 
                        - items = items or [  | 
                    |
| 4771 | 
                        - "Egg and bacon",  | 
                    |
| 4772 | 
                        - "Egg, sausage and bacon",  | 
                    |
| 4773 | 
                        - "Egg and spam",  | 
                    |
| 4774 | 
                        - "Egg, bacon and spam",  | 
                    |
| 4775 | 
                        - "Egg, bacon, sausage and spam",  | 
                    |
| 4776 | 
                        - "Spam, bacon, sausage and spam",  | 
                    |
| 4777 | 
                        - "Spam, egg, spam, spam, bacon and spam",  | 
                    |
| 4778 | 
                        - "Spam, spam, spam, egg and spam",  | 
                    |
| 4779 | 
                        - (  | 
                    |
| 4780 | 
                        - "Spam, spam, spam, spam, spam, spam, baked beans, "  | 
                    |
| 4781 | 
                        - "spam, spam, spam and spam"  | 
                    |
| 4782 | 
                        - ),  | 
                    |
| 4783 | 
                        - (  | 
                    |
| 4784 | 
                        - "Lobster thermidor aux crevettes with a mornay sauce "  | 
                    |
| 4785 | 
                        - "garnished with truffle paté, brandy "  | 
                    |
| 4786 | 
                        - "and a fried egg on top and spam"  | 
                    |
| 4787 | 
                        - ),  | 
                    |
| 4788 | 
                        - ]  | 
                    |
| 4789 | 
                        - index = cli_helpers.prompt_for_selection(items, heading=heading)  | 
                    |
| 4790 | 
                        -            click.echo("A fine choice: ", nl=False)
                       | 
                    |
| 4791 | 
                        - click.echo(items[index])  | 
                    |
| 4792 | 
                        -            click.echo("(Note: Vikings strictly optional.)")
                       | 
                    |
| 4793 | 2307 | 
                         | 
                    
| 4794 | 
                        - runner = machinery.CliRunner(mix_stderr=True)  | 
                    |
| 4795 | 
                        - result = runner.invoke(driver, [], input="9")  | 
                    |
| 4796 | 
                        - assert result.clean_exit(  | 
                    |
| 4797 | 
                        - output="""\  | 
                    |
| 4798 | 
                        -Our menu:  | 
                    |
| 4799 | 
                        -[1] Egg and bacon  | 
                    |
| 4800 | 
                        -[2] Egg, sausage and bacon  | 
                    |
| 4801 | 
                        -[3] Egg and spam  | 
                    |
| 4802 | 
                        -[4] Egg, bacon and spam  | 
                    |
| 4803 | 
                        -[5] Egg, bacon, sausage and spam  | 
                    |
| 4804 | 
                        -[6] Spam, bacon, sausage and spam  | 
                    |
| 4805 | 
                        -[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 4806 | 
                        -[8] Spam, spam, spam, egg and spam  | 
                    |
| 4807 | 
                        -[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 4808 | 
                        -[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 4809 | 
                        -Your selection? (1-10, leave empty to abort): 9  | 
                    |
| 4810 | 
                        -A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 4811 | 
                        -(Note: Vikings strictly optional.)  | 
                    |
| 4812 | 
                        -"""  | 
                    |
| 4813 | 
                        - ), "expected clean exit"  | 
                    |
| 4814 | 
                        - result = runner.invoke(  | 
                    |
| 4815 | 
                        - driver, ["--heading="], input="\n", catch_exceptions=True  | 
                    |
| 4816 | 
                        - )  | 
                    |
| 4817 | 
                        - assert result.error_exit(error=IndexError), (  | 
                    |
| 4818 | 
                        - "expected error exit and known error type"  | 
                    |
| 4819 | 
                        - )  | 
                    |
| 4820 | 
                        - assert (  | 
                    |
| 4821 | 
                        - result.stdout  | 
                    |
| 4822 | 
                        - == """\  | 
                    |
| 4823 | 
                        -[1] Egg and bacon  | 
                    |
| 4824 | 
                        -[2] Egg, sausage and bacon  | 
                    |
| 4825 | 
                        -[3] Egg and spam  | 
                    |
| 4826 | 
                        -[4] Egg, bacon and spam  | 
                    |
| 4827 | 
                        -[5] Egg, bacon, sausage and spam  | 
                    |
| 4828 | 
                        -[6] Spam, bacon, sausage and spam  | 
                    |
| 4829 | 
                        -[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 4830 | 
                        -[8] Spam, spam, spam, egg and spam  | 
                    |
| 4831 | 
                        -[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 4832 | 
                        -[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 4833 | 
                        -Your selection? (1-10, leave empty to abort):\x20  | 
                    |
| 4834 | 
                        -"""  | 
                    |
| 4835 | 
                        - ), "expected known output"  | 
                    |
| 4836 | 
                        - # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the  | 
                    |
| 4837 | 
                        - # click prompting machinery, meaning that the mixed output will  | 
                    |
| 4838 | 
                        - # incorrectly contain a line break, contrary to what the  | 
                    |
| 4839 | 
                        - # documentation for click.prompt prescribes.  | 
                    |
| 4840 | 
                        - result = runner.invoke(  | 
                    |
| 4841 | 
                        - driver, ["--heading="], input="", catch_exceptions=True  | 
                    |
| 4842 | 
                        - )  | 
                    |
| 4843 | 
                        - assert result.error_exit(error=IndexError), (  | 
                    |
| 4844 | 
                        - "expected error exit and known error type"  | 
                    |
| 4845 | 
                        - )  | 
                    |
| 4846 | 
                        -        assert result.stdout in {
                       | 
                    |
| 4847 | 
                        - """\  | 
                    |
| 4848 | 
                        -[1] Egg and bacon  | 
                    |
| 4849 | 
                        -[2] Egg, sausage and bacon  | 
                    |
| 4850 | 
                        -[3] Egg and spam  | 
                    |
| 4851 | 
                        -[4] Egg, bacon and spam  | 
                    |
| 4852 | 
                        -[5] Egg, bacon, sausage and spam  | 
                    |
| 4853 | 
                        -[6] Spam, bacon, sausage and spam  | 
                    |
| 4854 | 
                        -[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 4855 | 
                        -[8] Spam, spam, spam, egg and spam  | 
                    |
| 4856 | 
                        -[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 4857 | 
                        -[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 4858 | 
                        -Your selection? (1-10, leave empty to abort):\x20  | 
                    |
| 4859 | 
                        -""",  | 
                    |
| 4860 | 
                        - """\  | 
                    |
| 4861 | 
                        -[1] Egg and bacon  | 
                    |
| 4862 | 
                        -[2] Egg, sausage and bacon  | 
                    |
| 4863 | 
                        -[3] Egg and spam  | 
                    |
| 4864 | 
                        -[4] Egg, bacon and spam  | 
                    |
| 4865 | 
                        -[5] Egg, bacon, sausage and spam  | 
                    |
| 4866 | 
                        -[6] Spam, bacon, sausage and spam  | 
                    |
| 4867 | 
                        -[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 4868 | 
                        -[8] Spam, spam, spam, egg and spam  | 
                    |
| 4869 | 
                        -[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 4870 | 
                        -[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 4871 | 
                        -Your selection? (1-10, leave empty to abort): """,  | 
                    |
| 4872 | 
                        - }, "expected known output"  | 
                    |
| 4873 | 
                        -  | 
                    |
| 4874 | 
                        - def test_112_prompt_for_selection_single(self) -> None:  | 
                    |
| 4875 | 
                        - """[`cli_helpers.prompt_for_selection`][] works in the "single" case."""  | 
                    |
| 4876 | 
                        -  | 
                    |
| 4877 | 
                        - @click.command()  | 
                    |
| 4878 | 
                        -        @click.option("--item", default="baked beans")
                       | 
                    |
| 4879 | 
                        -        @click.argument("prompt")
                       | 
                    |
| 4880 | 
                        - def driver(item: str, prompt: str) -> None:  | 
                    |
| 4881 | 
                        - try:  | 
                    |
| 4882 | 
                        - cli_helpers.prompt_for_selection(  | 
                    |
| 4883 | 
                        - [item], heading="", single_choice_prompt=prompt  | 
                    |
| 4884 | 
                        - )  | 
                    |
| 4885 | 
                        - except IndexError:  | 
                    |
| 4886 | 
                        -                click.echo("Boo.")
                       | 
                    |
| 4887 | 
                        - raise  | 
                    |
| 4888 | 
                        - else:  | 
                    |
| 4889 | 
                        -                click.echo("Great!")
                       | 
                    |
| 2308 | 
                        + def prompt_for_selection(*_args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 2309 | 
                        + raise IndexError(cli_helpers.EMPTY_SELECTION)  | 
                    |
| 4890 | 2310 | 
                         | 
                    
| 4891 | 
                        - runner = machinery.CliRunner(mix_stderr=True)  | 
                    |
| 4892 | 
                        - result = runner.invoke(  | 
                    |
| 4893 | 
                        - driver, ["Will replace with spam. Confirm, y/n?"], input="y"  | 
                    |
| 4894 | 
                        - )  | 
                    |
| 4895 | 
                        - assert result.clean_exit(  | 
                    |
| 4896 | 
                        - output="""\  | 
                    |
| 4897 | 
                        -[1] baked beans  | 
                    |
| 4898 | 
                        -Will replace with spam. Confirm, y/n? y  | 
                    |
| 4899 | 
                        -Great!  | 
                    |
| 4900 | 
                        -"""  | 
                    |
| 4901 | 
                        - ), "expected clean exit"  | 
                    |
| 4902 | 
                        - result = runner.invoke(  | 
                    |
| 4903 | 
                        - driver,  | 
                    |
| 4904 | 
                        - ['Will replace with spam, okay? (Please say "y" or "n".)'],  | 
                    |
| 4905 | 
                        - input="\n",  | 
                    |
| 4906 | 
                        - )  | 
                    |
| 4907 | 
                        - assert result.error_exit(error=IndexError), (  | 
                    |
| 4908 | 
                        - "expected error exit and known error type"  | 
                    |
| 4909 | 
                        - )  | 
                    |
| 4910 | 
                        - assert (  | 
                    |
| 4911 | 
                        - result.stdout  | 
                    |
| 4912 | 
                        - == """\  | 
                    |
| 4913 | 
                        -[1] baked beans  | 
                    |
| 4914 | 
                        -Will replace with spam, okay? (Please say "y" or "n".):\x20  | 
                    |
| 4915 | 
                        -Boo.  | 
                    |
| 4916 | 
                        -"""  | 
                    |
| 4917 | 
                        - ), "expected known output"  | 
                    |
| 4918 | 
                        - # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the  | 
                    |
| 4919 | 
                        - # click prompting machinery, meaning that the mixed output will  | 
                    |
| 4920 | 
                        - # incorrectly contain a line break, contrary to what the  | 
                    |
| 4921 | 
                        - # documentation for click.prompt prescribes.  | 
                    |
| 4922 | 
                        - result = runner.invoke(  | 
                    |
| 4923 | 
                        - driver,  | 
                    |
| 4924 | 
                        - ['Will replace with spam, okay? (Please say "y" or "n".)'],  | 
                    |
| 4925 | 
                        - input="",  | 
                    |
| 4926 | 
                        - )  | 
                    |
| 4927 | 
                        - assert result.error_exit(error=IndexError), (  | 
                    |
| 4928 | 
                        - "expected error exit and known error type"  | 
                    |
| 4929 | 
                        - )  | 
                    |
| 4930 | 
                        -        assert result.stdout in {
                       | 
                    |
| 4931 | 
                        - """\  | 
                    |
| 4932 | 
                        -[1] baked beans  | 
                    |
| 4933 | 
                        -Will replace with spam, okay? (Please say "y" or "n".):\x20  | 
                    |
| 4934 | 
                        -Boo.  | 
                    |
| 4935 | 
                        -""",  | 
                    |
| 4936 | 
                        - """\  | 
                    |
| 4937 | 
                        -[1] baked beans  | 
                    |
| 4938 | 
                        -Will replace with spam, okay? (Please say "y" or "n".): Boo.  | 
                    |
| 4939 | 
                        -""",  | 
                    |
| 4940 | 
                        - }, "expected known output"  | 
                    |
| 4941 | 
                        -  | 
                    |
| 4942 | 
                        - def test_113_prompt_for_passphrase(  | 
                    |
| 4943 | 
                        - self,  | 
                    |
| 4944 | 
                        - ) -> None:  | 
                    |
| 4945 | 
                        - """[`cli_helpers.prompt_for_passphrase`][] works."""  | 
                    |
| 4946 | 
                        - with pytest.MonkeyPatch.context() as monkeypatch:  | 
                    |
| 4947 | 2311 | 
                        monkeypatch.setattr(  | 
                    
| 4948 | 
                        - click,  | 
                    |
| 4949 | 
                        - "prompt",  | 
                    |
| 4950 | 
                        -                lambda *a, **kw: json.dumps({"args": a, "kwargs": kw}),
                       | 
                    |
| 4951 | 
                        - )  | 
                    |
| 4952 | 
                        - res = json.loads(cli_helpers.prompt_for_passphrase())  | 
                    |
| 4953 | 
                        - err_msg = "missing arguments to passphrase prompt"  | 
                    |
| 4954 | 
                        - assert "args" in res, err_msg  | 
                    |
| 4955 | 
                        - assert "kwargs" in res, err_msg  | 
                    |
| 4956 | 
                        - assert res["args"][:1] == ["Passphrase"], err_msg  | 
                    |
| 4957 | 
                        -        assert res["kwargs"].get("default") == "", err_msg
                       | 
                    |
| 4958 | 
                        -        assert not res["kwargs"].get("show_default", True), err_msg
                       | 
                    |
| 4959 | 
                        -        assert res["kwargs"].get("err"), err_msg
                       | 
                    |
| 4960 | 
                        -        assert res["kwargs"].get("hide_input"), err_msg
                       | 
                    |
| 4961 | 
                        -  | 
                    |
| 4962 | 
                        - def test_120_standard_logging_context_manager(  | 
                    |
| 4963 | 
                        - self,  | 
                    |
| 4964 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 4965 | 
                        - capsys: pytest.CaptureFixture[str],  | 
                    |
| 4966 | 
                        - ) -> None:  | 
                    |
| 4967 | 
                        - """The standard logging context manager works.  | 
                    |
| 4968 | 
                        -  | 
                    |
| 4969 | 
                        - It registers its handlers, once, and emits formatted calls to  | 
                    |
| 4970 | 
                        - standard error prefixed with the program name.  | 
                    |
| 4971 | 
                        -  | 
                    |
| 4972 | 
                        - """  | 
                    |
| 4973 | 
                        - prog_name = cli_machinery.StandardCLILogging.prog_name  | 
                    |
| 4974 | 
                        - package_name = cli_machinery.StandardCLILogging.package_name  | 
                    |
| 4975 | 
                        - logger = logging.getLogger(package_name)  | 
                    |
| 4976 | 
                        -        deprecation_logger = logging.getLogger(f"{package_name}.deprecation")
                       | 
                    |
| 4977 | 
                        - logging_cm = cli_machinery.StandardCLILogging.ensure_standard_logging()  | 
                    |
| 4978 | 
                        - with logging_cm:  | 
                    |
| 4979 | 
                        - assert (  | 
                    |
| 4980 | 
                        - sum(  | 
                    |
| 4981 | 
                        - 1  | 
                    |
| 4982 | 
                        - for h in logger.handlers  | 
                    |
| 4983 | 
                        - if h is cli_machinery.StandardCLILogging.cli_handler  | 
                    |
| 4984 | 
                        - )  | 
                    |
| 4985 | 
                        - == 1  | 
                    |
| 4986 | 
                        - )  | 
                    |
| 4987 | 
                        -            logger.warning("message 1")
                       | 
                    |
| 4988 | 
                        - with logging_cm:  | 
                    |
| 4989 | 
                        -                deprecation_logger.warning("message 2")
                       | 
                    |
| 4990 | 
                        - assert (  | 
                    |
| 4991 | 
                        - sum(  | 
                    |
| 4992 | 
                        - 1  | 
                    |
| 4993 | 
                        - for h in logger.handlers  | 
                    |
| 4994 | 
                        - if h is cli_machinery.StandardCLILogging.cli_handler  | 
                    |
| 4995 | 
                        - )  | 
                    |
| 4996 | 
                        - == 1  | 
                    |
| 4997 | 
                        - )  | 
                    |
| 4998 | 
                        - assert capsys.readouterr() == (  | 
                    |
| 4999 | 
                        - "",  | 
                    |
| 5000 | 
                        - (  | 
                    |
| 5001 | 
                        -                        f"{prog_name}: Warning: message 1\n"
                       | 
                    |
| 5002 | 
                        -                        f"{prog_name}: Deprecation warning: message 2\n"
                       | 
                    |
| 5003 | 
                        - ),  | 
                    |
| 5004 | 
                        - )  | 
                    |
| 5005 | 
                        -            logger.warning("message 3")
                       | 
                    |
| 5006 | 
                        - assert (  | 
                    |
| 5007 | 
                        - sum(  | 
                    |
| 5008 | 
                        - 1  | 
                    |
| 5009 | 
                        - for h in logger.handlers  | 
                    |
| 5010 | 
                        - if h is cli_machinery.StandardCLILogging.cli_handler  | 
                    |
| 2312 | 
                        + cli_helpers, "prompt_for_selection", prompt_for_selection  | 
                    |
| 5011 | 2313 | 
                        )  | 
                    
| 5012 | 
                        - == 1  | 
                    |
| 2314 | 
                        + # Also patch the list of suitable SSH keys, lest we be at  | 
                    |
| 2315 | 
                        + # the mercy of whatever SSH agent may be running.  | 
                    |
| 2316 | 
                        + monkeypatch.setattr(  | 
                    |
| 2317 | 
                        + cli_helpers,  | 
                    |
| 2318 | 
                        + "get_suitable_ssh_keys",  | 
                    |
| 2319 | 
                        + callables.suitable_ssh_keys,  | 
                    |
| 5013 | 2320 | 
                        )  | 
                    
| 5014 | 
                        - assert capsys.readouterr() == (  | 
                    |
| 5015 | 
                        - "",  | 
                    |
| 5016 | 
                        -                f"{prog_name}: Warning: message 3\n",
                       | 
                    |
| 2321 | 
                        + result = runner.invoke(  | 
                    |
| 2322 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2323 | 
                        + ["--key", "--config"],  | 
                    |
| 2324 | 
                        + catch_exceptions=False,  | 
                    |
| 5017 | 2325 | 
                        )  | 
                    
| 5018 | 
                        - assert caplog.record_tuples == [  | 
                    |
| 5019 | 
                        - (package_name, logging.WARNING, "message 1"),  | 
                    |
| 5020 | 
                        -                (f"{package_name}.deprecation", logging.WARNING, "message 2"),
                       | 
                    |
| 5021 | 
                        - (package_name, logging.WARNING, "message 3"),  | 
                    |
| 5022 | 
                        - ]  | 
                    |
| 5023 | 
                        -  | 
                    |
| 5024 | 
                        - def test_121_standard_logging_warnings_context_manager(  | 
                    |
| 5025 | 
                        - self,  | 
                    |
| 5026 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 5027 | 
                        - capsys: pytest.CaptureFixture[str],  | 
                    |
| 5028 | 
                        - ) -> None:  | 
                    |
| 5029 | 
                        - """The standard warnings logging context manager works.  | 
                    |
| 5030 | 
                        -  | 
                    |
| 5031 | 
                        - It registers its handlers, once, and emits formatted calls to  | 
                    |
| 5032 | 
                        - standard error prefixed with the program name. It also adheres  | 
                    |
| 5033 | 
                        - to the global warnings filter concerning which messages it  | 
                    |
| 5034 | 
                        - actually emits to standard error.  | 
                    |
| 5035 | 
                        -  | 
                    |
| 5036 | 
                        - """  | 
                    |
| 5037 | 
                        - warnings_cm = (  | 
                    |
| 5038 | 
                        - cli_machinery.StandardCLILogging.ensure_standard_warnings_logging()  | 
                    |
| 2326 | 
                        + assert result.error_exit(error="the user aborted the request"), (  | 
                    |
| 2327 | 
                        + "expected error exit and known error message"  | 
                    |
| 5039 | 2328 | 
                        )  | 
                    
| 5040 | 
                        - THE_FUTURE = "the future will be here sooner than you think" # noqa: N806  | 
                    |
| 5041 | 
                        - JUST_TESTING = "just testing whether warnings work" # noqa: N806  | 
                    |
| 5042 | 
                        - with warnings_cm:  | 
                    |
| 5043 | 
                        - assert (  | 
                    |
| 5044 | 
                        - sum(  | 
                    |
| 5045 | 
                        - 1  | 
                    |
| 5046 | 
                        -                    for h in logging.getLogger("py.warnings").handlers
                       | 
                    |
| 5047 | 
                        - if h is cli_machinery.StandardCLILogging.warnings_handler  | 
                    |
| 5048 | 
                        - )  | 
                    |
| 5049 | 
                        - == 1  | 
                    |
| 5050 | 
                        - )  | 
                    |
| 5051 | 
                        - warnings.warn(UserWarning(JUST_TESTING), stacklevel=1)  | 
                    |
| 5052 | 
                        - with warnings_cm:  | 
                    |
| 5053 | 
                        - warnings.warn(FutureWarning(THE_FUTURE), stacklevel=1)  | 
                    |
| 5054 | 
                        - _out, err = capsys.readouterr()  | 
                    |
| 5055 | 
                        - err_lines = err.splitlines(True)  | 
                    |
| 5056 | 
                        - assert any(  | 
                    |
| 5057 | 
                        -                    f"UserWarning: {JUST_TESTING}" in line
                       | 
                    |
| 5058 | 
                        - for line in err_lines  | 
                    |
| 5059 | 
                        - )  | 
                    |
| 5060 | 
                        - assert any(  | 
                    |
| 5061 | 
                        -                    f"FutureWarning: {THE_FUTURE}" in line
                       | 
                    |
| 5062 | 
                        - for line in err_lines  | 
                    |
| 5063 | 
                        - )  | 
                    |
| 5064 | 
                        - warnings.warn(UserWarning(JUST_TESTING), stacklevel=1)  | 
                    |
| 5065 | 
                        - _out, err = capsys.readouterr()  | 
                    |
| 5066 | 
                        - err_lines = err.splitlines(True)  | 
                    |
| 5067 | 
                        - assert any(  | 
                    |
| 5068 | 
                        -                f"UserWarning: {JUST_TESTING}" in line for line in err_lines
                       | 
                    |
| 5069 | 
                        - )  | 
                    |
| 5070 | 
                        - assert not any(  | 
                    |
| 5071 | 
                        -                f"FutureWarning: {THE_FUTURE}" in line for line in err_lines
                       | 
                    |
| 5072 | 
                        - )  | 
                    |
| 5073 | 
                        - record_tuples = caplog.record_tuples  | 
                    |
| 5074 | 
                        - assert [tup[:2] for tup in record_tuples] == [  | 
                    |
| 5075 | 
                        -                ("py.warnings", logging.WARNING),
                       | 
                    |
| 5076 | 
                        -                ("py.warnings", logging.WARNING),
                       | 
                    |
| 5077 | 
                        -                ("py.warnings", logging.WARNING),
                       | 
                    |
| 5078 | 
                        - ]  | 
                    |
| 5079 | 
                        -            assert f"UserWarning: {JUST_TESTING}" in record_tuples[0][2]
                       | 
                    |
| 5080 | 
                        -            assert f"FutureWarning: {THE_FUTURE}" in record_tuples[1][2]
                       | 
                    |
| 5081 | 
                        -            assert f"UserWarning: {JUST_TESTING}" in record_tuples[2][2]
                       | 
                    |
| 5082 | 2329 | 
                         | 
                    
| 5083 | 
                        - def export_as_sh_helper(  | 
                    |
| 2330 | 
                        + def test_225b_store_config_fail_manual_no_ssh_agent(  | 
                    |
| 5084 | 2331 | 
                        self,  | 
                    
| 5085 | 
                        - config: Any,  | 
                    |
| 2332 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 5086 | 2333 | 
                        ) -> None:  | 
                    
| 5087 | 
                        - """Emits a config in sh(1) format, then reads it back to verify it.  | 
                    |
| 5088 | 
                        -  | 
                    |
| 5089 | 
                        - This function exports the configuration, sets up a new  | 
                    |
| 5090 | 
                        - enviroment, then calls  | 
                    |
| 5091 | 
                        - [`vault_config_exporter_shell_interpreter`][] on the export  | 
                    |
| 5092 | 
                        - script, verifying that each command ran successfully and that  | 
                    |
| 5093 | 
                        - the final configuration matches the initial one.  | 
                    |
| 5094 | 
                        -  | 
                    |
| 5095 | 
                        - Args:  | 
                    |
| 5096 | 
                        - config:  | 
                    |
| 5097 | 
                        - The configuration to emit and read back.  | 
                    |
| 5098 | 
                        -  | 
                    |
| 5099 | 
                        - """  | 
                    |
| 5100 | 
                        -        prog_name_list = ("derivepassphrase", "vault")
                       | 
                    |
| 5101 | 
                        - with io.StringIO() as outfile:  | 
                    |
| 5102 | 
                        - cli_helpers.print_config_as_sh_script(  | 
                    |
| 5103 | 
                        - config, outfile=outfile, prog_name_list=prog_name_list  | 
                    |
| 5104 | 
                        - )  | 
                    |
| 5105 | 
                        - script = outfile.getvalue()  | 
                    |
| 2334 | 
                        + """Not running an SSH agent during `--config --key` fails."""  | 
                    |
| 2335 | 
                        + del running_ssh_agent  | 
                    |
| 5106 | 2336 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5107 | 2337 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5108 | 2338 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5113,266 +2343,25 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.  | 
                  
| 5113 | 2343 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 5114 | 2344 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5115 | 2345 | 
                        runner=runner,  | 
                    
| 5116 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 5117 | 
                        - )  | 
                    |
| 5118 | 
                        - )  | 
                    |
| 5119 | 
                        - for result in vault_config_exporter_shell_interpreter(script):  | 
                    |
| 5120 | 
                        - assert result.clean_exit()  | 
                    |
| 5121 | 
                        - assert cli_helpers.load_config() == config  | 
                    |
| 5122 | 
                        -  | 
                    |
| 5123 | 
                        - @hypothesis.given(  | 
                    |
| 5124 | 
                        - global_config_settable=hypothesis_machinery.vault_full_service_config(),  | 
                    |
| 5125 | 
                        - global_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 5126 | 
                        -            {},
                       | 
                    |
| 5127 | 
                        -            optional={
                       | 
                    |
| 5128 | 
                        - "key": strategies.text(  | 
                    |
| 5129 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5130 | 
                        - min_codepoint=32,  | 
                    |
| 5131 | 
                        - max_codepoint=126,  | 
                    |
| 5132 | 
                        - ),  | 
                    |
| 5133 | 
                        - max_size=128,  | 
                    |
| 5134 | 
                        - ),  | 
                    |
| 5135 | 
                        - "phrase": strategies.text(  | 
                    |
| 5136 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5137 | 
                        - min_codepoint=32,  | 
                    |
| 5138 | 
                        - max_codepoint=126,  | 
                    |
| 5139 | 
                        - ),  | 
                    |
| 5140 | 
                        - max_size=64,  | 
                    |
| 5141 | 
                        - ),  | 
                    |
| 5142 | 
                        - },  | 
                    |
| 5143 | 
                        - ),  | 
                    |
| 2346 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 5144 | 2347 | 
                        )  | 
                    
| 5145 | 
                        - def test_130a_export_as_sh_global(  | 
                    |
| 5146 | 
                        - self,  | 
                    |
| 5147 | 
                        - global_config_settable: _types.VaultConfigServicesSettings,  | 
                    |
| 5148 | 
                        - global_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 5149 | 
                        - ) -> None:  | 
                    |
| 5150 | 
                        - """Exporting configurations as sh(1) script works.  | 
                    |
| 5151 | 
                        -  | 
                    |
| 5152 | 
                        - Here, we check global-only configurations which use both  | 
                    |
| 5153 | 
                        - settings settable via `--config` and settings requiring  | 
                    |
| 5154 | 
                        - `--import`.  | 
                    |
| 5155 | 
                        -  | 
                    |
| 5156 | 
                        - The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 5157 | 
                        -  | 
                    |
| 5158 | 
                        - """  | 
                    |
| 5159 | 
                        -        config: _types.VaultConfig = {
                       | 
                    |
| 5160 | 
                        - "global": global_config_settable | global_config_importable,  | 
                    |
| 5161 | 
                        -            "services": {},
                       | 
                    |
| 5162 | 
                        - }  | 
                    |
| 5163 | 
                        - assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 5164 | 
                        - assert _types.is_vault_config(config)  | 
                    |
| 5165 | 
                        - return self.export_as_sh_helper(config)  | 
                    |
| 5166 | 
                        -  | 
                    |
| 5167 | 
                        - @hypothesis.given(  | 
                    |
| 5168 | 
                        - global_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 5169 | 
                        -            {},
                       | 
                    |
| 5170 | 
                        -            optional={
                       | 
                    |
| 5171 | 
                        - "key": strategies.text(  | 
                    |
| 5172 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5173 | 
                        - min_codepoint=32,  | 
                    |
| 5174 | 
                        - max_codepoint=126,  | 
                    |
| 5175 | 
                        - ),  | 
                    |
| 5176 | 
                        - max_size=128,  | 
                    |
| 5177 | 
                        - ),  | 
                    |
| 5178 | 
                        - "phrase": strategies.text(  | 
                    |
| 5179 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5180 | 
                        - min_codepoint=32,  | 
                    |
| 5181 | 
                        - max_codepoint=126,  | 
                    |
| 5182 | 
                        - ),  | 
                    |
| 5183 | 
                        - max_size=64,  | 
                    |
| 5184 | 
                        - ),  | 
                    |
| 5185 | 
                        - },  | 
                    |
| 5186 | 
                        - ),  | 
                    |
| 5187 | 2348 | 
                        )  | 
                    
| 5188 | 
                        - def test_130b_export_as_sh_global_only_imports(  | 
                    |
| 5189 | 
                        - self,  | 
                    |
| 5190 | 
                        - global_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 5191 | 
                        - ) -> None:  | 
                    |
| 5192 | 
                        - """Exporting configurations as sh(1) script works.  | 
                    |
| 5193 | 
                        -  | 
                    |
| 5194 | 
                        - Here, we check global-only configurations which only use  | 
                    |
| 5195 | 
                        - settings requiring `--import`.  | 
                    |
| 5196 | 
                        -  | 
                    |
| 5197 | 
                        - The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 5198 | 
                        -  | 
                    |
| 5199 | 
                        - """  | 
                    |
| 5200 | 
                        -        config: _types.VaultConfig = {
                       | 
                    |
| 5201 | 
                        - "global": global_config_importable,  | 
                    |
| 5202 | 
                        -            "services": {},
                       | 
                    |
| 5203 | 
                        - }  | 
                    |
| 5204 | 
                        - assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 5205 | 
                        - assert _types.is_vault_config(config)  | 
                    |
| 5206 | 
                        - if not config["global"]:  | 
                    |
| 5207 | 
                        -            config.pop("global")
                       | 
                    |
| 5208 | 
                        - return self.export_as_sh_helper(config)  | 
                    |
| 5209 | 
                        -  | 
                    |
| 5210 | 
                        - @hypothesis.given(  | 
                    |
| 5211 | 
                        - service_name=strategies.text(  | 
                    |
| 5212 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5213 | 
                        - min_codepoint=32,  | 
                    |
| 5214 | 
                        - max_codepoint=126,  | 
                    |
| 5215 | 
                        - ),  | 
                    |
| 5216 | 
                        - min_size=4,  | 
                    |
| 5217 | 
                        - max_size=64,  | 
                    |
| 5218 | 
                        - ),  | 
                    |
| 5219 | 
                        - service_config_settable=hypothesis_machinery.vault_full_service_config(),  | 
                    |
| 5220 | 
                        - service_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 5221 | 
                        -            {},
                       | 
                    |
| 5222 | 
                        -            optional={
                       | 
                    |
| 5223 | 
                        - "key": strategies.text(  | 
                    |
| 5224 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5225 | 
                        - min_codepoint=32,  | 
                    |
| 5226 | 
                        - max_codepoint=126,  | 
                    |
| 5227 | 
                        - ),  | 
                    |
| 5228 | 
                        - max_size=128,  | 
                    |
| 5229 | 
                        - ),  | 
                    |
| 5230 | 
                        - "phrase": strategies.text(  | 
                    |
| 5231 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5232 | 
                        - min_codepoint=32,  | 
                    |
| 5233 | 
                        - max_codepoint=126,  | 
                    |
| 5234 | 
                        - ),  | 
                    |
| 5235 | 
                        - max_size=64,  | 
                    |
| 5236 | 
                        - ),  | 
                    |
| 5237 | 
                        - "notes": strategies.text(  | 
                    |
| 5238 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5239 | 
                        - min_codepoint=32,  | 
                    |
| 5240 | 
                        - max_codepoint=126,  | 
                    |
| 5241 | 
                        -                        include_characters=("\n", "\f", "\t"),
                       | 
                    |
| 5242 | 
                        - ),  | 
                    |
| 5243 | 
                        - max_size=256,  | 
                    |
| 5244 | 
                        - ),  | 
                    |
| 5245 | 
                        - },  | 
                    |
| 5246 | 
                        - ),  | 
                    |
| 2349 | 
                        +            monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
                       | 
                    |
| 2350 | 
                        + result = runner.invoke(  | 
                    |
| 2351 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2352 | 
                        + ["--key", "--config"],  | 
                    |
| 2353 | 
                        + catch_exceptions=False,  | 
                    |
| 5247 | 2354 | 
                        )  | 
                    
| 5248 | 
                        - def test_130c_export_as_sh_service(  | 
                    |
| 5249 | 
                        - self,  | 
                    |
| 5250 | 
                        - service_name: str,  | 
                    |
| 5251 | 
                        - service_config_settable: _types.VaultConfigServicesSettings,  | 
                    |
| 5252 | 
                        - service_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 5253 | 
                        - ) -> None:  | 
                    |
| 5254 | 
                        - """Exporting configurations as sh(1) script works.  | 
                    |
| 5255 | 
                        -  | 
                    |
| 5256 | 
                        - Here, we check service-only configurations which use both  | 
                    |
| 5257 | 
                        - settings settable via `--config` and settings requiring  | 
                    |
| 5258 | 
                        - `--import`.  | 
                    |
| 5259 | 
                        -  | 
                    |
| 5260 | 
                        - The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 5261 | 
                        -  | 
                    |
| 5262 | 
                        - """  | 
                    |
| 5263 | 
                        -        config: _types.VaultConfig = {
                       | 
                    |
| 5264 | 
                        -            "services": {
                       | 
                    |
| 5265 | 
                        - service_name: (  | 
                    |
| 5266 | 
                        - service_config_settable | service_config_importable  | 
                    |
| 5267 | 
                        - ),  | 
                    |
| 5268 | 
                        - },  | 
                    |
| 5269 | 
                        - }  | 
                    |
| 5270 | 
                        - assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 5271 | 
                        - assert _types.is_vault_config(config)  | 
                    |
| 5272 | 
                        - return self.export_as_sh_helper(config)  | 
                    |
| 5273 | 
                        -  | 
                    |
| 5274 | 
                        - @hypothesis.given(  | 
                    |
| 5275 | 
                        - service_name=strategies.text(  | 
                    |
| 5276 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5277 | 
                        - min_codepoint=32,  | 
                    |
| 5278 | 
                        - max_codepoint=126,  | 
                    |
| 5279 | 
                        - ),  | 
                    |
| 5280 | 
                        - min_size=4,  | 
                    |
| 5281 | 
                        - max_size=64,  | 
                    |
| 5282 | 
                        - ),  | 
                    |
| 5283 | 
                        - service_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 5284 | 
                        -            {},
                       | 
                    |
| 5285 | 
                        -            optional={
                       | 
                    |
| 5286 | 
                        - "key": strategies.text(  | 
                    |
| 5287 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5288 | 
                        - min_codepoint=32,  | 
                    |
| 5289 | 
                        - max_codepoint=126,  | 
                    |
| 5290 | 
                        - ),  | 
                    |
| 5291 | 
                        - max_size=128,  | 
                    |
| 5292 | 
                        - ),  | 
                    |
| 5293 | 
                        - "phrase": strategies.text(  | 
                    |
| 5294 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5295 | 
                        - min_codepoint=32,  | 
                    |
| 5296 | 
                        - max_codepoint=126,  | 
                    |
| 5297 | 
                        - ),  | 
                    |
| 5298 | 
                        - max_size=64,  | 
                    |
| 5299 | 
                        - ),  | 
                    |
| 5300 | 
                        - "notes": strategies.text(  | 
                    |
| 5301 | 
                        - alphabet=strategies.characters(  | 
                    |
| 5302 | 
                        - min_codepoint=32,  | 
                    |
| 5303 | 
                        - max_codepoint=126,  | 
                    |
| 5304 | 
                        -                        include_characters=("\n", "\f", "\t"),
                       | 
                    |
| 5305 | 
                        - ),  | 
                    |
| 5306 | 
                        - max_size=256,  | 
                    |
| 5307 | 
                        - ),  | 
                    |
| 5308 | 
                        - },  | 
                    |
| 5309 | 
                        - ),  | 
                    |
| 2355 | 
                        + assert result.error_exit(error="Cannot find any running SSH agent"), (  | 
                    |
| 2356 | 
                        + "expected error exit and known error message"  | 
                    |
| 5310 | 2357 | 
                        )  | 
                    
| 5311 | 
                        - def test_130d_export_as_sh_service_only_imports(  | 
                    |
| 5312 | 
                        - self,  | 
                    |
| 5313 | 
                        - service_name: str,  | 
                    |
| 5314 | 
                        - service_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 5315 | 
                        - ) -> None:  | 
                    |
| 5316 | 
                        - """Exporting configurations as sh(1) script works.  | 
                    |
| 5317 | 
                        -  | 
                    |
| 5318 | 
                        - Here, we check service-only configurations which only use  | 
                    |
| 5319 | 
                        - settings requiring `--import`.  | 
                    |
| 5320 | 
                        -  | 
                    |
| 5321 | 
                        - The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 5322 | 
                        -  | 
                    |
| 5323 | 
                        - """  | 
                    |
| 5324 | 
                        -        config: _types.VaultConfig = {
                       | 
                    |
| 5325 | 
                        -            "services": {
                       | 
                    |
| 5326 | 
                        - service_name: service_config_importable,  | 
                    |
| 5327 | 
                        - },  | 
                    |
| 5328 | 
                        - }  | 
                    |
| 5329 | 
                        - assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 5330 | 
                        - assert _types.is_vault_config(config)  | 
                    |
| 5331 | 
                        - return self.export_as_sh_helper(config)  | 
                    |
| 5332 | 2358 | 
                         | 
                    
| 5333 | 
                        - # The Annoying OS appears to silently truncate spaces at the end of  | 
                    |
| 5334 | 
                        - # filenames.  | 
                    |
| 5335 | 
                        - @hypothesis.given(  | 
                    |
| 5336 | 
                        - env_var=strategies.sampled_from(["TMPDIR", "TEMP", "TMP"]),  | 
                    |
| 5337 | 
                        - suffix=strategies.builds(  | 
                    |
| 5338 | 
                        - operator.add,  | 
                    |
| 5339 | 
                        - strategies.text(  | 
                    |
| 5340 | 
                        -                tuple(" 0123456789abcdefghijklmnopqrstuvwxyz"),
                       | 
                    |
| 5341 | 
                        - min_size=11,  | 
                    |
| 5342 | 
                        - max_size=11,  | 
                    |
| 5343 | 
                        - ),  | 
                    |
| 5344 | 
                        - strategies.text(  | 
                    |
| 5345 | 
                        -                tuple("0123456789abcdefghijklmnopqrstuvwxyz"),
                       | 
                    |
| 5346 | 
                        - min_size=1,  | 
                    |
| 5347 | 
                        - max_size=1,  | 
                    |
| 5348 | 
                        - ),  | 
                    |
| 5349 | 
                        - ),  | 
                    |
| 5350 | 
                        - )  | 
                    |
| 5351 | 
                        - @hypothesis.example(env_var="", suffix=".")  | 
                    |
| 5352 | 
                        - def test_140a_get_tempdir(  | 
                    |
| 2359 | 
                        + def test_225c_store_config_fail_manual_bad_ssh_agent_connection(  | 
                    |
| 5353 | 2360 | 
                        self,  | 
                    
| 5354 | 
                        - env_var: str,  | 
                    |
| 5355 | 
                        - suffix: str,  | 
                    |
| 2361 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 5356 | 2362 | 
                        ) -> None:  | 
                    
| 5357 | 
                        - """[`cli_helpers.get_tempdir`][] returns a temporary directory.  | 
                    |
| 5358 | 
                        -  | 
                    |
| 5359 | 
                        - If it is not the same as the temporary directory determined by  | 
                    |
| 5360 | 
                        - [`tempfile.gettempdir`][], then assert that  | 
                    |
| 5361 | 
                        - `tempfile.gettempdir` returned the current directory and  | 
                    |
| 5362 | 
                        - `cli_helpers.get_tempdir` returned the configuration directory.  | 
                    |
| 5363 | 
                        -  | 
                    |
| 5364 | 
                        - """  | 
                    |
| 5365 | 
                        -  | 
                    |
| 5366 | 
                        - @contextlib.contextmanager  | 
                    |
| 5367 | 
                        - def make_temporary_directory(  | 
                    |
| 5368 | 
                        - path: pathlib.Path,  | 
                    |
| 5369 | 
                        - ) -> Iterator[pathlib.Path]:  | 
                    |
| 5370 | 
                        - try:  | 
                    |
| 5371 | 
                        - path.mkdir()  | 
                    |
| 5372 | 
                        - yield path  | 
                    |
| 5373 | 
                        - finally:  | 
                    |
| 5374 | 
                        - shutil.rmtree(path)  | 
                    |
| 5375 | 
                        -  | 
                    |
| 2363 | 
                        + """Not running a reachable SSH agent during `--config --key` fails."""  | 
                    |
| 2364 | 
                        + running_ssh_agent.require_external_address()  | 
                    |
| 5376 | 2365 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5377 | 2366 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5378 | 2367 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5383,43 +2372,26 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.  | 
                  
| 5383 | 2372 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 5384 | 2373 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5385 | 2374 | 
                        runner=runner,  | 
                    
| 5386 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 2375 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 2376 | 
                        + )  | 
                    |
| 2377 | 
                        + )  | 
                    |
| 2378 | 
                        + cwd = pathlib.Path.cwd().resolve()  | 
                    |
| 2379 | 
                        +            monkeypatch.setenv("SSH_AUTH_SOCK", str(cwd))
                       | 
                    |
| 2380 | 
                        + result = runner.invoke(  | 
                    |
| 2381 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2382 | 
                        + ["--key", "--config"],  | 
                    |
| 2383 | 
                        + catch_exceptions=False,  | 
                    |
| 5387 | 2384 | 
                        )  | 
                    
| 2385 | 
                        + assert result.error_exit(error="Cannot connect to the SSH agent"), (  | 
                    |
| 2386 | 
                        + "expected error exit and known error message"  | 
                    |
| 5388 | 2387 | 
                        )  | 
                    
| 5389 | 
                        - old_tempdir = os.fsdecode(tempfile.gettempdir())  | 
                    |
| 5390 | 
                        -            monkeypatch.delenv("TMPDIR", raising=False)
                       | 
                    |
| 5391 | 
                        -            monkeypatch.delenv("TEMP", raising=False)
                       | 
                    |
| 5392 | 
                        -            monkeypatch.delenv("TMP", raising=False)
                       | 
                    |
| 5393 | 
                        - monkeypatch.setattr(tempfile, "tempdir", None)  | 
                    |
| 5394 | 
                        - temp_path = pathlib.Path.cwd() / suffix  | 
                    |
| 5395 | 
                        - if env_var:  | 
                    |
| 5396 | 
                        - monkeypatch.setenv(env_var, os.fsdecode(temp_path))  | 
                    |
| 5397 | 
                        - stack.enter_context(make_temporary_directory(temp_path))  | 
                    |
| 5398 | 
                        - new_tempdir = os.fsdecode(tempfile.gettempdir())  | 
                    |
| 5399 | 
                        - hypothesis.assume(  | 
                    |
| 5400 | 
                        - temp_path.resolve() == pathlib.Path.cwd().resolve()  | 
                    |
| 5401 | 
                        - or old_tempdir != new_tempdir  | 
                    |
| 5402 | 
                        - )  | 
                    |
| 5403 | 
                        - system_tempdir = os.fsdecode(tempfile.gettempdir())  | 
                    |
| 5404 | 
                        - our_tempdir = cli_helpers.get_tempdir()  | 
                    |
| 5405 | 
                        - assert system_tempdir == os.fsdecode(our_tempdir) or (  | 
                    |
| 5406 | 
                        - # TODO(the-13th-letter): `pytest_machinery.isolated_config`  | 
                    |
| 5407 | 
                        - # guarantees that `Path.cwd() == config_filename(None)`.  | 
                    |
| 5408 | 
                        - # So this sub-branch ought to never trigger in our  | 
                    |
| 5409 | 
                        - # tests.  | 
                    |
| 5410 | 
                        - system_tempdir == os.getcwd() # noqa: PTH109  | 
                    |
| 5411 | 
                        - and our_tempdir == cli_helpers.config_filename(subsystem=None)  | 
                    |
| 5412 | 
                        - )  | 
                    |
| 5413 | 
                        -        assert not temp_path.exists(), f"temp path {temp_path} not cleaned up!"
                       | 
                    |
| 5414 | 
                        -  | 
                    |
| 5415 | 
                        - def test_140b_get_tempdir_force_default(self) -> None:  | 
                    |
| 5416 | 
                        - """[`cli_helpers.get_tempdir`][] returns a temporary directory.  | 
                    |
| 5417 | 
                        -  | 
                    |
| 5418 | 
                        - If all candidates are mocked to fail for the standard temporary  | 
                    |
| 5419 | 
                        - directory choices, then we return the `derivepassphrase`  | 
                    |
| 5420 | 
                        - configuration directory.  | 
                    |
| 5421 | 2388 | 
                         | 
                    
| 5422 | 
                        - """  | 
                    |
| 2389 | 
                        + @Parametrize.TRY_RACE_FREE_IMPLEMENTATION  | 
                    |
| 2390 | 
                        + def test_225d_store_config_fail_manual_read_only_file(  | 
                    |
| 2391 | 
                        + self,  | 
                    |
| 2392 | 
                        + try_race_free_implementation: bool,  | 
                    |
| 2393 | 
                        + ) -> None:  | 
                    |
| 2394 | 
                        + """Using a read-only configuration file with `--config` fails."""  | 
                    |
| 5423 | 2395 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5424 | 2396 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5425 | 2397 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5430,51 +2402,26 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.  | 
                  
| 5430 | 2402 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 5431 | 2403 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5432 | 2404 | 
                        runner=runner,  | 
                    
| 5433 | 
                        -                    vault_config={"services": {}},
                       | 
                    |
| 2405 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 5434 | 2406 | 
                        )  | 
                    
| 5435 | 2407 | 
                        )  | 
                    
| 5436 | 
                        -            monkeypatch.delenv("TMPDIR", raising=False)
                       | 
                    |
| 5437 | 
                        -            monkeypatch.delenv("TEMP", raising=False)
                       | 
                    |
| 5438 | 
                        -            monkeypatch.delenv("TMP", raising=False)
                       | 
                    |
| 5439 | 
                        - config_dir = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 5440 | 
                        -  | 
                    |
| 5441 | 
                        - def is_dir_false(  | 
                    |
| 5442 | 
                        - self: pathlib.Path,  | 
                    |
| 5443 | 
                        - /,  | 
                    |
| 5444 | 
                        - *,  | 
                    |
| 5445 | 
                        - follow_symlinks: bool = False,  | 
                    |
| 5446 | 
                        - ) -> bool:  | 
                    |
| 5447 | 
                        - del self, follow_symlinks  | 
                    |
| 5448 | 
                        - return False  | 
                    |
| 5449 | 
                        -  | 
                    |
| 5450 | 
                        - def is_dir_error(  | 
                    |
| 5451 | 
                        - self: pathlib.Path,  | 
                    |
| 5452 | 
                        - /,  | 
                    |
| 5453 | 
                        - *,  | 
                    |
| 5454 | 
                        - follow_symlinks: bool = False,  | 
                    |
| 5455 | 
                        - ) -> bool:  | 
                    |
| 5456 | 
                        - del follow_symlinks  | 
                    |
| 5457 | 
                        - raise OSError(  | 
                    |
| 5458 | 
                        - errno.EACCES,  | 
                    |
| 5459 | 
                        - os.strerror(errno.EACCES),  | 
                    |
| 5460 | 
                        - str(self),  | 
                    |
| 2408 | 
                        + callables.make_file_readonly(  | 
                    |
| 2409 | 
                        + cli_helpers.config_filename(subsystem="vault"),  | 
                    |
| 2410 | 
                        + try_race_free_implementation=try_race_free_implementation,  | 
                    |
| 2411 | 
                        + )  | 
                    |
| 2412 | 
                        + result = runner.invoke(  | 
                    |
| 2413 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2414 | 
                        + ["--config", "--length=15", "--", DUMMY_SERVICE],  | 
                    |
| 2415 | 
                        + catch_exceptions=False,  | 
                    |
| 2416 | 
                        + )  | 
                    |
| 2417 | 
                        + assert result.error_exit(error="Cannot store vault settings:"), (  | 
                    |
| 2418 | 
                        + "expected error exit and known error message"  | 
                    |
| 5461 | 2419 | 
                        )  | 
                    
| 5462 | 2420 | 
                         | 
                    
| 5463 | 
                        - monkeypatch.setattr(pathlib.Path, "is_dir", is_dir_false)  | 
                    |
| 5464 | 
                        - assert cli_helpers.get_tempdir() == config_dir  | 
                    |
| 5465 | 
                        -  | 
                    |
| 5466 | 
                        - monkeypatch.setattr(pathlib.Path, "is_dir", is_dir_error)  | 
                    |
| 5467 | 
                        - assert cli_helpers.get_tempdir() == config_dir  | 
                    |
| 5468 | 
                        -  | 
                    |
| 5469 | 
                        - @Parametrize.DELETE_CONFIG_INPUT  | 
                    |
| 5470 | 
                        - def test_203_repeated_config_deletion(  | 
                    |
| 2421 | 
                        + def test_225e_store_config_fail_manual_custom_error(  | 
                    |
| 5471 | 2422 | 
                        self,  | 
                    
| 5472 | 
                        - command_line: list[str],  | 
                    |
| 5473 | 
                        - config: _types.VaultConfig,  | 
                    |
| 5474 | 
                        - result_config: _types.VaultConfig,  | 
                    |
| 5475 | 2423 | 
                        ) -> None:  | 
                    
| 5476 | 
                        - """Repeatedly removing the same parts of a configuration works."""  | 
                    |
| 5477 | 
                        - for start_config in [config, result_config]:  | 
                    |
| 2424 | 
                        + """OS-erroring with `--config` fails."""  | 
                    |
| 5478 | 2425 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5479 | 2426 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5480 | 2427 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5485,149 +2432,29 @@ Will replace with spam, okay? (Please say "y" or "n".): Boo.  | 
                  
| 5485 | 2432 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 5486 | 2433 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5487 | 2434 | 
                        runner=runner,  | 
                    
| 5488 | 
                        - vault_config=start_config,  | 
                    |
| 2435 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 5489 | 2436 | 
                        )  | 
                    
| 5490 | 2437 | 
                        )  | 
                    
| 2438 | 
                        + custom_error = "custom error message"  | 
                    |
| 2439 | 
                        +  | 
                    |
| 2440 | 
                        + def raiser(config: Any) -> None:  | 
                    |
| 2441 | 
                        + del config  | 
                    |
| 2442 | 
                        + raise RuntimeError(custom_error)  | 
                    |
| 2443 | 
                        +  | 
                    |
| 2444 | 
                        + monkeypatch.setattr(cli_helpers, "save_config", raiser)  | 
                    |
| 5491 | 2445 | 
                        result = runner.invoke(  | 
                    
| 5492 | 2446 | 
                        cli.derivepassphrase_vault,  | 
                    
| 5493 | 
                        - command_line,  | 
                    |
| 2447 | 
                        + ["--config", "--length=15", "--", DUMMY_SERVICE],  | 
                    |
| 5494 | 2448 | 
                        catch_exceptions=False,  | 
                    
| 5495 | 2449 | 
                        )  | 
                    
| 5496 | 
                        - assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 5497 | 
                        - "expected clean exit"  | 
                    |
| 5498 | 
                        - )  | 
                    |
| 5499 | 
                        - with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 5500 | 
                        - encoding="UTF-8"  | 
                    |
| 5501 | 
                        - ) as infile:  | 
                    |
| 5502 | 
                        - config_readback = json.load(infile)  | 
                    |
| 5503 | 
                        - assert config_readback == result_config  | 
                    |
| 5504 | 
                        -  | 
                    |
| 5505 | 
                        - def test_204_phrase_from_key_manually(self) -> None:  | 
                    |
| 5506 | 
                        - """The dummy service, key and config settings are consistent."""  | 
                    |
| 5507 | 
                        - assert (  | 
                    |
| 5508 | 
                        - vault.Vault(  | 
                    |
| 5509 | 
                        - phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS  | 
                    |
| 5510 | 
                        - ).generate(DUMMY_SERVICE)  | 
                    |
| 5511 | 
                        - == DUMMY_RESULT_KEY1  | 
                    |
| 5512 | 
                        - )  | 
                    |
| 5513 | 
                        -  | 
                    |
| 5514 | 
                        - @Parametrize.VALIDATION_FUNCTION_INPUT  | 
                    |
| 5515 | 
                        - def test_210a_validate_constraints_manually(  | 
                    |
| 5516 | 
                        - self,  | 
                    |
| 5517 | 
                        - vfunc: Callable[[click.Context, click.Parameter, Any], int | None],  | 
                    |
| 5518 | 
                        - input: int,  | 
                    |
| 5519 | 
                        - ) -> None:  | 
                    |
| 5520 | 
                        - """Command-line argument constraint validation works."""  | 
                    |
| 5521 | 
                        - ctx = cli.derivepassphrase_vault.make_context(cli.PROG_NAME, [])  | 
                    |
| 5522 | 
                        - param = cli.derivepassphrase_vault.params[0]  | 
                    |
| 5523 | 
                        - assert vfunc(ctx, param, input) == input  | 
                    |
| 5524 | 
                        -  | 
                    |
| 5525 | 
                        - @Parametrize.CONNECTION_HINTS  | 
                    |
| 5526 | 
                        - def test_227_get_suitable_ssh_keys(  | 
                    |
| 5527 | 
                        - self,  | 
                    |
| 5528 | 
                        - running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 5529 | 
                        - conn_hint: str,  | 
                    |
| 5530 | 
                        - ) -> None:  | 
                    |
| 5531 | 
                        - """[`cli_helpers.get_suitable_ssh_keys`][] works."""  | 
                    |
| 5532 | 
                        - with pytest.MonkeyPatch.context() as monkeypatch:  | 
                    |
| 5533 | 
                        - monkeypatch.setattr(  | 
                    |
| 5534 | 
                        - ssh_agent.SSHAgentClient,  | 
                    |
| 5535 | 
                        - "list_keys",  | 
                    |
| 5536 | 
                        - callables.list_keys,  | 
                    |
| 2450 | 
                        + assert result.error_exit(error=custom_error), (  | 
                    |
| 2451 | 
                        + "expected error exit and known error message"  | 
                    |
| 5537 | 2452 | 
                        )  | 
                    
| 5538 | 
                        - hint: ssh_agent.SSHAgentClient | _types.SSHAgentSocket | None  | 
                    |
| 5539 | 
                        - # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 5540 | 
                        - # matching.  | 
                    |
| 5541 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 5542 | 
                        - if conn_hint == "client":  | 
                    |
| 5543 | 
                        - hint = ssh_agent.SSHAgentClient()  | 
                    |
| 5544 | 
                        - elif conn_hint == "socket":  | 
                    |
| 5545 | 
                        - if isinstance(  | 
                    |
| 5546 | 
                        - running_ssh_agent.socket, str  | 
                    |
| 5547 | 
                        - ): # pragma: no cover  | 
                    |
| 5548 | 
                        - if not hasattr(socket, "AF_UNIX"):  | 
                    |
| 5549 | 
                        -                        pytest.skip("socket module does not support AF_UNIX")
                       | 
                    |
| 5550 | 
                        - # socket.AF_UNIX is not defined everywhere.  | 
                    |
| 5551 | 
                        - hint = socket.socket(family=socket.AF_UNIX) # type: ignore[attr-defined]  | 
                    |
| 5552 | 
                        - hint.connect(running_ssh_agent.socket)  | 
                    |
| 5553 | 
                        - else: # pragma: no cover  | 
                    |
| 5554 | 
                        - hint = running_ssh_agent.socket()  | 
                    |
| 5555 | 
                        - else:  | 
                    |
| 5556 | 
                        - assert conn_hint == "none"  | 
                    |
| 5557 | 
                        - hint = None  | 
                    |
| 5558 | 
                        - exception: Exception | None = None  | 
                    |
| 5559 | 
                        - try:  | 
                    |
| 5560 | 
                        - list(cli_helpers.get_suitable_ssh_keys(hint))  | 
                    |
| 5561 | 
                        - except RuntimeError: # pragma: no cover  | 
                    |
| 5562 | 
                        - pass  | 
                    |
| 5563 | 
                        - except Exception as e: # noqa: BLE001 # pragma: no cover  | 
                    |
| 5564 | 
                        - exception = e  | 
                    |
| 5565 | 
                        - finally:  | 
                    |
| 5566 | 
                        - assert exception is None, (  | 
                    |
| 5567 | 
                        - "exception querying suitable SSH keys"  | 
                    |
| 5568 | 
                        - )  | 
                    |
| 5569 | 
                        -  | 
                    |
| 5570 | 
                        - @Parametrize.KEY_TO_PHRASE_SETTINGS  | 
                    |
| 5571 | 
                        - def test_400_key_to_phrase(  | 
                    |
| 5572 | 
                        - self,  | 
                    |
| 5573 | 
                        - ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,  | 
                    |
| 5574 | 
                        - list_keys_action: ListKeysAction | None,  | 
                    |
| 5575 | 
                        - system_support_action: SystemSupportAction | None,  | 
                    |
| 5576 | 
                        - address_action: SocketAddressAction | None,  | 
                    |
| 5577 | 
                        - sign_action: SignAction,  | 
                    |
| 5578 | 
                        - pattern: str,  | 
                    |
| 5579 | 
                        - ) -> None:  | 
                    |
| 5580 | 
                        - """All errors in [`cli_helpers.key_to_phrase`][] are handled."""  | 
                    |
| 5581 | 
                        -  | 
                    |
| 5582 | 
                        - class ErrCallback(BaseException):  | 
                    |
| 5583 | 
                        - def __init__(self, *args: Any, **kwargs: Any) -> None:  | 
                    |
| 5584 | 
                        - super().__init__(*args[:1])  | 
                    |
| 5585 | 
                        - self.args = args  | 
                    |
| 5586 | 
                        - self.kwargs = kwargs  | 
                    |
| 5587 | 2453 | 
                         | 
                    
| 5588 | 
                        - def err(*args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 5589 | 
                        - raise ErrCallback(*args, **_kwargs)  | 
                    |
| 5590 | 
                        -  | 
                    |
| 5591 | 
                        - with pytest.MonkeyPatch.context() as monkeypatch:  | 
                    |
| 5592 | 
                        - loaded_keys = list(  | 
                    |
| 5593 | 
                        - ssh_agent_client_with_test_keys_loaded.list_keys()  | 
                    |
| 5594 | 
                        - )  | 
                    |
| 5595 | 
                        - loaded_key = base64.standard_b64encode(loaded_keys[0][0])  | 
                    |
| 5596 | 
                        - monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", sign_action)  | 
                    |
| 5597 | 
                        - if list_keys_action:  | 
                    |
| 5598 | 
                        - monkeypatch.setattr(  | 
                    |
| 5599 | 
                        - ssh_agent.SSHAgentClient, "list_keys", list_keys_action  | 
                    |
| 5600 | 
                        - )  | 
                    |
| 5601 | 
                        - if address_action:  | 
                    |
| 5602 | 
                        - address_action(monkeypatch)  | 
                    |
| 5603 | 
                        - if system_support_action:  | 
                    |
| 5604 | 
                        - system_support_action(monkeypatch)  | 
                    |
| 5605 | 
                        - with pytest.raises(ErrCallback, match=pattern) as excinfo:  | 
                    |
| 5606 | 
                        - cli_helpers.key_to_phrase(loaded_key, error_callback=err)  | 
                    |
| 5607 | 
                        - if list_keys_action == ListKeysAction.FAIL_RUNTIME:  | 
                    |
| 5608 | 
                        - assert excinfo.value.kwargs  | 
                    |
| 5609 | 
                        - assert isinstance(  | 
                    |
| 5610 | 
                        - excinfo.value.kwargs["exc_info"],  | 
                    |
| 5611 | 
                        - ssh_agent.SSHAgentFailedError,  | 
                    |
| 5612 | 
                        - )  | 
                    |
| 5613 | 
                        - assert excinfo.value.kwargs["exc_info"].__context__ is not None  | 
                    |
| 5614 | 
                        - assert isinstance(  | 
                    |
| 5615 | 
                        - excinfo.value.kwargs["exc_info"].__context__,  | 
                    |
| 5616 | 
                        - ssh_agent.TrailingDataError,  | 
                    |
| 5617 | 
                        - )  | 
                    |
| 5618 | 
                        -  | 
                    |
| 5619 | 
                        -  | 
                    |
| 5620 | 
                        -# TODO(the-13th-letter): Remove this class in v1.0.  | 
                    |
| 5621 | 
                        -# https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#upgrading-to-v1.0  | 
                    |
| 5622 | 
                        -class TestCLITransition:  | 
                    |
| 5623 | 
                        - """Transition tests for the command-line interface up to v1.0."""  | 
                    |
| 5624 | 
                        -  | 
                    |
| 5625 | 
                        - @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 5626 | 
                        - def test_110_load_config_backup(  | 
                    |
| 2454 | 
                        + def test_225f_store_config_fail_unset_and_set_same_settings(  | 
                    |
| 5627 | 2455 | 
                        self,  | 
                    
| 5628 | 
                        - config: Any,  | 
                    |
| 5629 | 2456 | 
                        ) -> None:  | 
                    
| 5630 | 
                        - """Loading the old settings file works."""  | 
                    |
| 2457 | 
                        + """Issuing conflicting settings to `--config` fails."""  | 
                    |
| 5631 | 2458 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5632 | 2459 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5633 | 2460 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5635,22 +2462,33 @@ class TestCLITransition:  | 
                  
| 5635 | 2462 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 5636 | 2463 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 5637 | 2464 | 
                        stack.enter_context(  | 
                    
| 5638 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 2465 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 5639 | 2466 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5640 | 2467 | 
                        runner=runner,  | 
                    
| 2468 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 2469 | 
                        + )  | 
                    |
| 5641 | 2470 | 
                        )  | 
                    
| 2471 | 
                        + result = runner.invoke(  | 
                    |
| 2472 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2473 | 
                        + [  | 
                    |
| 2474 | 
                        + "--config",  | 
                    |
| 2475 | 
                        + "--unset=length",  | 
                    |
| 2476 | 
                        + "--length=15",  | 
                    |
| 2477 | 
                        + "--",  | 
                    |
| 2478 | 
                        + DUMMY_SERVICE,  | 
                    |
| 2479 | 
                        + ],  | 
                    |
| 2480 | 
                        + catch_exceptions=False,  | 
                    |
| 5642 | 2481 | 
                        )  | 
                    
| 5643 | 
                        - cli_helpers.config_filename(  | 
                    |
| 5644 | 
                        - subsystem="old settings.json"  | 
                    |
| 5645 | 
                        - ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 5646 | 
                        - assert cli_helpers.migrate_and_load_old_config()[0] == config  | 
                    |
| 2482 | 
                        + assert result.error_exit(  | 
                    |
| 2483 | 
                        + error="Attempted to unset and set --length at the same time."  | 
                    |
| 2484 | 
                        + ), "expected error exit and known error message"  | 
                    |
| 5647 | 2485 | 
                         | 
                    
| 5648 | 
                        - @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 5649 | 
                        - def test_111_migrate_config(  | 
                    |
| 2486 | 
                        + def test_225g_store_config_fail_manual_ssh_agent_no_keys_loaded(  | 
                    |
| 5650 | 2487 | 
                        self,  | 
                    
| 5651 | 
                        - config: Any,  | 
                    |
| 2488 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 5652 | 2489 | 
                        ) -> None:  | 
                    
| 5653 | 
                        - """Migrating the old settings file works."""  | 
                    |
| 2490 | 
                        + """Not holding any SSH keys during `--config --key` fails."""  | 
                    |
| 2491 | 
                        + del running_ssh_agent  | 
                    |
| 5654 | 2492 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5655 | 2493 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5656 | 2494 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5658,22 +2496,35 @@ class TestCLITransition:  | 
                  
| 5658 | 2496 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 5659 | 2497 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 5660 | 2498 | 
                        stack.enter_context(  | 
                    
| 5661 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 2499 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 5662 | 2500 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5663 | 2501 | 
                        runner=runner,  | 
                    
| 2502 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 2503 | 
                        + )  | 
                    |
| 2504 | 
                        + )  | 
                    |
| 2505 | 
                        +  | 
                    |
| 2506 | 
                        + def func(  | 
                    |
| 2507 | 
                        + *_args: Any,  | 
                    |
| 2508 | 
                        + **_kwargs: Any,  | 
                    |
| 2509 | 
                        + ) -> list[_types.SSHKeyCommentPair]:  | 
                    |
| 2510 | 
                        + return []  | 
                    |
| 2511 | 
                        +  | 
                    |
| 2512 | 
                        + monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", func)  | 
                    |
| 2513 | 
                        + result = runner.invoke(  | 
                    |
| 2514 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2515 | 
                        + ["--key", "--config"],  | 
                    |
| 2516 | 
                        + catch_exceptions=False,  | 
                    |
| 5664 | 2517 | 
                        )  | 
                    
| 2518 | 
                        + assert result.error_exit(error="no keys suitable"), (  | 
                    |
| 2519 | 
                        + "expected error exit and known error message"  | 
                    |
| 5665 | 2520 | 
                        )  | 
                    
| 5666 | 
                        - cli_helpers.config_filename(  | 
                    |
| 5667 | 
                        - subsystem="old settings.json"  | 
                    |
| 5668 | 
                        - ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 5669 | 
                        - assert cli_helpers.migrate_and_load_old_config() == (config, None)  | 
                    |
| 5670 | 2521 | 
                         | 
                    
| 5671 | 
                        - @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 5672 | 
                        - def test_112_migrate_config_error(  | 
                    |
| 2522 | 
                        + def test_225h_store_config_fail_manual_ssh_agent_runtime_error(  | 
                    |
| 5673 | 2523 | 
                        self,  | 
                    
| 5674 | 
                        - config: Any,  | 
                    |
| 2524 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 5675 | 2525 | 
                        ) -> None:  | 
                    
| 5676 | 
                        - """Migrating the old settings file atop a directory fails."""  | 
                    |
| 2526 | 
                        + """The SSH agent erroring during `--config --key` fails."""  | 
                    |
| 2527 | 
                        + del running_ssh_agent  | 
                    |
| 5677 | 2528 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5678 | 2529 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5679 | 2530 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5681,29 +2532,32 @@ class TestCLITransition:  | 
                  
| 5681 | 2532 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 5682 | 2533 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 5683 | 2534 | 
                        stack.enter_context(  | 
                    
| 5684 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 2535 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 5685 | 2536 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5686 | 2537 | 
                        runner=runner,  | 
                    
| 2538 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 5687 | 2539 | 
                        )  | 
                    
| 5688 | 2540 | 
                        )  | 
                    
| 5689 | 
                        - cli_helpers.config_filename(  | 
                    |
| 5690 | 
                        - subsystem="old settings.json"  | 
                    |
| 5691 | 
                        - ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 5692 | 
                        - cli_helpers.config_filename(subsystem="vault").mkdir(  | 
                    |
| 5693 | 
                        - parents=True, exist_ok=True  | 
                    |
| 2541 | 
                        +  | 
                    |
| 2542 | 
                        + def raiser(*_args: Any, **_kwargs: Any) -> None:  | 
                    |
| 2543 | 
                        + raise ssh_agent.TrailingDataError()  | 
                    |
| 2544 | 
                        +  | 
                    |
| 2545 | 
                        + monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", raiser)  | 
                    |
| 2546 | 
                        + result = runner.invoke(  | 
                    |
| 2547 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2548 | 
                        + ["--key", "--config"],  | 
                    |
| 2549 | 
                        + catch_exceptions=False,  | 
                    |
| 5694 | 2550 | 
                        )  | 
                    
| 5695 | 
                        - config2, err = cli_helpers.migrate_and_load_old_config()  | 
                    |
| 5696 | 
                        - assert config2 == config  | 
                    |
| 5697 | 
                        - assert isinstance(err, OSError)  | 
                    |
| 5698 | 
                        - # The Annoying OS uses EEXIST, other OSes use EISDIR.  | 
                    |
| 5699 | 
                        -            assert err.errno in {errno.EISDIR, errno.EEXIST}
                       | 
                    |
| 2551 | 
                        + assert result.error_exit(  | 
                    |
| 2552 | 
                        + error="violates the communication protocol."  | 
                    |
| 2553 | 
                        + ), "expected error exit and known error message"  | 
                    |
| 5700 | 2554 | 
                         | 
                    
| 5701 | 
                        - @Parametrize.BAD_CONFIGS  | 
                    |
| 5702 | 
                        - def test_113_migrate_config_error_bad_config_value(  | 
                    |
| 2555 | 
                        + def test_225i_store_config_fail_manual_ssh_agent_refuses(  | 
                    |
| 5703 | 2556 | 
                        self,  | 
                    
| 5704 | 
                        - config: Any,  | 
                    |
| 2557 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 5705 | 2558 | 
                        ) -> None:  | 
                    
| 5706 | 
                        - """Migrating an invalid old settings file fails."""  | 
                    |
| 2559 | 
                        + """The SSH agent refusing during `--config --key` fails."""  | 
                    |
| 2560 | 
                        + del running_ssh_agent  | 
                    |
| 5707 | 2561 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5708 | 2562 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5709 | 2563 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5711,25 +2565,30 @@ class TestCLITransition:  | 
                  
| 5711 | 2565 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 5712 | 2566 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 5713 | 2567 | 
                        stack.enter_context(  | 
                    
| 5714 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 2568 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 5715 | 2569 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5716 | 2570 | 
                        runner=runner,  | 
                    
| 2571 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 5717 | 2572 | 
                        )  | 
                    
| 5718 | 2573 | 
                        )  | 
                    
| 5719 | 
                        - cli_helpers.config_filename(  | 
                    |
| 5720 | 
                        - subsystem="old settings.json"  | 
                    |
| 5721 | 
                        - ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 5722 | 
                        - with pytest.raises(  | 
                    |
| 5723 | 
                        - ValueError, match=cli_helpers.INVALID_VAULT_CONFIG  | 
                    |
| 5724 | 
                        - ):  | 
                    |
| 5725 | 
                        - cli_helpers.migrate_and_load_old_config()  | 
                    |
| 5726 | 2574 | 
                         | 
                    
| 5727 | 
                        - def test_200_forward_export_vault_path_parameter(  | 
                    |
| 5728 | 
                        - self,  | 
                    |
| 5729 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 5730 | 
                        - ) -> None:  | 
                    |
| 5731 | 
                        - """Forwarding arguments from "export" to "export vault" works."""  | 
                    |
| 5732 | 
                        -        pytest.importorskip("cryptography", minversion="38.0")
                       | 
                    |
| 2575 | 
                        + def func(*_args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 2576 | 
                        + raise ssh_agent.SSHAgentFailedError(  | 
                    |
| 2577 | 
                        + _types.SSH_AGENT.FAILURE, b""  | 
                    |
| 2578 | 
                        + )  | 
                    |
| 2579 | 
                        +  | 
                    |
| 2580 | 
                        + monkeypatch.setattr(ssh_agent.SSHAgentClient, "list_keys", func)  | 
                    |
| 2581 | 
                        + result = runner.invoke(  | 
                    |
| 2582 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2583 | 
                        + ["--key", "--config"],  | 
                    |
| 2584 | 
                        + catch_exceptions=False,  | 
                    |
| 2585 | 
                        + )  | 
                    |
| 2586 | 
                        + assert result.error_exit(error="refused to"), (  | 
                    |
| 2587 | 
                        + "expected error exit and known error message"  | 
                    |
| 2588 | 
                        + )  | 
                    |
| 2589 | 
                        +  | 
                    |
| 2590 | 
                        + def test_226_no_arguments(self) -> None:  | 
                    |
| 2591 | 
                        + """Calling `derivepassphrase vault` without any arguments fails."""  | 
                    |
| 5733 | 2592 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5734 | 2593 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5735 | 2594 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5737,33 +2596,22 @@ class TestCLITransition:  | 
                  
| 5737 | 2596 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 5738 | 2597 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 5739 | 2598 | 
                        stack.enter_context(  | 
                    
| 5740 | 
                        - pytest_machinery.isolated_vault_exporter_config(  | 
                    |
| 2599 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 5741 | 2600 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5742 | 2601 | 
                        runner=runner,  | 
                    
| 5743 | 
                        - vault_config=data.VAULT_V03_CONFIG,  | 
                    |
| 5744 | 
                        - vault_key=data.VAULT_MASTER_KEY,  | 
                    |
| 5745 | 2602 | 
                        )  | 
                    
| 5746 | 2603 | 
                        )  | 
                    
| 5747 | 
                        -            monkeypatch.setenv("VAULT_KEY", data.VAULT_MASTER_KEY)
                       | 
                    |
| 5748 | 2604 | 
                        result = runner.invoke(  | 
                    
| 5749 | 
                        - cli.derivepassphrase,  | 
                    |
| 5750 | 
                        - ["export", "VAULT_PATH"],  | 
                    |
| 5751 | 
                        - )  | 
                    |
| 5752 | 
                        - assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 5753 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5754 | 
                        - "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 5755 | 
                        - )  | 
                    |
| 5756 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5757 | 
                        - 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 2605 | 
                        + cli.derivepassphrase_vault, [], catch_exceptions=False  | 
                    |
| 5758 | 2606 | 
                        )  | 
                    
| 5759 | 
                        - assert json.loads(result.stdout) == data.VAULT_V03_CONFIG_DATA  | 
                    |
| 2607 | 
                        + assert result.error_exit(  | 
                    |
| 2608 | 
                        + error="Deriving a passphrase requires a SERVICE"  | 
                    |
| 2609 | 
                        + ), "expected error exit and known error message"  | 
                    |
| 5760 | 2610 | 
                         | 
                    
| 5761 | 
                        - def test_201_forward_export_vault_empty_commandline(  | 
                    |
| 2611 | 
                        + def test_226a_no_passphrase_or_key(  | 
                    |
| 5762 | 2612 | 
                        self,  | 
                    
| 5763 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 5764 | 2613 | 
                        ) -> None:  | 
                    
| 5765 | 
                        - """Deferring from "export" to "export vault" works."""  | 
                    |
| 5766 | 
                        -        pytest.importorskip("cryptography", minversion="38.0")
                       | 
                    |
| 2614 | 
                        + """Deriving a passphrase without a passphrase or key fails."""  | 
                    |
| 5767 | 2615 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5768 | 2616 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5769 | 2617 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5777,28 +2625,24 @@ class TestCLITransition:  | 
                  
| 5777 | 2625 | 
                        )  | 
                    
| 5778 | 2626 | 
                        )  | 
                    
| 5779 | 2627 | 
                        result = runner.invoke(  | 
                    
| 5780 | 
                        - cli.derivepassphrase,  | 
                    |
| 5781 | 
                        - ["export"],  | 
                    |
| 5782 | 
                        - )  | 
                    |
| 5783 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5784 | 
                        - "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 5785 | 
                        - )  | 
                    |
| 5786 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5787 | 
                        - 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 2628 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2629 | 
                        + ["--", DUMMY_SERVICE],  | 
                    |
| 2630 | 
                        + catch_exceptions=False,  | 
                    |
| 5788 | 2631 | 
                        )  | 
                    
| 5789 | 
                        - assert result.error_exit(error="Missing argument 'PATH'"), (  | 
                    |
| 5790 | 
                        - "expected error exit and known error type"  | 
                    |
| 2632 | 
                        + assert result.error_exit(error="No passphrase or key was given"), (  | 
                    |
| 2633 | 
                        + "expected error exit and known error message"  | 
                    |
| 5791 | 2634 | 
                        )  | 
                    
| 5792 | 2635 | 
                         | 
                    
| 5793 | 
                        - @Parametrize.CHARSET_NAME  | 
                    |
| 5794 | 
                        - def test_210_forward_vault_disable_character_set(  | 
                    |
| 2636 | 
                        + def test_230_config_directory_nonexistant(  | 
                    |
| 5795 | 2637 | 
                        self,  | 
                    
| 5796 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 5797 | 
                        - charset_name: str,  | 
                    |
| 5798 | 2638 | 
                        ) -> None:  | 
                    
| 5799 | 
                        - """Forwarding arguments from top-level to "vault" works."""  | 
                    |
| 5800 | 
                        -        option = f"--{charset_name}"
                       | 
                    |
| 5801 | 
                        -        charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
                       | 
                    |
| 2639 | 
                        + """Running without an existing config directory works.  | 
                    |
| 2640 | 
                        +  | 
                    |
| 2641 | 
                        + This is a regression test; see [issue\u00a0#6][] for context.  | 
                    |
| 2642 | 
                        +  | 
                    |
| 2643 | 
                        + [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6  | 
                    |
| 2644 | 
                        +  | 
                    |
| 2645 | 
                        + """  | 
                    |
| 5802 | 2646 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5803 | 2647 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5804 | 2648 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5811,34 +2655,40 @@ class TestCLITransition:  | 
                  
| 5811 | 2655 | 
                        runner=runner,  | 
                    
| 5812 | 2656 | 
                        )  | 
                    
| 5813 | 2657 | 
                        )  | 
                    
| 5814 | 
                        - monkeypatch.setattr(  | 
                    |
| 5815 | 
                        - cli_helpers,  | 
                    |
| 5816 | 
                        - "prompt_for_passphrase",  | 
                    |
| 5817 | 
                        - callables.auto_prompt,  | 
                    |
| 5818 | 
                        - )  | 
                    |
| 2658 | 
                        + with contextlib.suppress(FileNotFoundError):  | 
                    |
| 2659 | 
                        + shutil.rmtree(cli_helpers.config_filename(subsystem=None))  | 
                    |
| 5819 | 2660 | 
                        result = runner.invoke(  | 
                    
| 5820 | 
                        - cli.derivepassphrase,  | 
                    |
| 5821 | 
                        - [option, "0", "-p", "--", DUMMY_SERVICE],  | 
                    |
| 5822 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 2661 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2662 | 
                        + ["--config", "-p"],  | 
                    |
| 5823 | 2663 | 
                        catch_exceptions=False,  | 
                    
| 2664 | 
                        + input="abc\n",  | 
                    |
| 5824 | 2665 | 
                        )  | 
                    
| 5825 | 
                        - assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 5826 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5827 | 
                        - "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 5828 | 
                        - )  | 
                    |
| 5829 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5830 | 
                        - 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 5831 | 
                        - )  | 
                    |
| 5832 | 
                        - for c in charset:  | 
                    |
| 5833 | 
                        - assert c not in result.stdout, (  | 
                    |
| 5834 | 
                        -                f"derived password contains forbidden character {c!r}"
                       | 
                    |
| 2666 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 2667 | 
                        + assert result.stderr == "Passphrase:", (  | 
                    |
| 2668 | 
                        + "program unexpectedly failed?!"  | 
                    |
| 5835 | 2669 | 
                        )  | 
                    
| 2670 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 2671 | 
                        + encoding="UTF-8"  | 
                    |
| 2672 | 
                        + ) as infile:  | 
                    |
| 2673 | 
                        + config_readback = json.load(infile)  | 
                    |
| 2674 | 
                        +            assert config_readback == {
                       | 
                    |
| 2675 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 2676 | 
                        +                "services": {},
                       | 
                    |
| 2677 | 
                        + }, "config mismatch"  | 
                    |
| 2678 | 
                        +  | 
                    |
| 2679 | 
                        + def test_230a_config_directory_not_a_file(  | 
                    |
| 2680 | 
                        + self,  | 
                    |
| 2681 | 
                        + ) -> None:  | 
                    |
| 2682 | 
                        + """Erroring without an existing config directory errors normally.  | 
                    |
| 2683 | 
                        +  | 
                    |
| 2684 | 
                        + That is, the missing configuration directory does not cause any  | 
                    |
| 2685 | 
                        + errors by itself.  | 
                    |
| 2686 | 
                        +  | 
                    |
| 2687 | 
                        + This is a regression test; see [issue\u00a0#6][] for context.  | 
                    |
| 5836 | 2688 | 
                         | 
                    
| 5837 | 
                        - def test_211_forward_vault_empty_command_line(  | 
                    |
| 5838 | 
                        - self,  | 
                    |
| 5839 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 5840 | 
                        - ) -> None:  | 
                    |
| 5841 | 
                        - """Deferring from top-level to "vault" works."""  | 
                    |
| 2689 | 
                        + [issue #6]: https://github.com/the-13th-letter/derivepassphrase/issues/6  | 
                    |
| 2690 | 
                        +  | 
                    |
| 2691 | 
                        + """  | 
                    |
| 5842 | 2692 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5843 | 2693 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5844 | 2694 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5851,28 +2701,33 @@ class TestCLITransition:  | 
                  
| 5851 | 2701 | 
                        runner=runner,  | 
                    
| 5852 | 2702 | 
                        )  | 
                    
| 5853 | 2703 | 
                        )  | 
                    
| 2704 | 
                        + save_config_ = cli_helpers.save_config  | 
                    |
| 2705 | 
                        +  | 
                    |
| 2706 | 
                        + def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any:  | 
                    |
| 2707 | 
                        + config_dir = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 2708 | 
                        + with contextlib.suppress(FileNotFoundError):  | 
                    |
| 2709 | 
                        + shutil.rmtree(config_dir)  | 
                    |
| 2710 | 
                        +                config_dir.write_text("Obstruction!!\n")
                       | 
                    |
| 2711 | 
                        + monkeypatch.setattr(cli_helpers, "save_config", save_config_)  | 
                    |
| 2712 | 
                        + return save_config_(*args, **kwargs)  | 
                    |
| 2713 | 
                        +  | 
                    |
| 2714 | 
                        + monkeypatch.setattr(  | 
                    |
| 2715 | 
                        + cli_helpers, "save_config", obstruct_config_saving  | 
                    |
| 2716 | 
                        + )  | 
                    |
| 5854 | 2717 | 
                        result = runner.invoke(  | 
                    
| 5855 | 
                        - cli.derivepassphrase,  | 
                    |
| 5856 | 
                        - [],  | 
                    |
| 5857 | 
                        - input=DUMMY_PASSPHRASE,  | 
                    |
| 2718 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2719 | 
                        + ["--config", "-p"],  | 
                    |
| 5858 | 2720 | 
                        catch_exceptions=False,  | 
                    
| 2721 | 
                        + input="abc\n",  | 
                    |
| 5859 | 2722 | 
                        )  | 
                    
| 5860 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5861 | 
                        - "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 5862 | 
                        - )  | 
                    |
| 5863 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5864 | 
                        - 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 2723 | 
                        + assert result.error_exit(error="Cannot store vault settings:"), (  | 
                    |
| 2724 | 
                        + "expected error exit and known error message"  | 
                    |
| 5865 | 2725 | 
                        )  | 
                    
| 5866 | 
                        - assert result.error_exit(  | 
                    |
| 5867 | 
                        - error="Deriving a passphrase requires a SERVICE."  | 
                    |
| 5868 | 
                        - ), "expected error exit and known error type"  | 
                    |
| 5869 | 2726 | 
                         | 
                    
| 5870 | 
                        - def test_300_export_using_old_config_file(  | 
                    |
| 2727 | 
                        + def test_230b_store_config_custom_error(  | 
                    |
| 5871 | 2728 | 
                        self,  | 
                    
| 5872 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 5873 | 2729 | 
                        ) -> None:  | 
                    
| 5874 | 
                        - """Exporting from (and migrating) the old settings file works."""  | 
                    |
| 5875 | 
                        - caplog.set_level(logging.INFO)  | 
                    |
| 2730 | 
                        + """Storing the configuration reacts even to weird errors."""  | 
                    |
| 5876 | 2731 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5877 | 2732 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5878 | 2733 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5885,34 +2740,33 @@ class TestCLITransition:  | 
                  
| 5885 | 2740 | 
                        runner=runner,  | 
                    
| 5886 | 2741 | 
                        )  | 
                    
| 5887 | 2742 | 
                        )  | 
                    
| 5888 | 
                        - cli_helpers.config_filename(  | 
                    |
| 5889 | 
                        - subsystem="old settings.json"  | 
                    |
| 5890 | 
                        - ).write_text(  | 
                    |
| 5891 | 
                        - json.dumps(  | 
                    |
| 5892 | 
                        -                    {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    |
| 5893 | 
                        - indent=2,  | 
                    |
| 5894 | 
                        - )  | 
                    |
| 5895 | 
                        - + "\n",  | 
                    |
| 5896 | 
                        - encoding="UTF-8",  | 
                    |
| 5897 | 
                        - )  | 
                    |
| 2743 | 
                        + custom_error = "custom error message"  | 
                    |
| 2744 | 
                        +  | 
                    |
| 2745 | 
                        + def raiser(config: Any) -> None:  | 
                    |
| 2746 | 
                        + del config  | 
                    |
| 2747 | 
                        + raise RuntimeError(custom_error)  | 
                    |
| 2748 | 
                        +  | 
                    |
| 2749 | 
                        + monkeypatch.setattr(cli_helpers, "save_config", raiser)  | 
                    |
| 5898 | 2750 | 
                        result = runner.invoke(  | 
                    
| 5899 | 2751 | 
                        cli.derivepassphrase_vault,  | 
                    
| 5900 | 
                        - ["--export", "-"],  | 
                    |
| 2752 | 
                        + ["--config", "-p"],  | 
                    |
| 5901 | 2753 | 
                        catch_exceptions=False,  | 
                    
| 2754 | 
                        + input="abc\n",  | 
                    |
| 2755 | 
                        + )  | 
                    |
| 2756 | 
                        + assert result.error_exit(error=custom_error), (  | 
                    |
| 2757 | 
                        + "expected error exit and known error message"  | 
                    |
| 5902 | 2758 | 
                        )  | 
                    
| 5903 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 5904 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5905 | 
                        - "v0.1-style config file", caplog.record_tuples  | 
                    |
| 5906 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 5907 | 
                        - assert machinery.deprecation_info_emitted(  | 
                    |
| 5908 | 
                        - "Successfully migrated to ", caplog.record_tuples  | 
                    |
| 5909 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 5910 | 2759 | 
                         | 
                    
| 5911 | 
                        - def test_300a_export_using_old_config_file_migration_error(  | 
                    |
| 2760 | 
                        + @Parametrize.UNICODE_NORMALIZATION_WARNING_INPUTS  | 
                    |
| 2761 | 
                        + def test_300_unicode_normalization_form_warning(  | 
                    |
| 5912 | 2762 | 
                        self,  | 
                    
| 5913 | 2763 | 
                        caplog: pytest.LogCaptureFixture,  | 
                    
| 2764 | 
                        + main_config: str,  | 
                    |
| 2765 | 
                        + command_line: list[str],  | 
                    |
| 2766 | 
                        + input: str | None,  | 
                    |
| 2767 | 
                        + warning_message: str,  | 
                    |
| 5914 | 2768 | 
                        ) -> None:  | 
                    
| 5915 | 
                        - """Exporting from (and not migrating) the old settings file fails."""  | 
                    |
| 2769 | 
                        + """Using unnormalized Unicode passphrases warns."""  | 
                    |
| 5916 | 2770 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5917 | 2771 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5918 | 2772 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5920,49 +2774,37 @@ class TestCLITransition:  | 
                  
| 5920 | 2774 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 5921 | 2775 | 
                        monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    
| 5922 | 2776 | 
                        stack.enter_context(  | 
                    
| 5923 | 
                        - pytest_machinery.isolated_config(  | 
                    |
| 2777 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 5924 | 2778 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5925 | 2779 | 
                        runner=runner,  | 
                    
| 2780 | 
                        +                    vault_config={
                       | 
                    |
| 2781 | 
                        +                        "services": {
                       | 
                    |
| 2782 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()  | 
                    |
| 2783 | 
                        + }  | 
                    |
| 2784 | 
                        + },  | 
                    |
| 2785 | 
                        + main_config_str=main_config,  | 
                    |
| 5926 | 2786 | 
                        )  | 
                    
| 5927 | 2787 | 
                        )  | 
                    
| 5928 | 
                        - cli_helpers.config_filename(  | 
                    |
| 5929 | 
                        - subsystem="old settings.json"  | 
                    |
| 5930 | 
                        - ).write_text(  | 
                    |
| 5931 | 
                        - json.dumps(  | 
                    |
| 5932 | 
                        -                    {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    |
| 5933 | 
                        - indent=2,  | 
                    |
| 5934 | 
                        - )  | 
                    |
| 5935 | 
                        - + "\n",  | 
                    |
| 5936 | 
                        - encoding="UTF-8",  | 
                    |
| 5937 | 
                        - )  | 
                    |
| 5938 | 
                        -  | 
                    |
| 5939 | 
                        - def raiser(*_args: Any, **_kwargs: Any) -> None:  | 
                    |
| 5940 | 
                        - raise OSError(  | 
                    |
| 5941 | 
                        - errno.EACCES,  | 
                    |
| 5942 | 
                        - os.strerror(errno.EACCES),  | 
                    |
| 5943 | 
                        - cli_helpers.config_filename(subsystem="vault"),  | 
                    |
| 5944 | 
                        - )  | 
                    |
| 5945 | 
                        -  | 
                    |
| 5946 | 
                        - monkeypatch.setattr(os, "replace", raiser)  | 
                    |
| 5947 | 
                        - monkeypatch.setattr(pathlib.Path, "rename", raiser)  | 
                    |
| 5948 | 2788 | 
                        result = runner.invoke(  | 
                    
| 5949 | 2789 | 
                        cli.derivepassphrase_vault,  | 
                    
| 5950 | 
                        - ["--export", "-"],  | 
                    |
| 2790 | 
                        + ["--debug", *command_line],  | 
                    |
| 5951 | 2791 | 
                        catch_exceptions=False,  | 
                    
| 2792 | 
                        + input=input,  | 
                    |
| 5952 | 2793 | 
                        )  | 
                    
| 5953 | 2794 | 
                        assert result.clean_exit(), "expected clean exit"  | 
                    
| 5954 | 
                        - assert machinery.deprecation_warning_emitted(  | 
                    |
| 5955 | 
                        - "v0.1-style config file", caplog.record_tuples  | 
                    |
| 5956 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 5957 | 2795 | 
                        assert machinery.warning_emitted(  | 
                    
| 5958 | 
                        - "Failed to migrate to ", caplog.record_tuples  | 
                    |
| 2796 | 
                        + warning_message, caplog.record_tuples  | 
                    |
| 5959 | 2797 | 
                        ), "expected known warning message in stderr"  | 
                    
| 5960 | 2798 | 
                         | 
                    
| 5961 | 
                        - def test_400_completion_service_name_old_config_file(  | 
                    |
| 2799 | 
                        + @Parametrize.UNICODE_NORMALIZATION_ERROR_INPUTS  | 
                    |
| 2800 | 
                        + def test_301_unicode_normalization_form_error(  | 
                    |
| 5962 | 2801 | 
                        self,  | 
                    
| 2802 | 
                        + main_config: str,  | 
                    |
| 2803 | 
                        + command_line: list[str],  | 
                    |
| 2804 | 
                        + input: str | None,  | 
                    |
| 2805 | 
                        + error_message: str,  | 
                    |
| 5963 | 2806 | 
                        ) -> None:  | 
                    
| 5964 | 
                        - """Completing service names from the old settings file works."""  | 
                    |
| 5965 | 
                        -        config = {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}
                       | 
                    |
| 2807 | 
                        + """Using unknown Unicode normalization forms fails."""  | 
                    |
| 5966 | 2808 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 5967 | 2809 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 5968 | 2810 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -5973,138 +2815,33 @@ class TestCLITransition:  | 
                  
| 5973 | 2815 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 5974 | 2816 | 
                        monkeypatch=monkeypatch,  | 
                    
| 5975 | 2817 | 
                        runner=runner,  | 
                    
| 5976 | 
                        - vault_config=config,  | 
                    |
| 5977 | 
                        - )  | 
                    |
| 2818 | 
                        +                    vault_config={
                       | 
                    |
| 2819 | 
                        +                        "services": {
                       | 
                    |
| 2820 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()  | 
                    |
| 2821 | 
                        + }  | 
                    |
| 2822 | 
                        + },  | 
                    |
| 2823 | 
                        + main_config_str=main_config,  | 
                    |
| 5978 | 2824 | 
                        )  | 
                    
| 5979 | 
                        - old_name = cli_helpers.config_filename(  | 
                    |
| 5980 | 
                        - subsystem="old settings.json"  | 
                    |
| 5981 | 2825 | 
                        )  | 
                    
| 5982 | 
                        - new_name = cli_helpers.config_filename(subsystem="vault")  | 
                    |
| 5983 | 
                        - old_name.unlink(missing_ok=True)  | 
                    |
| 5984 | 
                        - new_name.rename(old_name)  | 
                    |
| 5985 | 
                        - assert cli_helpers.shell_complete_service(  | 
                    |
| 5986 | 
                        - click.Context(cli.derivepassphrase),  | 
                    |
| 5987 | 
                        - click.Argument(["some_parameter"]),  | 
                    |
| 5988 | 
                        - "",  | 
                    |
| 5989 | 
                        - ) == [DUMMY_SERVICE]  | 
                    |
| 5990 | 
                        -  | 
                    |
| 5991 | 
                        -  | 
                    |
| 5992 | 
                        -def completion_item(  | 
                    |
| 5993 | 
                        - item: str | click.shell_completion.CompletionItem,  | 
                    |
| 5994 | 
                        -) -> click.shell_completion.CompletionItem:  | 
                    |
| 5995 | 
                        - """Convert a string to a completion item, if necessary."""  | 
                    |
| 5996 | 
                        - return (  | 
                    |
| 5997 | 
                        - click.shell_completion.CompletionItem(item, type="plain")  | 
                    |
| 5998 | 
                        - if isinstance(item, str)  | 
                    |
| 5999 | 
                        - else item  | 
                    |
| 2826 | 
                        + result = runner.invoke(  | 
                    |
| 2827 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2828 | 
                        + command_line,  | 
                    |
| 2829 | 
                        + catch_exceptions=False,  | 
                    |
| 2830 | 
                        + input=input,  | 
                    |
| 6000 | 2831 | 
                        )  | 
                    
| 6001 | 
                        -  | 
                    |
| 6002 | 
                        -  | 
                    |
| 6003 | 
                        -def assertable_item(  | 
                    |
| 6004 | 
                        - item: str | click.shell_completion.CompletionItem,  | 
                    |
| 6005 | 
                        -) -> tuple[str, Any, str | None]:  | 
                    |
| 6006 | 
                        - """Convert a completion item into a pretty-printable item.  | 
                    |
| 6007 | 
                        -  | 
                    |
| 6008 | 
                        - Intended to make completion items introspectable in pytest's  | 
                    |
| 6009 | 
                        - `assert` output.  | 
                    |
| 6010 | 
                        -  | 
                    |
| 6011 | 
                        - """  | 
                    |
| 6012 | 
                        - item = completion_item(item)  | 
                    |
| 6013 | 
                        - return (item.type, item.value, item.help)  | 
                    |
| 6014 | 
                        -  | 
                    |
| 6015 | 
                        -  | 
                    |
| 6016 | 
                        -class TestShellCompletion:  | 
                    |
| 6017 | 
                        - """Tests for the shell completion machinery."""  | 
                    |
| 6018 | 
                        -  | 
                    |
| 6019 | 
                        - class Completions:  | 
                    |
| 6020 | 
                        - """A deferred completion call."""  | 
                    |
| 6021 | 
                        -  | 
                    |
| 6022 | 
                        - def __init__(  | 
                    |
| 6023 | 
                        - self,  | 
                    |
| 6024 | 
                        - args: Sequence[str],  | 
                    |
| 6025 | 
                        - incomplete: str,  | 
                    |
| 6026 | 
                        - ) -> None:  | 
                    |
| 6027 | 
                        - """Initialize the object.  | 
                    |
| 6028 | 
                        -  | 
                    |
| 6029 | 
                        - Args:  | 
                    |
| 6030 | 
                        - args:  | 
                    |
| 6031 | 
                        - The sequence of complete command-line arguments.  | 
                    |
| 6032 | 
                        - incomplete:  | 
                    |
| 6033 | 
                        - The final, incomplete, partial argument.  | 
                    |
| 6034 | 
                        -  | 
                    |
| 6035 | 
                        - """  | 
                    |
| 6036 | 
                        - self.args = tuple(args)  | 
                    |
| 6037 | 
                        - self.incomplete = incomplete  | 
                    |
| 6038 | 
                        -  | 
                    |
| 6039 | 
                        - def __call__(self) -> Sequence[click.shell_completion.CompletionItem]:  | 
                    |
| 6040 | 
                        - """Return the completion items."""  | 
                    |
| 6041 | 
                        - args = list(self.args)  | 
                    |
| 6042 | 
                        - completion = click.shell_completion.ShellComplete(  | 
                    |
| 6043 | 
                        - cli=cli.derivepassphrase,  | 
                    |
| 6044 | 
                        -                ctx_args={},
                       | 
                    |
| 6045 | 
                        - prog_name="derivepassphrase",  | 
                    |
| 6046 | 
                        - complete_var="_DERIVEPASSPHRASE_COMPLETE",  | 
                    |
| 6047 | 
                        - )  | 
                    |
| 6048 | 
                        - return completion.get_completions(args, self.incomplete)  | 
                    |
| 6049 | 
                        -  | 
                    |
| 6050 | 
                        - def get_words(self) -> Sequence[str]:  | 
                    |
| 6051 | 
                        - """Return the completion items' values, as a sequence."""  | 
                    |
| 6052 | 
                        - return tuple(c.value for c in self())  | 
                    |
| 6053 | 
                        -  | 
                    |
| 6054 | 
                        - @Parametrize.COMPLETABLE_ITEMS  | 
                    |
| 6055 | 
                        - def test_100_is_completable_item(  | 
                    |
| 6056 | 
                        - self,  | 
                    |
| 6057 | 
                        - partial: str,  | 
                    |
| 6058 | 
                        - is_completable: bool,  | 
                    |
| 6059 | 
                        - ) -> None:  | 
                    |
| 6060 | 
                        - """Our `_is_completable_item` predicate for service names works."""  | 
                    |
| 6061 | 
                        - assert cli_helpers.is_completable_item(partial) == is_completable  | 
                    |
| 6062 | 
                        -  | 
                    |
| 6063 | 
                        - @Parametrize.COMPLETABLE_OPTIONS  | 
                    |
| 6064 | 
                        - def test_200_options(  | 
                    |
| 6065 | 
                        - self,  | 
                    |
| 6066 | 
                        - command_prefix: Sequence[str],  | 
                    |
| 6067 | 
                        - incomplete: str,  | 
                    |
| 6068 | 
                        - completions: AbstractSet[str],  | 
                    |
| 6069 | 
                        - ) -> None:  | 
                    |
| 6070 | 
                        - """Our completion machinery works for all commands' options."""  | 
                    |
| 6071 | 
                        - comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 6072 | 
                        - assert frozenset(comp.get_words()) == completions  | 
                    |
| 6073 | 
                        -  | 
                    |
| 6074 | 
                        - @Parametrize.COMPLETABLE_SUBCOMMANDS  | 
                    |
| 6075 | 
                        - def test_201_subcommands(  | 
                    |
| 6076 | 
                        - self,  | 
                    |
| 6077 | 
                        - command_prefix: Sequence[str],  | 
                    |
| 6078 | 
                        - incomplete: str,  | 
                    |
| 6079 | 
                        - completions: AbstractSet[str],  | 
                    |
| 6080 | 
                        - ) -> None:  | 
                    |
| 6081 | 
                        - """Our completion machinery works for all commands' subcommands."""  | 
                    |
| 6082 | 
                        - comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 6083 | 
                        - assert frozenset(comp.get_words()) == completions  | 
                    |
| 6084 | 
                        -  | 
                    |
| 6085 | 
                        - @Parametrize.COMPLETABLE_PATH_ARGUMENT  | 
                    |
| 6086 | 
                        - @Parametrize.INCOMPLETE  | 
                    |
| 6087 | 
                        - def test_202_paths(  | 
                    |
| 6088 | 
                        - self,  | 
                    |
| 6089 | 
                        - command_prefix: Sequence[str],  | 
                    |
| 6090 | 
                        - incomplete: str,  | 
                    |
| 6091 | 
                        - ) -> None:  | 
                    |
| 6092 | 
                        - """Our completion machinery works for all commands' paths."""  | 
                    |
| 6093 | 
                        -        file = click.shell_completion.CompletionItem("", type="file")
                       | 
                    |
| 6094 | 
                        -        completions = frozenset({(file.type, file.value, file.help)})
                       | 
                    |
| 6095 | 
                        - comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 6096 | 
                        - assert (  | 
                    |
| 6097 | 
                        - frozenset((x.type, x.value, x.help) for x in comp()) == completions  | 
                    |
| 2832 | 
                        + assert result.error_exit(  | 
                    |
| 2833 | 
                        + error="The user configuration file is invalid."  | 
                    |
| 2834 | 
                        + ), "expected error exit and known error message"  | 
                    |
| 2835 | 
                        + assert result.error_exit(error=error_message), (  | 
                    |
| 2836 | 
                        + "expected error exit and known error message"  | 
                    |
| 6098 | 2837 | 
                        )  | 
                    
| 6099 | 2838 | 
                         | 
                    
| 6100 | 
                        - @Parametrize.COMPLETABLE_SERVICE_NAMES  | 
                    |
| 6101 | 
                        - def test_203_service_names(  | 
                    |
| 2839 | 
                        + @Parametrize.UNICODE_NORMALIZATION_COMMAND_LINES  | 
                    |
| 2840 | 
                        + def test_301a_unicode_normalization_form_error_from_stored_config(  | 
                    |
| 6102 | 2841 | 
                        self,  | 
                    
| 6103 | 
                        - config: _types.VaultConfig,  | 
                    |
| 6104 | 
                        - incomplete: str,  | 
                    |
| 6105 | 
                        - completions: AbstractSet[str],  | 
                    |
| 2842 | 
                        + command_line: list[str],  | 
                    |
| 6106 | 2843 | 
                        ) -> None:  | 
                    
| 6107 | 
                        - """Our completion machinery works for vault service names."""  | 
                    |
| 2844 | 
                        + """Using unknown Unicode normalization forms in the config fails."""  | 
                    |
| 6108 | 2845 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 6109 | 2846 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 6110 | 2847 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -6115,28 +2852,36 @@ class TestShellCompletion:  | 
                  
| 6115 | 2852 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 6116 | 2853 | 
                        monkeypatch=monkeypatch,  | 
                    
| 6117 | 2854 | 
                        runner=runner,  | 
                    
| 6118 | 
                        - vault_config=config,  | 
                    |
| 2855 | 
                        +                    vault_config={
                       | 
                    |
| 2856 | 
                        +                        "services": {
                       | 
                    |
| 2857 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()  | 
                    |
| 2858 | 
                        + }  | 
                    |
| 2859 | 
                        + },  | 
                    |
| 2860 | 
                        + main_config_str=(  | 
                    |
| 2861 | 
                        + "[vault]\ndefault-unicode-normalization-form = 'XXX'\n"  | 
                    |
| 2862 | 
                        + ),  | 
                    |
| 2863 | 
                        + )  | 
                    |
| 6119 | 2864 | 
                        )  | 
                    
| 2865 | 
                        + result = runner.invoke(  | 
                    |
| 2866 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2867 | 
                        + command_line,  | 
                    |
| 2868 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 2869 | 
                        + catch_exceptions=False,  | 
                    |
| 6120 | 2870 | 
                        )  | 
                    
| 6121 | 
                        - comp = self.Completions(["vault"], incomplete)  | 
                    |
| 6122 | 
                        - assert frozenset(comp.get_words()) == completions  | 
                    |
| 2871 | 
                        + assert result.error_exit(  | 
                    |
| 2872 | 
                        + error="The user configuration file is invalid."  | 
                    |
| 2873 | 
                        + ), "expected error exit and known error message"  | 
                    |
| 2874 | 
                        + assert result.error_exit(  | 
                    |
| 2875 | 
                        + error=(  | 
                    |
| 2876 | 
                        + "Invalid value 'XXX' for config key "  | 
                    |
| 2877 | 
                        + "vault.default-unicode-normalization-form"  | 
                    |
| 2878 | 
                        + ),  | 
                    |
| 2879 | 
                        + ), "expected error exit and known error message"  | 
                    |
| 6123 | 2880 | 
                         | 
                    
| 6124 | 
                        - @Parametrize.SHELL_FORMATTER  | 
                    |
| 6125 | 
                        - @Parametrize.COMPLETION_FUNCTION_INPUTS  | 
                    |
| 6126 | 
                        - def test_300_shell_completion_formatting(  | 
                    |
| 2881 | 
                        + def test_310_bad_user_config_file(  | 
                    |
| 6127 | 2882 | 
                        self,  | 
                    
| 6128 | 
                        - shell: str,  | 
                    |
| 6129 | 
                        - format_func: Callable[[click.shell_completion.CompletionItem], str],  | 
                    |
| 6130 | 
                        - config: _types.VaultConfig,  | 
                    |
| 6131 | 
                        - comp_func: Callable[  | 
                    |
| 6132 | 
                        - [click.Context, click.Parameter, str],  | 
                    |
| 6133 | 
                        - list[str | click.shell_completion.CompletionItem],  | 
                    |
| 6134 | 
                        - ],  | 
                    |
| 6135 | 
                        - args: list[str],  | 
                    |
| 6136 | 
                        - incomplete: str,  | 
                    |
| 6137 | 
                        - results: list[str | click.shell_completion.CompletionItem],  | 
                    |
| 6138 | 2883 | 
                        ) -> None:  | 
                    
| 6139 | 
                        - """Custom completion functions work for all shells."""  | 
                    |
| 2884 | 
                        + """Loading a user configuration file in an invalid format fails."""  | 
                    |
| 6140 | 2885 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 6141 | 2886 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 6142 | 2887 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -6147,58 +2892,24 @@ class TestShellCompletion:  | 
                  
| 6147 | 2892 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 6148 | 2893 | 
                        monkeypatch=monkeypatch,  | 
                    
| 6149 | 2894 | 
                        runner=runner,  | 
                    
| 6150 | 
                        - vault_config=config,  | 
                    |
| 6151 | 
                        - )  | 
                    |
| 6152 | 
                        - )  | 
                    |
| 6153 | 
                        - expected_items = [assertable_item(item) for item in results]  | 
                    |
| 6154 | 
                        - expected_string = "\n".join(  | 
                    |
| 6155 | 
                        - format_func(completion_item(item)) for item in results  | 
                    |
| 6156 | 
                        - )  | 
                    |
| 6157 | 
                        - manual_raw_items = comp_func(  | 
                    |
| 6158 | 
                        - click.Context(cli.derivepassphrase),  | 
                    |
| 6159 | 
                        - click.Argument(["sample_parameter"]),  | 
                    |
| 6160 | 
                        - incomplete,  | 
                    |
| 6161 | 
                        - )  | 
                    |
| 6162 | 
                        - manual_items = [assertable_item(item) for item in manual_raw_items]  | 
                    |
| 6163 | 
                        - manual_string = "\n".join(  | 
                    |
| 6164 | 
                        - format_func(completion_item(item)) for item in manual_raw_items  | 
                    |
| 2895 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 2896 | 
                        + main_config_str="This file is not valid TOML.\n",  | 
                    |
| 6165 | 2897 | 
                        )  | 
                    
| 6166 | 
                        - assert manual_items == expected_items  | 
                    |
| 6167 | 
                        - assert manual_string == expected_string  | 
                    |
| 6168 | 
                        - comp_class = click.shell_completion.get_completion_class(shell)  | 
                    |
| 6169 | 
                        - assert comp_class is not None  | 
                    |
| 6170 | 
                        - comp = comp_class(  | 
                    |
| 6171 | 
                        - cli.derivepassphrase,  | 
                    |
| 6172 | 
                        -                {},
                       | 
                    |
| 6173 | 
                        - "derivepassphrase",  | 
                    |
| 6174 | 
                        - "_DERIVEPASSPHRASE_COMPLETE",  | 
                    |
| 6175 | 2898 | 
                        )  | 
                    
| 6176 | 
                        - monkeypatch.setattr(  | 
                    |
| 6177 | 
                        - comp,  | 
                    |
| 6178 | 
                        - "get_completion_args",  | 
                    |
| 6179 | 
                        - lambda *_a, **_kw: (args, incomplete),  | 
                    |
| 2899 | 
                        + result = runner.invoke(  | 
                    |
| 2900 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2901 | 
                        + ["--phrase", "--", DUMMY_SERVICE],  | 
                    |
| 2902 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 2903 | 
                        + catch_exceptions=False,  | 
                    |
| 6180 | 2904 | 
                        )  | 
                    
| 6181 | 
                        - actual_raw_items = comp.get_completions(  | 
                    |
| 6182 | 
                        - *comp.get_completion_args()  | 
                    |
| 2905 | 
                        + assert result.error_exit(error="Cannot load user config:"), (  | 
                    |
| 2906 | 
                        + "expected error exit and known error message"  | 
                    |
| 6183 | 2907 | 
                        )  | 
                    
| 6184 | 
                        - actual_items = [assertable_item(item) for item in actual_raw_items]  | 
                    |
| 6185 | 
                        - actual_string = comp.complete()  | 
                    |
| 6186 | 
                        - assert actual_items == expected_items  | 
                    |
| 6187 | 
                        - assert actual_string == expected_string  | 
                    |
| 6188 | 2908 | 
                         | 
                    
| 6189 | 
                        - @Parametrize.CONFIG_SETTING_MODE  | 
                    |
| 6190 | 
                        - @Parametrize.SERVICE_NAME_COMPLETION_INPUTS  | 
                    |
| 6191 | 
                        - def test_400_incompletable_service_names(  | 
                    |
| 2909 | 
                        + def test_311_bad_user_config_is_a_directory(  | 
                    |
| 6192 | 2910 | 
                        self,  | 
                    
| 6193 | 
                        - caplog: pytest.LogCaptureFixture,  | 
                    |
| 6194 | 
                        - mode: Literal["config", "import"],  | 
                    |
| 6195 | 
                        - config: _types.VaultConfig,  | 
                    |
| 6196 | 
                        - key: str,  | 
                    |
| 6197 | 
                        - incomplete: str,  | 
                    |
| 6198 | 
                        - completions: AbstractSet[str],  | 
                    |
| 6199 | 2911 | 
                        ) -> None:  | 
                    
| 6200 | 
                        - """Completion skips incompletable items."""  | 
                    |
| 6201 | 
                        -        vault_config = config if mode == "config" else {"services": {}}
                       | 
                    |
| 2912 | 
                        + """Loading a user configuration file in an invalid format fails."""  | 
                    |
| 6202 | 2913 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 6203 | 2914 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 6204 | 2915 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -6209,37 +2920,30 @@ class TestShellCompletion:  | 
                  
| 6209 | 2920 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 6210 | 2921 | 
                        monkeypatch=monkeypatch,  | 
                    
| 6211 | 2922 | 
                        runner=runner,  | 
                    
| 6212 | 
                        - vault_config=vault_config,  | 
                    |
| 2923 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 2924 | 
                        + main_config_str="",  | 
                    |
| 6213 | 2925 | 
                        )  | 
                    
| 6214 | 2926 | 
                        )  | 
                    
| 6215 | 
                        - if mode == "config":  | 
                    |
| 6216 | 
                        - result = runner.invoke(  | 
                    |
| 6217 | 
                        - cli.derivepassphrase_vault,  | 
                    |
| 6218 | 
                        - ["--config", "--length=10", "--", key],  | 
                    |
| 6219 | 
                        - catch_exceptions=False,  | 
                    |
| 2927 | 
                        + user_config = cli_helpers.config_filename(  | 
                    |
| 2928 | 
                        + subsystem="user configuration"  | 
                    |
| 6220 | 2929 | 
                        )  | 
                    
| 6221 | 
                        - else:  | 
                    |
| 2930 | 
                        + user_config.unlink()  | 
                    |
| 2931 | 
                        + user_config.mkdir(parents=True, exist_ok=True)  | 
                    |
| 6222 | 2932 | 
                        result = runner.invoke(  | 
                    
| 6223 | 2933 | 
                        cli.derivepassphrase_vault,  | 
                    
| 6224 | 
                        - ["--import", "-"],  | 
                    |
| 2934 | 
                        + ["--phrase", "--", DUMMY_SERVICE],  | 
                    |
| 2935 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 6225 | 2936 | 
                        catch_exceptions=False,  | 
                    
| 6226 | 
                        - input=json.dumps(config),  | 
                    |
| 6227 | 2937 | 
                        )  | 
                    
| 6228 | 
                        - assert result.clean_exit(), "expected clean exit"  | 
                    |
| 6229 | 
                        - assert machinery.warning_emitted(  | 
                    |
| 6230 | 
                        - "contains an ASCII control character", caplog.record_tuples  | 
                    |
| 6231 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 6232 | 
                        - assert machinery.warning_emitted(  | 
                    |
| 6233 | 
                        - "not be available for completion", caplog.record_tuples  | 
                    |
| 6234 | 
                        - ), "expected known warning message in stderr"  | 
                    |
| 6235 | 
                        - assert cli_helpers.load_config() == config  | 
                    |
| 6236 | 
                        - comp = self.Completions(["vault"], incomplete)  | 
                    |
| 6237 | 
                        - assert frozenset(comp.get_words()) == completions  | 
                    |
| 2938 | 
                        + assert result.error_exit(error="Cannot load user config:"), (  | 
                    |
| 2939 | 
                        + "expected error exit and known error message"  | 
                    |
| 2940 | 
                        + )  | 
                    |
| 6238 | 2941 | 
                         | 
                    
| 6239 | 
                        - def test_410a_service_name_exceptions_not_found(  | 
                    |
| 2942 | 
                        + def test_400_missing_af_unix_support(  | 
                    |
| 6240 | 2943 | 
                        self,  | 
                    
| 2944 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 6241 | 2945 | 
                        ) -> None:  | 
                    
| 6242 | 
                        - """Service name completion quietly fails on missing configuration."""  | 
                    |
| 2946 | 
                        + """Querying the SSH agent without `AF_UNIX` support fails."""  | 
                    |
| 6243 | 2947 | 
                        runner = machinery.CliRunner(mix_stderr=False)  | 
                    
| 6244 | 2948 | 
                        # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    
| 6245 | 2949 | 
                        # with-statements.  | 
                    
| ... | ... | 
                      @@ -6250,48 +2954,25 @@ class TestShellCompletion:  | 
                  
| 6250 | 2954 | 
                        pytest_machinery.isolated_vault_config(  | 
                    
| 6251 | 2955 | 
                        monkeypatch=monkeypatch,  | 
                    
| 6252 | 2956 | 
                        runner=runner,  | 
                    
| 6253 | 
                        -                    vault_config={
                       | 
                    |
| 6254 | 
                        -                        "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 6255 | 
                        - },  | 
                    |
| 6256 | 
                        - )  | 
                    |
| 6257 | 
                        - )  | 
                    |
| 6258 | 
                        - cli_helpers.config_filename(subsystem="vault").unlink(  | 
                    |
| 6259 | 
                        - missing_ok=True  | 
                    |
| 2957 | 
                        +                    vault_config={"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 6260 | 2958 | 
                        )  | 
                    
| 6261 | 
                        - assert not cli_helpers.shell_complete_service(  | 
                    |
| 6262 | 
                        - click.Context(cli.derivepassphrase),  | 
                    |
| 6263 | 
                        - click.Argument(["some_parameter"]),  | 
                    |
| 6264 | 
                        - "",  | 
                    |
| 6265 | 2959 | 
                        )  | 
                    
| 6266 | 
                        -  | 
                    |
| 6267 | 
                        - @Parametrize.SERVICE_NAME_EXCEPTIONS  | 
                    |
| 6268 | 
                        - def test_410b_service_name_exceptions_custom_error(  | 
                    |
| 6269 | 
                        - self,  | 
                    |
| 6270 | 
                        - exc_type: type[Exception],  | 
                    |
| 6271 | 
                        - ) -> None:  | 
                    |
| 6272 | 
                        - """Service name completion quietly fails on configuration errors."""  | 
                    |
| 6273 | 
                        - runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 6274 | 
                        - # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 6275 | 
                        - # with-statements.  | 
                    |
| 6276 | 
                        - # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 6277 | 
                        - with contextlib.ExitStack() as stack:  | 
                    |
| 6278 | 
                        - monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 6279 | 
                        - stack.enter_context(  | 
                    |
| 6280 | 
                        - pytest_machinery.isolated_vault_config(  | 
                    |
| 6281 | 
                        - monkeypatch=monkeypatch,  | 
                    |
| 6282 | 
                        - runner=runner,  | 
                    |
| 6283 | 
                        -                    vault_config={
                       | 
                    |
| 6284 | 
                        -                        "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 6285 | 
                        - },  | 
                    |
| 2960 | 
                        + monkeypatch.setenv(  | 
                    |
| 2961 | 
                        + "SSH_AUTH_SOCK", "the value doesn't even matter"  | 
                    |
| 6286 | 2962 | 
                        )  | 
                    
| 2963 | 
                        + monkeypatch.setattr(  | 
                    |
| 2964 | 
                        + ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", ["posix"]  | 
                    |
| 6287 | 2965 | 
                        )  | 
                    
| 6288 | 
                        -  | 
                    |
| 6289 | 
                        - def raiser(*_a: Any, **_kw: Any) -> NoReturn:  | 
                    |
| 6290 | 
                        -                raise exc_type("just being difficult")  # noqa: EM101,TRY003
                       | 
                    |
| 6291 | 
                        -  | 
                    |
| 6292 | 
                        - monkeypatch.setattr(cli_helpers, "load_config", raiser)  | 
                    |
| 6293 | 
                        - assert not cli_helpers.shell_complete_service(  | 
                    |
| 6294 | 
                        - click.Context(cli.derivepassphrase),  | 
                    |
| 6295 | 
                        - click.Argument(["some_parameter"]),  | 
                    |
| 6296 | 
                        - "",  | 
                    |
| 2966 | 
                        + monkeypatch.delattr(socket, "AF_UNIX", raising=False)  | 
                    |
| 2967 | 
                        + result = runner.invoke(  | 
                    |
| 2968 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 2969 | 
                        + ["--key", "--config"],  | 
                    |
| 2970 | 
                        + catch_exceptions=False,  | 
                    |
| 6297 | 2971 | 
                        )  | 
                    
| 2972 | 
                        + assert result.error_exit(  | 
                    |
| 2973 | 
                        + error="does not support communicating with it"  | 
                    |
| 2974 | 
                        + ), "expected error exit and known error message"  | 
                    |
| 2975 | 
                        + assert machinery.warning_emitted(  | 
                    |
| 2976 | 
                        + "Cannot connect to an SSH agent via UNIX domain sockets",  | 
                    |
| 2977 | 
                        + caplog.record_tuples,  | 
                    |
| 2978 | 
                        + ), "expected known warning message in stderr"  | 
                    
| ... | ... | 
                      @@ -0,0 +1,771 @@  | 
                  
| 1 | 
                        +# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>  | 
                    |
| 2 | 
                        +#  | 
                    |
| 3 | 
                        +# SPDX-License-Identifier: Zlib  | 
                    |
| 4 | 
                        +  | 
                    |
| 5 | 
                        +from __future__ import annotations  | 
                    |
| 6 | 
                        +  | 
                    |
| 7 | 
                        +import contextlib  | 
                    |
| 8 | 
                        +import enum  | 
                    |
| 9 | 
                        +import re  | 
                    |
| 10 | 
                        +import types  | 
                    |
| 11 | 
                        +  | 
                    |
| 12 | 
                        +import exceptiongroup  | 
                    |
| 13 | 
                        +import pytest  | 
                    |
| 14 | 
                        +from typing_extensions import NamedTuple  | 
                    |
| 15 | 
                        +  | 
                    |
| 16 | 
                        +from derivepassphrase import _types, cli, ssh_agent  | 
                    |
| 17 | 
                        +from derivepassphrase._internals import cli_messages  | 
                    |
| 18 | 
                        +from tests import machinery  | 
                    |
| 19 | 
                        +from tests.machinery import pytest as pytest_machinery  | 
                    |
| 20 | 
                        +  | 
                    |
| 21 | 
                        +  | 
                    |
| 22 | 
                        +class VersionOutputData(NamedTuple):  | 
                    |
| 23 | 
                        + derivation_schemes: dict[str, bool]  | 
                    |
| 24 | 
                        + foreign_configuration_formats: dict[str, bool]  | 
                    |
| 25 | 
                        + extras: frozenset[str]  | 
                    |
| 26 | 
                        + subcommands: frozenset[str]  | 
                    |
| 27 | 
                        + features: dict[str, bool]  | 
                    |
| 28 | 
                        +  | 
                    |
| 29 | 
                        +  | 
                    |
| 30 | 
                        +class KnownLineType(str, enum.Enum):  | 
                    |
| 31 | 
                        + SUPPORTED_FOREIGN_CONFS = cli_messages.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS.value.singular.rstrip(  | 
                    |
| 32 | 
                        + ":"  | 
                    |
| 33 | 
                        + )  | 
                    |
| 34 | 
                        + UNAVAILABLE_FOREIGN_CONFS = cli_messages.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS.value.singular.rstrip(  | 
                    |
| 35 | 
                        + ":"  | 
                    |
| 36 | 
                        + )  | 
                    |
| 37 | 
                        + SUPPORTED_SCHEMES = (  | 
                    |
| 38 | 
                        + cli_messages.Label.SUPPORTED_DERIVATION_SCHEMES.value.singular.rstrip(  | 
                    |
| 39 | 
                        + ":"  | 
                    |
| 40 | 
                        + )  | 
                    |
| 41 | 
                        + )  | 
                    |
| 42 | 
                        + UNAVAILABLE_SCHEMES = cli_messages.Label.UNAVAILABLE_DERIVATION_SCHEMES.value.singular.rstrip(  | 
                    |
| 43 | 
                        + ":"  | 
                    |
| 44 | 
                        + )  | 
                    |
| 45 | 
                        + SUPPORTED_SUBCOMMANDS = (  | 
                    |
| 46 | 
                        +        cli_messages.Label.SUPPORTED_SUBCOMMANDS.value.singular.rstrip(":")
                       | 
                    |
| 47 | 
                        + )  | 
                    |
| 48 | 
                        + SUPPORTED_FEATURES = (  | 
                    |
| 49 | 
                        +        cli_messages.Label.SUPPORTED_FEATURES.value.singular.rstrip(":")
                       | 
                    |
| 50 | 
                        + )  | 
                    |
| 51 | 
                        + UNAVAILABLE_FEATURES = (  | 
                    |
| 52 | 
                        +        cli_messages.Label.UNAVAILABLE_FEATURES.value.singular.rstrip(":")
                       | 
                    |
| 53 | 
                        + )  | 
                    |
| 54 | 
                        + ENABLED_EXTRAS = (  | 
                    |
| 55 | 
                        +        cli_messages.Label.ENABLED_PEP508_EXTRAS.value.singular.rstrip(":")
                       | 
                    |
| 56 | 
                        + )  | 
                    |
| 57 | 
                        +  | 
                    |
| 58 | 
                        +  | 
                    |
| 59 | 
                        +class Parametrize(types.SimpleNamespace):  | 
                    |
| 60 | 
                        + """Common test parametrizations."""  | 
                    |
| 61 | 
                        +  | 
                    |
| 62 | 
                        + EAGER_ARGUMENTS = pytest.mark.parametrize(  | 
                    |
| 63 | 
                        + "arguments",  | 
                    |
| 64 | 
                        + [["--help"], ["--version"]],  | 
                    |
| 65 | 
                        + ids=["help", "version"],  | 
                    |
| 66 | 
                        + )  | 
                    |
| 67 | 
                        + COMMAND_NON_EAGER_ARGUMENTS = pytest.mark.parametrize(  | 
                    |
| 68 | 
                        + ["command", "non_eager_arguments"],  | 
                    |
| 69 | 
                        + [  | 
                    |
| 70 | 
                        + pytest.param(  | 
                    |
| 71 | 
                        + [],  | 
                    |
| 72 | 
                        + [],  | 
                    |
| 73 | 
                        + id="top-nothing",  | 
                    |
| 74 | 
                        + ),  | 
                    |
| 75 | 
                        + pytest.param(  | 
                    |
| 76 | 
                        + [],  | 
                    |
| 77 | 
                        + ["export"],  | 
                    |
| 78 | 
                        + id="top-export",  | 
                    |
| 79 | 
                        + ),  | 
                    |
| 80 | 
                        + pytest.param(  | 
                    |
| 81 | 
                        + ["export"],  | 
                    |
| 82 | 
                        + [],  | 
                    |
| 83 | 
                        + id="export-nothing",  | 
                    |
| 84 | 
                        + ),  | 
                    |
| 85 | 
                        + pytest.param(  | 
                    |
| 86 | 
                        + ["export"],  | 
                    |
| 87 | 
                        + ["vault"],  | 
                    |
| 88 | 
                        + id="export-vault",  | 
                    |
| 89 | 
                        + ),  | 
                    |
| 90 | 
                        + pytest.param(  | 
                    |
| 91 | 
                        + ["export", "vault"],  | 
                    |
| 92 | 
                        + [],  | 
                    |
| 93 | 
                        + id="export-vault-nothing",  | 
                    |
| 94 | 
                        + ),  | 
                    |
| 95 | 
                        + pytest.param(  | 
                    |
| 96 | 
                        + ["export", "vault"],  | 
                    |
| 97 | 
                        + ["--format", "this-format-doesnt-exist"],  | 
                    |
| 98 | 
                        + id="export-vault-args",  | 
                    |
| 99 | 
                        + ),  | 
                    |
| 100 | 
                        + pytest.param(  | 
                    |
| 101 | 
                        + ["vault"],  | 
                    |
| 102 | 
                        + [],  | 
                    |
| 103 | 
                        + id="vault-nothing",  | 
                    |
| 104 | 
                        + ),  | 
                    |
| 105 | 
                        + pytest.param(  | 
                    |
| 106 | 
                        + ["vault"],  | 
                    |
| 107 | 
                        + ["--export", "./"],  | 
                    |
| 108 | 
                        + id="vault-args",  | 
                    |
| 109 | 
                        + ),  | 
                    |
| 110 | 
                        + ],  | 
                    |
| 111 | 
                        + )  | 
                    |
| 112 | 
                        + COLORFUL_COMMAND_INPUT = pytest.mark.parametrize(  | 
                    |
| 113 | 
                        + ["command_line", "input"],  | 
                    |
| 114 | 
                        + [  | 
                    |
| 115 | 
                        + (  | 
                    |
| 116 | 
                        + ["vault", "--import", "-"],  | 
                    |
| 117 | 
                        +                '{"services": {"": {"length": 20}}}',
                       | 
                    |
| 118 | 
                        + ),  | 
                    |
| 119 | 
                        + ],  | 
                    |
| 120 | 
                        + ids=["cmd"],  | 
                    |
| 121 | 
                        + )  | 
                    |
| 122 | 
                        + ISATTY = pytest.mark.parametrize(  | 
                    |
| 123 | 
                        + "isatty",  | 
                    |
| 124 | 
                        + [False, True],  | 
                    |
| 125 | 
                        + ids=["notty", "tty"],  | 
                    |
| 126 | 
                        + )  | 
                    |
| 127 | 
                        +    MASK_PROG_NAME = pytest.mark.parametrize("mask_prog_name", [False, True])
                       | 
                    |
| 128 | 
                        +    MASK_VERSION = pytest.mark.parametrize("mask_version", [False, True])
                       | 
                    |
| 129 | 
                        + VERSION_OUTPUT_DATA = pytest.mark.parametrize(  | 
                    |
| 130 | 
                        + ["version_output", "prog_name", "version", "expected_parse"],  | 
                    |
| 131 | 
                        + [  | 
                    |
| 132 | 
                        + pytest.param(  | 
                    |
| 133 | 
                        + """\  | 
                    |
| 134 | 
                        +derivepassphrase 0.4.0  | 
                    |
| 135 | 
                        +Using cryptography 44.0.0  | 
                    |
| 136 | 
                        +  | 
                    |
| 137 | 
                        +Supported foreign configuration formats: vault storeroom, vault v0.2,  | 
                    |
| 138 | 
                        + vault v0.3.  | 
                    |
| 139 | 
                        +PEP 508 extras: export.  | 
                    |
| 140 | 
                        +""",  | 
                    |
| 141 | 
                        + "derivepassphrase",  | 
                    |
| 142 | 
                        + "0.4.0",  | 
                    |
| 143 | 
                        + VersionOutputData(  | 
                    |
| 144 | 
                        +                    derivation_schemes={},
                       | 
                    |
| 145 | 
                        +                    foreign_configuration_formats={
                       | 
                    |
| 146 | 
                        + "vault storeroom": True,  | 
                    |
| 147 | 
                        + "vault v0.2": True,  | 
                    |
| 148 | 
                        + "vault v0.3": True,  | 
                    |
| 149 | 
                        + },  | 
                    |
| 150 | 
                        + subcommands=frozenset(),  | 
                    |
| 151 | 
                        +                    features={},
                       | 
                    |
| 152 | 
                        +                    extras=frozenset({"export"}),
                       | 
                    |
| 153 | 
                        + ),  | 
                    |
| 154 | 
                        + id="derivepassphrase-0.4.0-export",  | 
                    |
| 155 | 
                        + ),  | 
                    |
| 156 | 
                        + pytest.param(  | 
                    |
| 157 | 
                        + """\  | 
                    |
| 158 | 
                        +derivepassphrase 0.5  | 
                    |
| 159 | 
                        +  | 
                    |
| 160 | 
                        +Supported derivation schemes: vault.  | 
                    |
| 161 | 
                        +Known foreign configuration formats: vault storeroom, vault v0.2, vault v0.3.  | 
                    |
| 162 | 
                        +Supported subcommands: export, vault.  | 
                    |
| 163 | 
                        +No PEP 508 extras are active.  | 
                    |
| 164 | 
                        +""",  | 
                    |
| 165 | 
                        + "derivepassphrase",  | 
                    |
| 166 | 
                        + "0.5",  | 
                    |
| 167 | 
                        + VersionOutputData(  | 
                    |
| 168 | 
                        +                    derivation_schemes={"vault": True},
                       | 
                    |
| 169 | 
                        +                    foreign_configuration_formats={
                       | 
                    |
| 170 | 
                        + "vault storeroom": False,  | 
                    |
| 171 | 
                        + "vault v0.2": False,  | 
                    |
| 172 | 
                        + "vault v0.3": False,  | 
                    |
| 173 | 
                        + },  | 
                    |
| 174 | 
                        +                    subcommands=frozenset({"export", "vault"}),
                       | 
                    |
| 175 | 
                        +                    features={},
                       | 
                    |
| 176 | 
                        +                    extras=frozenset({}),
                       | 
                    |
| 177 | 
                        + ),  | 
                    |
| 178 | 
                        + id="derivepassphrase-0.5-plain",  | 
                    |
| 179 | 
                        + ),  | 
                    |
| 180 | 
                        + pytest.param(  | 
                    |
| 181 | 
                        + """\  | 
                    |
| 182 | 
                        +  | 
                    |
| 183 | 
                        +  | 
                    |
| 184 | 
                        +  | 
                    |
| 185 | 
                        +inventpassphrase -1.3  | 
                    |
| 186 | 
                        +Using not-a-library 7.12  | 
                    |
| 187 | 
                        +Copyright 2025 Nobody. All rights reserved.  | 
                    |
| 188 | 
                        +  | 
                    |
| 189 | 
                        +Supported derivation schemes: nonsense.  | 
                    |
| 190 | 
                        +Known derivation schemes: divination, /dev/random,  | 
                    |
| 191 | 
                        + geiger counter,  | 
                    |
| 192 | 
                        + crossword solver.  | 
                    |
| 193 | 
                        +Supported foreign configuration formats: derivepassphrase, nonsense.  | 
                    |
| 194 | 
                        +Known foreign configuration formats: divination v3.141592,  | 
                    |
| 195 | 
                        + /dev/random.  | 
                    |
| 196 | 
                        +Supported subcommands: delete-all-files, dump-core.  | 
                    |
| 197 | 
                        +Supported features: delete-while-open.  | 
                    |
| 198 | 
                        +Known features: backups-are-nice-to-have.  | 
                    |
| 199 | 
                        +PEP 508 extras: annoying-popups, delete-all-files,  | 
                    |
| 200 | 
                        + dump-core-depending-on-the-phase-of-the-moon.  | 
                    |
| 201 | 
                        +  | 
                    |
| 202 | 
                        +  | 
                    |
| 203 | 
                        +  | 
                    |
| 204 | 
                        +""",  | 
                    |
| 205 | 
                        + "inventpassphrase",  | 
                    |
| 206 | 
                        + "-1.3",  | 
                    |
| 207 | 
                        + VersionOutputData(  | 
                    |
| 208 | 
                        +                    derivation_schemes={
                       | 
                    |
| 209 | 
                        + "nonsense": True,  | 
                    |
| 210 | 
                        + "divination": False,  | 
                    |
| 211 | 
                        + "/dev/random": False,  | 
                    |
| 212 | 
                        + "geiger counter": False,  | 
                    |
| 213 | 
                        + "crossword solver": False,  | 
                    |
| 214 | 
                        + },  | 
                    |
| 215 | 
                        +                    foreign_configuration_formats={
                       | 
                    |
| 216 | 
                        + "derivepassphrase": True,  | 
                    |
| 217 | 
                        + "nonsense": True,  | 
                    |
| 218 | 
                        + "divination v3.141592": False,  | 
                    |
| 219 | 
                        + "/dev/random": False,  | 
                    |
| 220 | 
                        + },  | 
                    |
| 221 | 
                        +                    subcommands=frozenset({"delete-all-files", "dump-core"}),
                       | 
                    |
| 222 | 
                        +                    features={
                       | 
                    |
| 223 | 
                        + "delete-while-open": True,  | 
                    |
| 224 | 
                        + "backups-are-nice-to-have": False,  | 
                    |
| 225 | 
                        + },  | 
                    |
| 226 | 
                        +                    extras=frozenset({
                       | 
                    |
| 227 | 
                        + "annoying-popups",  | 
                    |
| 228 | 
                        + "delete-all-files",  | 
                    |
| 229 | 
                        + "dump-core-depending-on-the-phase-of-the-moon",  | 
                    |
| 230 | 
                        + }),  | 
                    |
| 231 | 
                        + ),  | 
                    |
| 232 | 
                        + id="inventpassphrase",  | 
                    |
| 233 | 
                        + ),  | 
                    |
| 234 | 
                        + ],  | 
                    |
| 235 | 
                        + )  | 
                    |
| 236 | 
                        + """Sample data for [`parse_version_output`][]."""  | 
                    |
| 237 | 
                        +  | 
                    |
| 238 | 
                        +  | 
                    |
| 239 | 
                        +def parse_version_output( # noqa: C901  | 
                    |
| 240 | 
                        + version_output: str,  | 
                    |
| 241 | 
                        + /,  | 
                    |
| 242 | 
                        + *,  | 
                    |
| 243 | 
                        + prog_name: str | None = cli_messages.PROG_NAME,  | 
                    |
| 244 | 
                        + version: str | None = cli_messages.VERSION,  | 
                    |
| 245 | 
                        +) -> VersionOutputData:  | 
                    |
| 246 | 
                        + r"""Parse the output of the `--version` option.  | 
                    |
| 247 | 
                        +  | 
                    |
| 248 | 
                        + The version output contains two paragraphs. The first paragraph  | 
                    |
| 249 | 
                        + details the version number, and the version number of any major  | 
                    |
| 250 | 
                        + libraries in use. The second paragraph details known and supported  | 
                    |
| 251 | 
                        + passphrase derivation schemes, foreign configuration formats,  | 
                    |
| 252 | 
                        + subcommands and PEP 508 package extras. For the schemes and  | 
                    |
| 253 | 
                        + formats, there is a "supported" line for supported items, and  | 
                    |
| 254 | 
                        + a "known" line for known but currently unsupported items (usually  | 
                    |
| 255 | 
                        + because of missing dependencies), either of which may be empty and  | 
                    |
| 256 | 
                        + thus omitted. For extras, only active items are shown, and there is  | 
                    |
| 257 | 
                        + a separate message for the "no extras active" case. Item lists may  | 
                    |
| 258 | 
                        + be spilled across multiple lines, but only at item boundaries, and  | 
                    |
| 259 | 
                        + the continuation lines are then indented.  | 
                    |
| 260 | 
                        +  | 
                    |
| 261 | 
                        + Args:  | 
                    |
| 262 | 
                        + version_output:  | 
                    |
| 263 | 
                        + The version output text to parse.  | 
                    |
| 264 | 
                        + prog_name:  | 
                    |
| 265 | 
                        + The program name to assert, defaulting to the true program  | 
                    |
| 266 | 
                        + name, `derivepassphrase`. Set to `None` to disable this  | 
                    |
| 267 | 
                        + check.  | 
                    |
| 268 | 
                        + version:  | 
                    |
| 269 | 
                        + The program version to assert, defaulting to the true  | 
                    |
| 270 | 
                        + current version of `derivepassphrase`. Set to `None` to  | 
                    |
| 271 | 
                        + disable this check.  | 
                    |
| 272 | 
                        +  | 
                    |
| 273 | 
                        + Examples:  | 
                    |
| 274 | 
                        + See [`Parametrize.VERSION_OUTPUT_DATA`][].  | 
                    |
| 275 | 
                        +  | 
                    |
| 276 | 
                        + """  | 
                    |
| 277 | 
                        + paragraphs: list[list[str]] = []  | 
                    |
| 278 | 
                        + paragraph: list[str] = []  | 
                    |
| 279 | 
                        + for line in version_output.splitlines(keepends=False):  | 
                    |
| 280 | 
                        + if not line.strip():  | 
                    |
| 281 | 
                        + if paragraph:  | 
                    |
| 282 | 
                        + paragraphs.append(paragraph.copy())  | 
                    |
| 283 | 
                        + paragraph.clear()  | 
                    |
| 284 | 
                        + elif paragraph and line.lstrip() != line:  | 
                    |
| 285 | 
                        +            paragraph[-1] = f"{paragraph[-1]} {line.lstrip()}"
                       | 
                    |
| 286 | 
                        + else:  | 
                    |
| 287 | 
                        + paragraph.append(line)  | 
                    |
| 288 | 
                        + if paragraph: # pragma: no branch  | 
                    |
| 289 | 
                        + paragraphs.append(paragraph.copy())  | 
                    |
| 290 | 
                        + paragraph.clear()  | 
                    |
| 291 | 
                        + assert paragraphs, (  | 
                    |
| 292 | 
                        +        f"expected at least one paragraph of version output: {paragraphs!r}"
                       | 
                    |
| 293 | 
                        + )  | 
                    |
| 294 | 
                        + assert prog_name is None or prog_name in paragraphs[0][0], (  | 
                    |
| 295 | 
                        + f"first version output line should mention "  | 
                    |
| 296 | 
                        +        f"{prog_name}: {paragraphs[0][0]!r}"
                       | 
                    |
| 297 | 
                        + )  | 
                    |
| 298 | 
                        + assert version is None or version in paragraphs[0][0], (  | 
                    |
| 299 | 
                        + f"first version output line should mention the version number "  | 
                    |
| 300 | 
                        +        f"{version}: {paragraphs[0][0]!r}"
                       | 
                    |
| 301 | 
                        + )  | 
                    |
| 302 | 
                        +    schemes: dict[str, bool] = {}
                       | 
                    |
| 303 | 
                        +    formats: dict[str, bool] = {}
                       | 
                    |
| 304 | 
                        + subcommands: set[str] = set()  | 
                    |
| 305 | 
                        + extras: set[str] = set()  | 
                    |
| 306 | 
                        +    features: dict[str, bool] = {}
                       | 
                    |
| 307 | 
                        + if len(paragraphs) < 2: # pragma: no cover  | 
                    |
| 308 | 
                        + return VersionOutputData(  | 
                    |
| 309 | 
                        + derivation_schemes=schemes,  | 
                    |
| 310 | 
                        + foreign_configuration_formats=formats,  | 
                    |
| 311 | 
                        + subcommands=frozenset(subcommands),  | 
                    |
| 312 | 
                        + extras=frozenset(extras),  | 
                    |
| 313 | 
                        + features=features,  | 
                    |
| 314 | 
                        + )  | 
                    |
| 315 | 
                        + for line in paragraphs[1]:  | 
                    |
| 316 | 
                        +        line_type, _, value = line.partition(":")
                       | 
                    |
| 317 | 
                        + if line_type == line:  | 
                    |
| 318 | 
                        + continue  | 
                    |
| 319 | 
                        + for item_ in re.split(r"(?:, *|.$)", value):  | 
                    |
| 320 | 
                        + item = item_.strip()  | 
                    |
| 321 | 
                        + if not item:  | 
                    |
| 322 | 
                        + continue  | 
                    |
| 323 | 
                        + if line_type == KnownLineType.SUPPORTED_FOREIGN_CONFS:  | 
                    |
| 324 | 
                        + formats[item] = True  | 
                    |
| 325 | 
                        + elif line_type == KnownLineType.UNAVAILABLE_FOREIGN_CONFS:  | 
                    |
| 326 | 
                        + formats[item] = False  | 
                    |
| 327 | 
                        + elif line_type == KnownLineType.SUPPORTED_SCHEMES:  | 
                    |
| 328 | 
                        + schemes[item] = True  | 
                    |
| 329 | 
                        + elif line_type == KnownLineType.UNAVAILABLE_SCHEMES:  | 
                    |
| 330 | 
                        + schemes[item] = False  | 
                    |
| 331 | 
                        + elif line_type == KnownLineType.SUPPORTED_SUBCOMMANDS:  | 
                    |
| 332 | 
                        + subcommands.add(item)  | 
                    |
| 333 | 
                        + elif line_type == KnownLineType.ENABLED_EXTRAS:  | 
                    |
| 334 | 
                        + extras.add(item)  | 
                    |
| 335 | 
                        + elif line_type == KnownLineType.SUPPORTED_FEATURES:  | 
                    |
| 336 | 
                        + features[item] = True  | 
                    |
| 337 | 
                        + elif line_type == KnownLineType.UNAVAILABLE_FEATURES:  | 
                    |
| 338 | 
                        + features[item] = False  | 
                    |
| 339 | 
                        + else:  | 
                    |
| 340 | 
                        + raise AssertionError( # noqa: TRY003  | 
                    |
| 341 | 
                        +                    f"Unknown version info line type: {line_type!r}"  # noqa: EM102
                       | 
                    |
| 342 | 
                        + )  | 
                    |
| 343 | 
                        + return VersionOutputData(  | 
                    |
| 344 | 
                        + derivation_schemes=schemes,  | 
                    |
| 345 | 
                        + foreign_configuration_formats=formats,  | 
                    |
| 346 | 
                        + subcommands=frozenset(subcommands),  | 
                    |
| 347 | 
                        + extras=frozenset(extras),  | 
                    |
| 348 | 
                        + features=features,  | 
                    |
| 349 | 
                        + )  | 
                    |
| 350 | 
                        +  | 
                    |
| 351 | 
                        +  | 
                    |
| 352 | 
                        +class TestAllCLI:  | 
                    |
| 353 | 
                        + """Tests uniformly for all command-line interfaces."""  | 
                    |
| 354 | 
                        +  | 
                    |
| 355 | 
                        + @Parametrize.MASK_PROG_NAME  | 
                    |
| 356 | 
                        + @Parametrize.MASK_VERSION  | 
                    |
| 357 | 
                        + @Parametrize.VERSION_OUTPUT_DATA  | 
                    |
| 358 | 
                        + def test_001_parse_version_output(  | 
                    |
| 359 | 
                        + self,  | 
                    |
| 360 | 
                        + version_output: str,  | 
                    |
| 361 | 
                        + prog_name: str | None,  | 
                    |
| 362 | 
                        + version: str | None,  | 
                    |
| 363 | 
                        + mask_prog_name: bool,  | 
                    |
| 364 | 
                        + mask_version: bool,  | 
                    |
| 365 | 
                        + expected_parse: VersionOutputData,  | 
                    |
| 366 | 
                        + ) -> None:  | 
                    |
| 367 | 
                        + """The parsing machinery for expected version output data works."""  | 
                    |
| 368 | 
                        + prog_name = None if mask_prog_name else prog_name  | 
                    |
| 369 | 
                        + version = None if mask_version else version  | 
                    |
| 370 | 
                        + assert (  | 
                    |
| 371 | 
                        + parse_version_output(  | 
                    |
| 372 | 
                        + version_output, prog_name=prog_name, version=version  | 
                    |
| 373 | 
                        + )  | 
                    |
| 374 | 
                        + == expected_parse  | 
                    |
| 375 | 
                        + )  | 
                    |
| 376 | 
                        +  | 
                    |
| 377 | 
                        + # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 378 | 
                        + # check for?  | 
                    |
| 379 | 
                        + def test_100_help_output(self) -> None:  | 
                    |
| 380 | 
                        + """The top-level help text mentions subcommands.  | 
                    |
| 381 | 
                        +  | 
                    |
| 382 | 
                        + TODO: Do we actually need this? What should we check for?  | 
                    |
| 383 | 
                        +  | 
                    |
| 384 | 
                        + """  | 
                    |
| 385 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 386 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 387 | 
                        + # with-statements.  | 
                    |
| 388 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 389 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 390 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 391 | 
                        + stack.enter_context(  | 
                    |
| 392 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 393 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 394 | 
                        + runner=runner,  | 
                    |
| 395 | 
                        + )  | 
                    |
| 396 | 
                        + )  | 
                    |
| 397 | 
                        + result = runner.invoke(  | 
                    |
| 398 | 
                        + cli.derivepassphrase, ["--help"], catch_exceptions=False  | 
                    |
| 399 | 
                        + )  | 
                    |
| 400 | 
                        + assert result.clean_exit(  | 
                    |
| 401 | 
                        + empty_stderr=True, output="currently implemented subcommands"  | 
                    |
| 402 | 
                        + ), "expected clean exit, and known help text"  | 
                    |
| 403 | 
                        +  | 
                    |
| 404 | 
                        + # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 405 | 
                        + # check for?  | 
                    |
| 406 | 
                        + def test_101_help_output_export(  | 
                    |
| 407 | 
                        + self,  | 
                    |
| 408 | 
                        + ) -> None:  | 
                    |
| 409 | 
                        + """The "export" subcommand help text mentions subcommands.  | 
                    |
| 410 | 
                        +  | 
                    |
| 411 | 
                        + TODO: Do we actually need this? What should we check for?  | 
                    |
| 412 | 
                        +  | 
                    |
| 413 | 
                        + """  | 
                    |
| 414 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 415 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 416 | 
                        + # with-statements.  | 
                    |
| 417 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 418 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 419 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 420 | 
                        + stack.enter_context(  | 
                    |
| 421 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 422 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 423 | 
                        + runner=runner,  | 
                    |
| 424 | 
                        + )  | 
                    |
| 425 | 
                        + )  | 
                    |
| 426 | 
                        + result = runner.invoke(  | 
                    |
| 427 | 
                        + cli.derivepassphrase,  | 
                    |
| 428 | 
                        + ["export", "--help"],  | 
                    |
| 429 | 
                        + catch_exceptions=False,  | 
                    |
| 430 | 
                        + )  | 
                    |
| 431 | 
                        + assert result.clean_exit(  | 
                    |
| 432 | 
                        + empty_stderr=True, output="only available subcommand"  | 
                    |
| 433 | 
                        + ), "expected clean exit, and known help text"  | 
                    |
| 434 | 
                        +  | 
                    |
| 435 | 
                        + # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 436 | 
                        + # check for?  | 
                    |
| 437 | 
                        + def test_102_help_output_export_vault(  | 
                    |
| 438 | 
                        + self,  | 
                    |
| 439 | 
                        + ) -> None:  | 
                    |
| 440 | 
                        + """The "export vault" subcommand help text has known content.  | 
                    |
| 441 | 
                        +  | 
                    |
| 442 | 
                        + TODO: Do we actually need this? What should we check for?  | 
                    |
| 443 | 
                        +  | 
                    |
| 444 | 
                        + """  | 
                    |
| 445 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 446 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 447 | 
                        + # with-statements.  | 
                    |
| 448 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 449 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 450 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 451 | 
                        + stack.enter_context(  | 
                    |
| 452 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 453 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 454 | 
                        + runner=runner,  | 
                    |
| 455 | 
                        + )  | 
                    |
| 456 | 
                        + )  | 
                    |
| 457 | 
                        + result = runner.invoke(  | 
                    |
| 458 | 
                        + cli.derivepassphrase,  | 
                    |
| 459 | 
                        + ["export", "vault", "--help"],  | 
                    |
| 460 | 
                        + catch_exceptions=False,  | 
                    |
| 461 | 
                        + )  | 
                    |
| 462 | 
                        + assert result.clean_exit(  | 
                    |
| 463 | 
                        + empty_stderr=True, output="Export a vault-native configuration"  | 
                    |
| 464 | 
                        + ), "expected clean exit, and known help text"  | 
                    |
| 465 | 
                        +  | 
                    |
| 466 | 
                        + # TODO(the-13th-letter): Do we actually need this? What should we  | 
                    |
| 467 | 
                        + # check for?  | 
                    |
| 468 | 
                        + def test_103_help_output_vault(  | 
                    |
| 469 | 
                        + self,  | 
                    |
| 470 | 
                        + ) -> None:  | 
                    |
| 471 | 
                        + """The "vault" subcommand help text has known content.  | 
                    |
| 472 | 
                        +  | 
                    |
| 473 | 
                        + TODO: Do we actually need this? What should we check for?  | 
                    |
| 474 | 
                        +  | 
                    |
| 475 | 
                        + """  | 
                    |
| 476 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 477 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 478 | 
                        + # with-statements.  | 
                    |
| 479 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 480 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 481 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 482 | 
                        + stack.enter_context(  | 
                    |
| 483 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 484 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 485 | 
                        + runner=runner,  | 
                    |
| 486 | 
                        + )  | 
                    |
| 487 | 
                        + )  | 
                    |
| 488 | 
                        + result = runner.invoke(  | 
                    |
| 489 | 
                        + cli.derivepassphrase,  | 
                    |
| 490 | 
                        + ["vault", "--help"],  | 
                    |
| 491 | 
                        + catch_exceptions=False,  | 
                    |
| 492 | 
                        + )  | 
                    |
| 493 | 
                        + assert result.clean_exit(  | 
                    |
| 494 | 
                        + empty_stderr=True, output="Passphrase generation:\n"  | 
                    |
| 495 | 
                        + ), "expected clean exit, and option groups in help text"  | 
                    |
| 496 | 
                        + assert result.clean_exit(  | 
                    |
| 497 | 
                        + empty_stderr=True, output="Use $VISUAL or $EDITOR to configure"  | 
                    |
| 498 | 
                        + ), "expected clean exit, and option group epilog in help text"  | 
                    |
| 499 | 
                        +  | 
                    |
| 500 | 
                        + @Parametrize.COMMAND_NON_EAGER_ARGUMENTS  | 
                    |
| 501 | 
                        + @Parametrize.EAGER_ARGUMENTS  | 
                    |
| 502 | 
                        + def test_200_eager_options(  | 
                    |
| 503 | 
                        + self,  | 
                    |
| 504 | 
                        + command: list[str],  | 
                    |
| 505 | 
                        + arguments: list[str],  | 
                    |
| 506 | 
                        + non_eager_arguments: list[str],  | 
                    |
| 507 | 
                        + ) -> None:  | 
                    |
| 508 | 
                        + """Eager options terminate option and argument processing."""  | 
                    |
| 509 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 510 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 511 | 
                        + # with-statements.  | 
                    |
| 512 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 513 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 514 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 515 | 
                        + stack.enter_context(  | 
                    |
| 516 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 517 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 518 | 
                        + runner=runner,  | 
                    |
| 519 | 
                        + )  | 
                    |
| 520 | 
                        + )  | 
                    |
| 521 | 
                        + result = runner.invoke(  | 
                    |
| 522 | 
                        + cli.derivepassphrase,  | 
                    |
| 523 | 
                        + [*command, *arguments, *non_eager_arguments],  | 
                    |
| 524 | 
                        + catch_exceptions=False,  | 
                    |
| 525 | 
                        + )  | 
                    |
| 526 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 527 | 
                        +  | 
                    |
| 528 | 
                        + @Parametrize.ISATTY  | 
                    |
| 529 | 
                        + @Parametrize.COLORFUL_COMMAND_INPUT  | 
                    |
| 530 | 
                        + def test_201_automatic_color_mode(  | 
                    |
| 531 | 
                        + self,  | 
                    |
| 532 | 
                        + isatty: bool,  | 
                    |
| 533 | 
                        + command_line: list[str],  | 
                    |
| 534 | 
                        + input: str | None,  | 
                    |
| 535 | 
                        + ) -> None:  | 
                    |
| 536 | 
                        + """Auto-detect if color should be used.  | 
                    |
| 537 | 
                        +  | 
                    |
| 538 | 
                        + (The answer currently is always no. See the  | 
                    |
| 539 | 
                        + [`conventional-configurable-text-styling` wishlist  | 
                    |
| 540 | 
                        + entry](../wishlist/conventional-configurable-text-styling.md).)  | 
                    |
| 541 | 
                        +  | 
                    |
| 542 | 
                        + """  | 
                    |
| 543 | 
                        + color = False  | 
                    |
| 544 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 545 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 546 | 
                        + # with-statements.  | 
                    |
| 547 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 548 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 549 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 550 | 
                        + stack.enter_context(  | 
                    |
| 551 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 552 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 553 | 
                        + runner=runner,  | 
                    |
| 554 | 
                        + )  | 
                    |
| 555 | 
                        + )  | 
                    |
| 556 | 
                        + result = runner.invoke(  | 
                    |
| 557 | 
                        + cli.derivepassphrase,  | 
                    |
| 558 | 
                        + command_line,  | 
                    |
| 559 | 
                        + input=input,  | 
                    |
| 560 | 
                        + catch_exceptions=False,  | 
                    |
| 561 | 
                        + color=isatty,  | 
                    |
| 562 | 
                        + )  | 
                    |
| 563 | 
                        + assert (  | 
                    |
| 564 | 
                        + not color  | 
                    |
| 565 | 
                        + or "\x1b[0m" in result.stderr  | 
                    |
| 566 | 
                        + or "\x1b[m" in result.stderr  | 
                    |
| 567 | 
                        + ), "Expected color, but found no ANSI reset sequence"  | 
                    |
| 568 | 
                        + assert color or "\x1b[" not in result.stderr, (  | 
                    |
| 569 | 
                        + "Expected no color, but found an ANSI control sequence"  | 
                    |
| 570 | 
                        + )  | 
                    |
| 571 | 
                        +  | 
                    |
| 572 | 
                        + def test_202a_derivepassphrase_version_option_output(  | 
                    |
| 573 | 
                        + self,  | 
                    |
| 574 | 
                        + ) -> None:  | 
                    |
| 575 | 
                        + """The version output states supported features.  | 
                    |
| 576 | 
                        +  | 
                    |
| 577 | 
                        + The version output is parsed using [`parse_version_output`][].  | 
                    |
| 578 | 
                        + Format examples can be found in  | 
                    |
| 579 | 
                        + [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 580 | 
                        + top-level `derivepassphrase` command, the output should contain  | 
                    |
| 581 | 
                        + the known and supported derivation schemes, and a list of  | 
                    |
| 582 | 
                        + subcommands.  | 
                    |
| 583 | 
                        +  | 
                    |
| 584 | 
                        + """  | 
                    |
| 585 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 586 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 587 | 
                        + # with-statements.  | 
                    |
| 588 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 589 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 590 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 591 | 
                        + stack.enter_context(  | 
                    |
| 592 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 593 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 594 | 
                        + runner=runner,  | 
                    |
| 595 | 
                        + )  | 
                    |
| 596 | 
                        + )  | 
                    |
| 597 | 
                        + result = runner.invoke(  | 
                    |
| 598 | 
                        + cli.derivepassphrase,  | 
                    |
| 599 | 
                        + ["--version"],  | 
                    |
| 600 | 
                        + catch_exceptions=False,  | 
                    |
| 601 | 
                        + )  | 
                    |
| 602 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 603 | 
                        + assert result.stdout.strip(), "expected version output"  | 
                    |
| 604 | 
                        + version_data = parse_version_output(result.stdout)  | 
                    |
| 605 | 
                        + actually_known_schemes = dict.fromkeys(_types.DerivationScheme, True)  | 
                    |
| 606 | 
                        + subcommands = set(_types.Subcommand)  | 
                    |
| 607 | 
                        + assert version_data.derivation_schemes == actually_known_schemes  | 
                    |
| 608 | 
                        + assert not version_data.foreign_configuration_formats  | 
                    |
| 609 | 
                        + assert version_data.subcommands == subcommands  | 
                    |
| 610 | 
                        + assert not version_data.features  | 
                    |
| 611 | 
                        + assert not version_data.extras  | 
                    |
| 612 | 
                        +  | 
                    |
| 613 | 
                        + def test_202b_export_version_option_output(  | 
                    |
| 614 | 
                        + self,  | 
                    |
| 615 | 
                        + ) -> None:  | 
                    |
| 616 | 
                        + """The version output states supported features.  | 
                    |
| 617 | 
                        +  | 
                    |
| 618 | 
                        + The version output is parsed using [`parse_version_output`][].  | 
                    |
| 619 | 
                        + Format examples can be found in  | 
                    |
| 620 | 
                        + [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 621 | 
                        + `export` command, the output should contain the known foreign  | 
                    |
| 622 | 
                        + configuration formats (but not marked as supported), and a list  | 
                    |
| 623 | 
                        + of subcommands.  | 
                    |
| 624 | 
                        +  | 
                    |
| 625 | 
                        + """  | 
                    |
| 626 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 627 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 628 | 
                        + # with-statements.  | 
                    |
| 629 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 630 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 631 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 632 | 
                        + stack.enter_context(  | 
                    |
| 633 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 634 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 635 | 
                        + runner=runner,  | 
                    |
| 636 | 
                        + )  | 
                    |
| 637 | 
                        + )  | 
                    |
| 638 | 
                        + result = runner.invoke(  | 
                    |
| 639 | 
                        + cli.derivepassphrase,  | 
                    |
| 640 | 
                        + ["export", "--version"],  | 
                    |
| 641 | 
                        + catch_exceptions=False,  | 
                    |
| 642 | 
                        + )  | 
                    |
| 643 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 644 | 
                        + assert result.stdout.strip(), "expected version output"  | 
                    |
| 645 | 
                        + version_data = parse_version_output(result.stdout)  | 
                    |
| 646 | 
                        +        actually_known_formats: dict[str, bool] = {
                       | 
                    |
| 647 | 
                        + _types.ForeignConfigurationFormat.VAULT_STOREROOM: False,  | 
                    |
| 648 | 
                        + _types.ForeignConfigurationFormat.VAULT_V02: False,  | 
                    |
| 649 | 
                        + _types.ForeignConfigurationFormat.VAULT_V03: False,  | 
                    |
| 650 | 
                        + }  | 
                    |
| 651 | 
                        + subcommands = set(_types.ExportSubcommand)  | 
                    |
| 652 | 
                        + assert not version_data.derivation_schemes  | 
                    |
| 653 | 
                        + assert (  | 
                    |
| 654 | 
                        + version_data.foreign_configuration_formats  | 
                    |
| 655 | 
                        + == actually_known_formats  | 
                    |
| 656 | 
                        + )  | 
                    |
| 657 | 
                        + assert version_data.subcommands == subcommands  | 
                    |
| 658 | 
                        + assert not version_data.features  | 
                    |
| 659 | 
                        + assert not version_data.extras  | 
                    |
| 660 | 
                        +  | 
                    |
| 661 | 
                        + def test_202c_export_vault_version_option_output(  | 
                    |
| 662 | 
                        + self,  | 
                    |
| 663 | 
                        + ) -> None:  | 
                    |
| 664 | 
                        + """The version output states supported features.  | 
                    |
| 665 | 
                        +  | 
                    |
| 666 | 
                        + The version output is parsed using [`parse_version_output`][].  | 
                    |
| 667 | 
                        + Format examples can be found in  | 
                    |
| 668 | 
                        + [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 669 | 
                        + `export vault` subcommand, the output should contain the  | 
                    |
| 670 | 
                        + vault-specific subset of the known or supported foreign  | 
                    |
| 671 | 
                        + configuration formats, and a list of available PEP 508 extras.  | 
                    |
| 672 | 
                        +  | 
                    |
| 673 | 
                        + """  | 
                    |
| 674 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 675 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 676 | 
                        + # with-statements.  | 
                    |
| 677 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 678 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 679 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 680 | 
                        + stack.enter_context(  | 
                    |
| 681 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 682 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 683 | 
                        + runner=runner,  | 
                    |
| 684 | 
                        + )  | 
                    |
| 685 | 
                        + )  | 
                    |
| 686 | 
                        + result = runner.invoke(  | 
                    |
| 687 | 
                        + cli.derivepassphrase,  | 
                    |
| 688 | 
                        + ["export", "vault", "--version"],  | 
                    |
| 689 | 
                        + catch_exceptions=False,  | 
                    |
| 690 | 
                        + )  | 
                    |
| 691 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 692 | 
                        + assert result.stdout.strip(), "expected version output"  | 
                    |
| 693 | 
                        + version_data = parse_version_output(result.stdout)  | 
                    |
| 694 | 
                        +        actually_known_formats: dict[str, bool] = {}
                       | 
                    |
| 695 | 
                        + actually_enabled_extras: set[str] = set()  | 
                    |
| 696 | 
                        + with contextlib.suppress(ModuleNotFoundError):  | 
                    |
| 697 | 
                        + from derivepassphrase.exporter import storeroom, vault_native # noqa: I001,PLC0415  | 
                    |
| 698 | 
                        +  | 
                    |
| 699 | 
                        +            actually_known_formats.update({
                       | 
                    |
| 700 | 
                        + _types.ForeignConfigurationFormat.VAULT_STOREROOM: not storeroom.STUBBED,  | 
                    |
| 701 | 
                        + _types.ForeignConfigurationFormat.VAULT_V02: not vault_native.STUBBED,  | 
                    |
| 702 | 
                        + _types.ForeignConfigurationFormat.VAULT_V03: not vault_native.STUBBED,  | 
                    |
| 703 | 
                        + })  | 
                    |
| 704 | 
                        + with contextlib.suppress(ModuleNotFoundError):  | 
                    |
| 705 | 
                        + import cryptography # noqa: F401,PLC0415  | 
                    |
| 706 | 
                        +  | 
                    |
| 707 | 
                        + actually_enabled_extras.add(_types.PEP508Extra.EXPORT)  | 
                    |
| 708 | 
                        + assert not version_data.derivation_schemes  | 
                    |
| 709 | 
                        + assert (  | 
                    |
| 710 | 
                        + version_data.foreign_configuration_formats  | 
                    |
| 711 | 
                        + == actually_known_formats  | 
                    |
| 712 | 
                        + )  | 
                    |
| 713 | 
                        + assert not version_data.subcommands  | 
                    |
| 714 | 
                        + assert not version_data.features  | 
                    |
| 715 | 
                        + assert version_data.extras == actually_enabled_extras  | 
                    |
| 716 | 
                        +  | 
                    |
| 717 | 
                        + def test_202d_vault_version_option_output(  | 
                    |
| 718 | 
                        + self,  | 
                    |
| 719 | 
                        + ) -> None:  | 
                    |
| 720 | 
                        + """The version output states supported features.  | 
                    |
| 721 | 
                        +  | 
                    |
| 722 | 
                        + The version output is parsed using [`parse_version_output`][].  | 
                    |
| 723 | 
                        + Format examples can be found in  | 
                    |
| 724 | 
                        + [`Parametrize.VERSION_OUTPUT_DATA`][]. Specifically, for the  | 
                    |
| 725 | 
                        + vault command, the output should not contain anything beyond the  | 
                    |
| 726 | 
                        + first paragraph.  | 
                    |
| 727 | 
                        +  | 
                    |
| 728 | 
                        + """  | 
                    |
| 729 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 730 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 731 | 
                        + # with-statements.  | 
                    |
| 732 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 733 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 734 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 735 | 
                        + stack.enter_context(  | 
                    |
| 736 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 737 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 738 | 
                        + runner=runner,  | 
                    |
| 739 | 
                        + )  | 
                    |
| 740 | 
                        + )  | 
                    |
| 741 | 
                        + result = runner.invoke(  | 
                    |
| 742 | 
                        + cli.derivepassphrase,  | 
                    |
| 743 | 
                        + ["vault", "--version"],  | 
                    |
| 744 | 
                        + catch_exceptions=False,  | 
                    |
| 745 | 
                        + )  | 
                    |
| 746 | 
                        + assert result.clean_exit(empty_stderr=True), "expected clean exit"  | 
                    |
| 747 | 
                        + assert result.stdout.strip(), "expected version output"  | 
                    |
| 748 | 
                        + version_data = parse_version_output(result.stdout)  | 
                    |
| 749 | 
                        +  | 
                    |
| 750 | 
                        + ssh_key_supported = True  | 
                    |
| 751 | 
                        +  | 
                    |
| 752 | 
                        + def react_to_notimplementederror(  | 
                    |
| 753 | 
                        + _exc: BaseException,  | 
                    |
| 754 | 
                        + ) -> None: # pragma: no cover[unused]  | 
                    |
| 755 | 
                        + nonlocal ssh_key_supported  | 
                    |
| 756 | 
                        + ssh_key_supported = False  | 
                    |
| 757 | 
                        +  | 
                    |
| 758 | 
                        +        with exceptiongroup.catch({  # noqa: SIM117
                       | 
                    |
| 759 | 
                        + NotImplementedError: react_to_notimplementederror,  | 
                    |
| 760 | 
                        + Exception: lambda *_args: None,  | 
                    |
| 761 | 
                        + }):  | 
                    |
| 762 | 
                        + with ssh_agent.SSHAgentClient.ensure_agent_subcontext():  | 
                    |
| 763 | 
                        + pass  | 
                    |
| 764 | 
                        +        features: dict[str, bool] = {
                       | 
                    |
| 765 | 
                        + _types.Feature.SSH_KEY: ssh_key_supported,  | 
                    |
| 766 | 
                        + }  | 
                    |
| 767 | 
                        + assert not version_data.derivation_schemes  | 
                    |
| 768 | 
                        + assert not version_data.foreign_configuration_formats  | 
                    |
| 769 | 
                        + assert not version_data.subcommands  | 
                    |
| 770 | 
                        + assert version_data.features == features  | 
                    |
| 771 | 
                        + assert not version_data.extras  | 
                    
| ... | ... | 
                      @@ -0,0 +1,835 @@  | 
                  
| 1 | 
                        +# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>  | 
                    |
| 2 | 
                        +#  | 
                    |
| 3 | 
                        +# SPDX-License-Identifier: Zlib  | 
                    |
| 4 | 
                        +  | 
                    |
| 5 | 
                        +from __future__ import annotations  | 
                    |
| 6 | 
                        +  | 
                    |
| 7 | 
                        +import contextlib  | 
                    |
| 8 | 
                        +import json  | 
                    |
| 9 | 
                        +import types  | 
                    |
| 10 | 
                        +from typing import TYPE_CHECKING  | 
                    |
| 11 | 
                        +  | 
                    |
| 12 | 
                        +import click.testing  | 
                    |
| 13 | 
                        +import pytest  | 
                    |
| 14 | 
                        +from typing_extensions import Any  | 
                    |
| 15 | 
                        +  | 
                    |
| 16 | 
                        +from derivepassphrase import _types, cli  | 
                    |
| 17 | 
                        +from derivepassphrase._internals import cli_helpers  | 
                    |
| 18 | 
                        +from tests import data, machinery  | 
                    |
| 19 | 
                        +from tests.machinery import pytest as pytest_machinery  | 
                    |
| 20 | 
                        +  | 
                    |
| 21 | 
                        +if TYPE_CHECKING:  | 
                    |
| 22 | 
                        + from collections.abc import Callable, Sequence  | 
                    |
| 23 | 
                        + from collections.abc import Set as AbstractSet  | 
                    |
| 24 | 
                        + from typing import NoReturn  | 
                    |
| 25 | 
                        +  | 
                    |
| 26 | 
                        + from typing_extensions import Literal  | 
                    |
| 27 | 
                        +  | 
                    |
| 28 | 
                        +DUMMY_SERVICE = data.DUMMY_SERVICE  | 
                    |
| 29 | 
                        +DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS  | 
                    |
| 30 | 
                        +  | 
                    |
| 31 | 
                        +  | 
                    |
| 32 | 
                        +def bash_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 33 | 
                        + """A formatter for `bash`-style shell completion items.  | 
                    |
| 34 | 
                        +  | 
                    |
| 35 | 
                        + The format is `type,value`, and is dictated by [`click`][].  | 
                    |
| 36 | 
                        +  | 
                    |
| 37 | 
                        + """  | 
                    |
| 38 | 
                        + type, value = ( # noqa: A001  | 
                    |
| 39 | 
                        + item.type,  | 
                    |
| 40 | 
                        + item.value,  | 
                    |
| 41 | 
                        + )  | 
                    |
| 42 | 
                        +    return f"{type},{value}"
                       | 
                    |
| 43 | 
                        +  | 
                    |
| 44 | 
                        +  | 
                    |
| 45 | 
                        +def fish_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 46 | 
                        + r"""A formatter for `fish`-style shell completion items.  | 
                    |
| 47 | 
                        +  | 
                    |
| 48 | 
                        + The format is `type,value<tab>help`, and is dictated by [`click`][].  | 
                    |
| 49 | 
                        +  | 
                    |
| 50 | 
                        + """  | 
                    |
| 51 | 
                        + type, value, help = ( # noqa: A001  | 
                    |
| 52 | 
                        + item.type,  | 
                    |
| 53 | 
                        + item.value,  | 
                    |
| 54 | 
                        + item.help,  | 
                    |
| 55 | 
                        + )  | 
                    |
| 56 | 
                        +    return f"{type},{value}\t{help}" if help else f"{type},{value}"
                       | 
                    |
| 57 | 
                        +  | 
                    |
| 58 | 
                        +  | 
                    |
| 59 | 
                        +def zsh_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 60 | 
                        + r"""A formatter for `zsh`-style shell completion items.  | 
                    |
| 61 | 
                        +  | 
                    |
| 62 | 
                        + The format is `type<newline>value<newline>help<newline>`, and is  | 
                    |
| 63 | 
                        + dictated by [`click`][]. Upstream `click` currently (v8.2.0) does  | 
                    |
| 64 | 
                        + not deal with colons in the value correctly when the help text is  | 
                    |
| 65 | 
                        + non-degenerate. Our formatter here does, provided the upstream  | 
                    |
| 66 | 
                        + `zsh` completion script is used; see the  | 
                    |
| 67 | 
                        + [`cli_machinery.ZshComplete`][] class. A request is underway to  | 
                    |
| 68 | 
                        + merge this change into upstream `click`; see  | 
                    |
| 69 | 
                        + [`pallets/click#2846`][PR2846].  | 
                    |
| 70 | 
                        +  | 
                    |
| 71 | 
                        + [PR2846]: https://github.com/pallets/click/pull/2846  | 
                    |
| 72 | 
                        +  | 
                    |
| 73 | 
                        + """  | 
                    |
| 74 | 
                        + empty_help = "_"  | 
                    |
| 75 | 
                        + help_, value = (  | 
                    |
| 76 | 
                        +        (item.help, item.value.replace(":", r"\:"))
                       | 
                    |
| 77 | 
                        + if item.help and item.help == empty_help  | 
                    |
| 78 | 
                        + else (empty_help, item.value)  | 
                    |
| 79 | 
                        + )  | 
                    |
| 80 | 
                        +    return f"{item.type}\n{value}\n{help_}"
                       | 
                    |
| 81 | 
                        +  | 
                    |
| 82 | 
                        +  | 
                    |
| 83 | 
                        +def completion_item(  | 
                    |
| 84 | 
                        + item: str | click.shell_completion.CompletionItem,  | 
                    |
| 85 | 
                        +) -> click.shell_completion.CompletionItem:  | 
                    |
| 86 | 
                        + """Convert a string to a completion item, if necessary."""  | 
                    |
| 87 | 
                        + return (  | 
                    |
| 88 | 
                        + click.shell_completion.CompletionItem(item, type="plain")  | 
                    |
| 89 | 
                        + if isinstance(item, str)  | 
                    |
| 90 | 
                        + else item  | 
                    |
| 91 | 
                        + )  | 
                    |
| 92 | 
                        +  | 
                    |
| 93 | 
                        +  | 
                    |
| 94 | 
                        +def assertable_item(  | 
                    |
| 95 | 
                        + item: str | click.shell_completion.CompletionItem,  | 
                    |
| 96 | 
                        +) -> tuple[str, Any, str | None]:  | 
                    |
| 97 | 
                        + """Convert a completion item into a pretty-printable item.  | 
                    |
| 98 | 
                        +  | 
                    |
| 99 | 
                        + Intended to make completion items introspectable in pytest's  | 
                    |
| 100 | 
                        + `assert` output.  | 
                    |
| 101 | 
                        +  | 
                    |
| 102 | 
                        + """  | 
                    |
| 103 | 
                        + item = completion_item(item)  | 
                    |
| 104 | 
                        + return (item.type, item.value, item.help)  | 
                    |
| 105 | 
                        +  | 
                    |
| 106 | 
                        +  | 
                    |
| 107 | 
                        +class Parametrize(types.SimpleNamespace):  | 
                    |
| 108 | 
                        + """Common test parametrizations."""  | 
                    |
| 109 | 
                        +  | 
                    |
| 110 | 
                        + COMPLETABLE_PATH_ARGUMENT = pytest.mark.parametrize(  | 
                    |
| 111 | 
                        + "command_prefix",  | 
                    |
| 112 | 
                        + [  | 
                    |
| 113 | 
                        + pytest.param(  | 
                    |
| 114 | 
                        +                ("export", "vault"),
                       | 
                    |
| 115 | 
                        + id="derivepassphrase-export-vault",  | 
                    |
| 116 | 
                        + ),  | 
                    |
| 117 | 
                        + pytest.param(  | 
                    |
| 118 | 
                        +                ("vault", "--export"),
                       | 
                    |
| 119 | 
                        + id="derivepassphrase-vault--export",  | 
                    |
| 120 | 
                        + ),  | 
                    |
| 121 | 
                        + pytest.param(  | 
                    |
| 122 | 
                        +                ("vault", "--import"),
                       | 
                    |
| 123 | 
                        + id="derivepassphrase-vault--import",  | 
                    |
| 124 | 
                        + ),  | 
                    |
| 125 | 
                        + ],  | 
                    |
| 126 | 
                        + )  | 
                    |
| 127 | 
                        + COMPLETABLE_OPTIONS = pytest.mark.parametrize(  | 
                    |
| 128 | 
                        + ["command_prefix", "incomplete", "completions"],  | 
                    |
| 129 | 
                        + [  | 
                    |
| 130 | 
                        + pytest.param(  | 
                    |
| 131 | 
                        + (),  | 
                    |
| 132 | 
                        + "-",  | 
                    |
| 133 | 
                        +                frozenset({
                       | 
                    |
| 134 | 
                        + "--help",  | 
                    |
| 135 | 
                        + "-h",  | 
                    |
| 136 | 
                        + "--version",  | 
                    |
| 137 | 
                        + "--debug",  | 
                    |
| 138 | 
                        + "--verbose",  | 
                    |
| 139 | 
                        + "-v",  | 
                    |
| 140 | 
                        + "--quiet",  | 
                    |
| 141 | 
                        + "-q",  | 
                    |
| 142 | 
                        + }),  | 
                    |
| 143 | 
                        + id="derivepassphrase",  | 
                    |
| 144 | 
                        + ),  | 
                    |
| 145 | 
                        + pytest.param(  | 
                    |
| 146 | 
                        +                ("export",),
                       | 
                    |
| 147 | 
                        + "-",  | 
                    |
| 148 | 
                        +                frozenset({
                       | 
                    |
| 149 | 
                        + "--help",  | 
                    |
| 150 | 
                        + "-h",  | 
                    |
| 151 | 
                        + "--version",  | 
                    |
| 152 | 
                        + "--debug",  | 
                    |
| 153 | 
                        + "--verbose",  | 
                    |
| 154 | 
                        + "-v",  | 
                    |
| 155 | 
                        + "--quiet",  | 
                    |
| 156 | 
                        + "-q",  | 
                    |
| 157 | 
                        + }),  | 
                    |
| 158 | 
                        + id="derivepassphrase-export",  | 
                    |
| 159 | 
                        + ),  | 
                    |
| 160 | 
                        + pytest.param(  | 
                    |
| 161 | 
                        +                ("export", "vault"),
                       | 
                    |
| 162 | 
                        + "-",  | 
                    |
| 163 | 
                        +                frozenset({
                       | 
                    |
| 164 | 
                        + "--help",  | 
                    |
| 165 | 
                        + "-h",  | 
                    |
| 166 | 
                        + "--version",  | 
                    |
| 167 | 
                        + "--debug",  | 
                    |
| 168 | 
                        + "--verbose",  | 
                    |
| 169 | 
                        + "-v",  | 
                    |
| 170 | 
                        + "--quiet",  | 
                    |
| 171 | 
                        + "-q",  | 
                    |
| 172 | 
                        + "--format",  | 
                    |
| 173 | 
                        + "-f",  | 
                    |
| 174 | 
                        + "--key",  | 
                    |
| 175 | 
                        + "-k",  | 
                    |
| 176 | 
                        + }),  | 
                    |
| 177 | 
                        + id="derivepassphrase-export-vault",  | 
                    |
| 178 | 
                        + ),  | 
                    |
| 179 | 
                        + pytest.param(  | 
                    |
| 180 | 
                        +                ("vault",),
                       | 
                    |
| 181 | 
                        + "-",  | 
                    |
| 182 | 
                        +                frozenset({
                       | 
                    |
| 183 | 
                        + "--help",  | 
                    |
| 184 | 
                        + "-h",  | 
                    |
| 185 | 
                        + "--version",  | 
                    |
| 186 | 
                        + "--debug",  | 
                    |
| 187 | 
                        + "--verbose",  | 
                    |
| 188 | 
                        + "-v",  | 
                    |
| 189 | 
                        + "--quiet",  | 
                    |
| 190 | 
                        + "-q",  | 
                    |
| 191 | 
                        + "--phrase",  | 
                    |
| 192 | 
                        + "-p",  | 
                    |
| 193 | 
                        + "--key",  | 
                    |
| 194 | 
                        + "-k",  | 
                    |
| 195 | 
                        + "--length",  | 
                    |
| 196 | 
                        + "-l",  | 
                    |
| 197 | 
                        + "--repeat",  | 
                    |
| 198 | 
                        + "-r",  | 
                    |
| 199 | 
                        + "--upper",  | 
                    |
| 200 | 
                        + "--lower",  | 
                    |
| 201 | 
                        + "--number",  | 
                    |
| 202 | 
                        + "--space",  | 
                    |
| 203 | 
                        + "--dash",  | 
                    |
| 204 | 
                        + "--symbol",  | 
                    |
| 205 | 
                        + "--config",  | 
                    |
| 206 | 
                        + "-c",  | 
                    |
| 207 | 
                        + "--notes",  | 
                    |
| 208 | 
                        + "-n",  | 
                    |
| 209 | 
                        + "--delete",  | 
                    |
| 210 | 
                        + "-x",  | 
                    |
| 211 | 
                        + "--delete-globals",  | 
                    |
| 212 | 
                        + "--clear",  | 
                    |
| 213 | 
                        + "-X",  | 
                    |
| 214 | 
                        + "--export",  | 
                    |
| 215 | 
                        + "-e",  | 
                    |
| 216 | 
                        + "--import",  | 
                    |
| 217 | 
                        + "-i",  | 
                    |
| 218 | 
                        + "--overwrite-existing",  | 
                    |
| 219 | 
                        + "--merge-existing",  | 
                    |
| 220 | 
                        + "--unset",  | 
                    |
| 221 | 
                        + "--export-as",  | 
                    |
| 222 | 
                        + "--modern-editor-interface",  | 
                    |
| 223 | 
                        + "--vault-legacy-editor-interface",  | 
                    |
| 224 | 
                        + "--print-notes-before",  | 
                    |
| 225 | 
                        + "--print-notes-after",  | 
                    |
| 226 | 
                        + }),  | 
                    |
| 227 | 
                        + id="derivepassphrase-vault",  | 
                    |
| 228 | 
                        + ),  | 
                    |
| 229 | 
                        + ],  | 
                    |
| 230 | 
                        + )  | 
                    |
| 231 | 
                        + COMPLETABLE_SUBCOMMANDS = pytest.mark.parametrize(  | 
                    |
| 232 | 
                        + ["command_prefix", "incomplete", "completions"],  | 
                    |
| 233 | 
                        + [  | 
                    |
| 234 | 
                        + pytest.param(  | 
                    |
| 235 | 
                        + (),  | 
                    |
| 236 | 
                        + "",  | 
                    |
| 237 | 
                        +                frozenset({"export", "vault"}),
                       | 
                    |
| 238 | 
                        + id="derivepassphrase",  | 
                    |
| 239 | 
                        + ),  | 
                    |
| 240 | 
                        + pytest.param(  | 
                    |
| 241 | 
                        +                ("export",),
                       | 
                    |
| 242 | 
                        + "",  | 
                    |
| 243 | 
                        +                frozenset({"vault"}),
                       | 
                    |
| 244 | 
                        + id="derivepassphrase-export",  | 
                    |
| 245 | 
                        + ),  | 
                    |
| 246 | 
                        + ],  | 
                    |
| 247 | 
                        + )  | 
                    |
| 248 | 
                        + COMPLETION_FUNCTION_INPUTS = pytest.mark.parametrize(  | 
                    |
| 249 | 
                        + ["config", "comp_func", "args", "incomplete", "results"],  | 
                    |
| 250 | 
                        + [  | 
                    |
| 251 | 
                        + pytest.param(  | 
                    |
| 252 | 
                        +                {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
                       | 
                    |
| 253 | 
                        + cli_helpers.shell_complete_service,  | 
                    |
| 254 | 
                        + ["vault"],  | 
                    |
| 255 | 
                        + "",  | 
                    |
| 256 | 
                        + [DUMMY_SERVICE],  | 
                    |
| 257 | 
                        + id="base_config-service",  | 
                    |
| 258 | 
                        + ),  | 
                    |
| 259 | 
                        + pytest.param(  | 
                    |
| 260 | 
                        +                {"services": {}},
                       | 
                    |
| 261 | 
                        + cli_helpers.shell_complete_service,  | 
                    |
| 262 | 
                        + ["vault"],  | 
                    |
| 263 | 
                        + "",  | 
                    |
| 264 | 
                        + [],  | 
                    |
| 265 | 
                        + id="empty_config-service",  | 
                    |
| 266 | 
                        + ),  | 
                    |
| 267 | 
                        + pytest.param(  | 
                    |
| 268 | 
                        +                {
                       | 
                    |
| 269 | 
                        +                    "services": {
                       | 
                    |
| 270 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 271 | 
                        + "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 272 | 
                        + }  | 
                    |
| 273 | 
                        + },  | 
                    |
| 274 | 
                        + cli_helpers.shell_complete_service,  | 
                    |
| 275 | 
                        + ["vault"],  | 
                    |
| 276 | 
                        + "",  | 
                    |
| 277 | 
                        + [DUMMY_SERVICE],  | 
                    |
| 278 | 
                        + id="incompletable_newline_config-service",  | 
                    |
| 279 | 
                        + ),  | 
                    |
| 280 | 
                        + pytest.param(  | 
                    |
| 281 | 
                        +                {
                       | 
                    |
| 282 | 
                        +                    "services": {
                       | 
                    |
| 283 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 284 | 
                        + "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 285 | 
                        + }  | 
                    |
| 286 | 
                        + },  | 
                    |
| 287 | 
                        + cli_helpers.shell_complete_service,  | 
                    |
| 288 | 
                        + ["vault"],  | 
                    |
| 289 | 
                        + "",  | 
                    |
| 290 | 
                        + [DUMMY_SERVICE],  | 
                    |
| 291 | 
                        + id="incompletable_backspace_config-service",  | 
                    |
| 292 | 
                        + ),  | 
                    |
| 293 | 
                        + pytest.param(  | 
                    |
| 294 | 
                        +                {
                       | 
                    |
| 295 | 
                        +                    "services": {
                       | 
                    |
| 296 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 297 | 
                        + "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 298 | 
                        + }  | 
                    |
| 299 | 
                        + },  | 
                    |
| 300 | 
                        + cli_helpers.shell_complete_service,  | 
                    |
| 301 | 
                        + ["vault"],  | 
                    |
| 302 | 
                        + "",  | 
                    |
| 303 | 
                        + sorted([DUMMY_SERVICE, "colon:in:name"]),  | 
                    |
| 304 | 
                        + id="brittle_colon_config-service",  | 
                    |
| 305 | 
                        + ),  | 
                    |
| 306 | 
                        + pytest.param(  | 
                    |
| 307 | 
                        +                {
                       | 
                    |
| 308 | 
                        +                    "services": {
                       | 
                    |
| 309 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 310 | 
                        + "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 311 | 
                        + "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 312 | 
                        + "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 313 | 
                        + "nul\x00in\x00name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 314 | 
                        + "del\x7fin\x7fname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 315 | 
                        + }  | 
                    |
| 316 | 
                        + },  | 
                    |
| 317 | 
                        + cli_helpers.shell_complete_service,  | 
                    |
| 318 | 
                        + ["vault"],  | 
                    |
| 319 | 
                        + "",  | 
                    |
| 320 | 
                        + sorted([DUMMY_SERVICE, "colon:in:name"]),  | 
                    |
| 321 | 
                        + id="brittle_incompletable_multi_config-service",  | 
                    |
| 322 | 
                        + ),  | 
                    |
| 323 | 
                        + pytest.param(  | 
                    |
| 324 | 
                        +                {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
                       | 
                    |
| 325 | 
                        + cli_helpers.shell_complete_path,  | 
                    |
| 326 | 
                        + ["vault", "--import"],  | 
                    |
| 327 | 
                        + "",  | 
                    |
| 328 | 
                        +                [click.shell_completion.CompletionItem("", type="file")],
                       | 
                    |
| 329 | 
                        + id="base_config-path",  | 
                    |
| 330 | 
                        + ),  | 
                    |
| 331 | 
                        + pytest.param(  | 
                    |
| 332 | 
                        +                {"services": {}},
                       | 
                    |
| 333 | 
                        + cli_helpers.shell_complete_path,  | 
                    |
| 334 | 
                        + ["vault", "--import"],  | 
                    |
| 335 | 
                        + "",  | 
                    |
| 336 | 
                        +                [click.shell_completion.CompletionItem("", type="file")],
                       | 
                    |
| 337 | 
                        + id="empty_config-path",  | 
                    |
| 338 | 
                        + ),  | 
                    |
| 339 | 
                        + ],  | 
                    |
| 340 | 
                        + )  | 
                    |
| 341 | 
                        + COMPLETABLE_SERVICE_NAMES = pytest.mark.parametrize(  | 
                    |
| 342 | 
                        + ["config", "incomplete", "completions"],  | 
                    |
| 343 | 
                        + [  | 
                    |
| 344 | 
                        + pytest.param(  | 
                    |
| 345 | 
                        +                {"services": {}},
                       | 
                    |
| 346 | 
                        + "",  | 
                    |
| 347 | 
                        + frozenset(),  | 
                    |
| 348 | 
                        + id="no_services",  | 
                    |
| 349 | 
                        + ),  | 
                    |
| 350 | 
                        + pytest.param(  | 
                    |
| 351 | 
                        +                {"services": {}},
                       | 
                    |
| 352 | 
                        + "partial",  | 
                    |
| 353 | 
                        + frozenset(),  | 
                    |
| 354 | 
                        + id="no_services_partial",  | 
                    |
| 355 | 
                        + ),  | 
                    |
| 356 | 
                        + pytest.param(  | 
                    |
| 357 | 
                        +                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 358 | 
                        + "",  | 
                    |
| 359 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 360 | 
                        + id="one_service",  | 
                    |
| 361 | 
                        + ),  | 
                    |
| 362 | 
                        + pytest.param(  | 
                    |
| 363 | 
                        +                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 364 | 
                        + DUMMY_SERVICE[:4],  | 
                    |
| 365 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 366 | 
                        + id="one_service_partial",  | 
                    |
| 367 | 
                        + ),  | 
                    |
| 368 | 
                        + pytest.param(  | 
                    |
| 369 | 
                        +                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 370 | 
                        + DUMMY_SERVICE[-4:],  | 
                    |
| 371 | 
                        + frozenset(),  | 
                    |
| 372 | 
                        + id="one_service_partial_miss",  | 
                    |
| 373 | 
                        + ),  | 
                    |
| 374 | 
                        + ],  | 
                    |
| 375 | 
                        + )  | 
                    |
| 376 | 
                        + SERVICE_NAME_COMPLETION_INPUTS = pytest.mark.parametrize(  | 
                    |
| 377 | 
                        + ["config", "key", "incomplete", "completions"],  | 
                    |
| 378 | 
                        + [  | 
                    |
| 379 | 
                        + pytest.param(  | 
                    |
| 380 | 
                        +                {
                       | 
                    |
| 381 | 
                        +                    "services": {
                       | 
                    |
| 382 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 383 | 
                        +                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 384 | 
                        + },  | 
                    |
| 385 | 
                        + },  | 
                    |
| 386 | 
                        + "newline\nin\nname",  | 
                    |
| 387 | 
                        + "",  | 
                    |
| 388 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 389 | 
                        + id="newline",  | 
                    |
| 390 | 
                        + ),  | 
                    |
| 391 | 
                        + pytest.param(  | 
                    |
| 392 | 
                        +                {
                       | 
                    |
| 393 | 
                        +                    "services": {
                       | 
                    |
| 394 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 395 | 
                        +                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 396 | 
                        + },  | 
                    |
| 397 | 
                        + },  | 
                    |
| 398 | 
                        + "newline\nin\nname",  | 
                    |
| 399 | 
                        + "serv",  | 
                    |
| 400 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 401 | 
                        + id="newline_partial_other",  | 
                    |
| 402 | 
                        + ),  | 
                    |
| 403 | 
                        + pytest.param(  | 
                    |
| 404 | 
                        +                {
                       | 
                    |
| 405 | 
                        +                    "services": {
                       | 
                    |
| 406 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 407 | 
                        +                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 408 | 
                        + },  | 
                    |
| 409 | 
                        + },  | 
                    |
| 410 | 
                        + "newline\nin\nname",  | 
                    |
| 411 | 
                        + "newline",  | 
                    |
| 412 | 
                        +                frozenset({}),
                       | 
                    |
| 413 | 
                        + id="newline_partial_specific",  | 
                    |
| 414 | 
                        + ),  | 
                    |
| 415 | 
                        + pytest.param(  | 
                    |
| 416 | 
                        +                {
                       | 
                    |
| 417 | 
                        +                    "services": {
                       | 
                    |
| 418 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 419 | 
                        +                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 420 | 
                        + },  | 
                    |
| 421 | 
                        + },  | 
                    |
| 422 | 
                        + "nul\x00in\x00name",  | 
                    |
| 423 | 
                        + "",  | 
                    |
| 424 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 425 | 
                        + id="nul",  | 
                    |
| 426 | 
                        + ),  | 
                    |
| 427 | 
                        + pytest.param(  | 
                    |
| 428 | 
                        +                {
                       | 
                    |
| 429 | 
                        +                    "services": {
                       | 
                    |
| 430 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 431 | 
                        +                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 432 | 
                        + },  | 
                    |
| 433 | 
                        + },  | 
                    |
| 434 | 
                        + "nul\x00in\x00name",  | 
                    |
| 435 | 
                        + "serv",  | 
                    |
| 436 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 437 | 
                        + id="nul_partial_other",  | 
                    |
| 438 | 
                        + ),  | 
                    |
| 439 | 
                        + pytest.param(  | 
                    |
| 440 | 
                        +                {
                       | 
                    |
| 441 | 
                        +                    "services": {
                       | 
                    |
| 442 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 443 | 
                        +                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 444 | 
                        + },  | 
                    |
| 445 | 
                        + },  | 
                    |
| 446 | 
                        + "nul\x00in\x00name",  | 
                    |
| 447 | 
                        + "nul",  | 
                    |
| 448 | 
                        +                frozenset({}),
                       | 
                    |
| 449 | 
                        + id="nul_partial_specific",  | 
                    |
| 450 | 
                        + ),  | 
                    |
| 451 | 
                        + pytest.param(  | 
                    |
| 452 | 
                        +                {
                       | 
                    |
| 453 | 
                        +                    "services": {
                       | 
                    |
| 454 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 455 | 
                        +                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 456 | 
                        + },  | 
                    |
| 457 | 
                        + },  | 
                    |
| 458 | 
                        + "backspace\bin\bname",  | 
                    |
| 459 | 
                        + "",  | 
                    |
| 460 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 461 | 
                        + id="backspace",  | 
                    |
| 462 | 
                        + ),  | 
                    |
| 463 | 
                        + pytest.param(  | 
                    |
| 464 | 
                        +                {
                       | 
                    |
| 465 | 
                        +                    "services": {
                       | 
                    |
| 466 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 467 | 
                        +                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 468 | 
                        + },  | 
                    |
| 469 | 
                        + },  | 
                    |
| 470 | 
                        + "backspace\bin\bname",  | 
                    |
| 471 | 
                        + "serv",  | 
                    |
| 472 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 473 | 
                        + id="backspace_partial_other",  | 
                    |
| 474 | 
                        + ),  | 
                    |
| 475 | 
                        + pytest.param(  | 
                    |
| 476 | 
                        +                {
                       | 
                    |
| 477 | 
                        +                    "services": {
                       | 
                    |
| 478 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 479 | 
                        +                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 480 | 
                        + },  | 
                    |
| 481 | 
                        + },  | 
                    |
| 482 | 
                        + "backspace\bin\bname",  | 
                    |
| 483 | 
                        + "back",  | 
                    |
| 484 | 
                        +                frozenset({}),
                       | 
                    |
| 485 | 
                        + id="backspace_partial_specific",  | 
                    |
| 486 | 
                        + ),  | 
                    |
| 487 | 
                        + pytest.param(  | 
                    |
| 488 | 
                        +                {
                       | 
                    |
| 489 | 
                        +                    "services": {
                       | 
                    |
| 490 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 491 | 
                        +                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 492 | 
                        + },  | 
                    |
| 493 | 
                        + },  | 
                    |
| 494 | 
                        + "del\x7fin\x7fname",  | 
                    |
| 495 | 
                        + "",  | 
                    |
| 496 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 497 | 
                        + id="del",  | 
                    |
| 498 | 
                        + ),  | 
                    |
| 499 | 
                        + pytest.param(  | 
                    |
| 500 | 
                        +                {
                       | 
                    |
| 501 | 
                        +                    "services": {
                       | 
                    |
| 502 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 503 | 
                        +                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 504 | 
                        + },  | 
                    |
| 505 | 
                        + },  | 
                    |
| 506 | 
                        + "del\x7fin\x7fname",  | 
                    |
| 507 | 
                        + "serv",  | 
                    |
| 508 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 509 | 
                        + id="del_partial_other",  | 
                    |
| 510 | 
                        + ),  | 
                    |
| 511 | 
                        + pytest.param(  | 
                    |
| 512 | 
                        +                {
                       | 
                    |
| 513 | 
                        +                    "services": {
                       | 
                    |
| 514 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 515 | 
                        +                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 516 | 
                        + },  | 
                    |
| 517 | 
                        + },  | 
                    |
| 518 | 
                        + "del\x7fin\x7fname",  | 
                    |
| 519 | 
                        + "del",  | 
                    |
| 520 | 
                        +                frozenset({}),
                       | 
                    |
| 521 | 
                        + id="del_partial_specific",  | 
                    |
| 522 | 
                        + ),  | 
                    |
| 523 | 
                        + ],  | 
                    |
| 524 | 
                        + )  | 
                    |
| 525 | 
                        + SERVICE_NAME_EXCEPTIONS = pytest.mark.parametrize(  | 
                    |
| 526 | 
                        + "exc_type", [RuntimeError, KeyError, ValueError]  | 
                    |
| 527 | 
                        + )  | 
                    |
| 528 | 
                        +    INCOMPLETE = pytest.mark.parametrize("incomplete", ["", "partial"])
                       | 
                    |
| 529 | 
                        +    CONFIG_SETTING_MODE = pytest.mark.parametrize("mode", ["config", "import"])
                       | 
                    |
| 530 | 
                        + COMPLETABLE_ITEMS = pytest.mark.parametrize(  | 
                    |
| 531 | 
                        + ["partial", "is_completable"],  | 
                    |
| 532 | 
                        + [  | 
                    |
| 533 | 
                        +            ("", True),
                       | 
                    |
| 534 | 
                        + (DUMMY_SERVICE, True),  | 
                    |
| 535 | 
                        +            ("a\bn", False),
                       | 
                    |
| 536 | 
                        +            ("\b", False),
                       | 
                    |
| 537 | 
                        +            ("\x00", False),
                       | 
                    |
| 538 | 
                        +            ("\x20", True),
                       | 
                    |
| 539 | 
                        +            ("\x7f", False),
                       | 
                    |
| 540 | 
                        +            ("service with spaces", True),
                       | 
                    |
| 541 | 
                        +            ("service\nwith\nnewlines", False),
                       | 
                    |
| 542 | 
                        + ],  | 
                    |
| 543 | 
                        + )  | 
                    |
| 544 | 
                        + SHELL_FORMATTER = pytest.mark.parametrize(  | 
                    |
| 545 | 
                        + ["shell", "format_func"],  | 
                    |
| 546 | 
                        + [  | 
                    |
| 547 | 
                        +            pytest.param("bash", bash_format, id="bash"),
                       | 
                    |
| 548 | 
                        +            pytest.param("fish", fish_format, id="fish"),
                       | 
                    |
| 549 | 
                        +            pytest.param("zsh", zsh_format, id="zsh"),
                       | 
                    |
| 550 | 
                        + ],  | 
                    |
| 551 | 
                        + )  | 
                    |
| 552 | 
                        +  | 
                    |
| 553 | 
                        +  | 
                    |
| 554 | 
                        +class TestShellCompletion:  | 
                    |
| 555 | 
                        + """Tests for the shell completion machinery."""  | 
                    |
| 556 | 
                        +  | 
                    |
| 557 | 
                        + class Completions:  | 
                    |
| 558 | 
                        + """A deferred completion call."""  | 
                    |
| 559 | 
                        +  | 
                    |
| 560 | 
                        + def __init__(  | 
                    |
| 561 | 
                        + self,  | 
                    |
| 562 | 
                        + args: Sequence[str],  | 
                    |
| 563 | 
                        + incomplete: str,  | 
                    |
| 564 | 
                        + ) -> None:  | 
                    |
| 565 | 
                        + """Initialize the object.  | 
                    |
| 566 | 
                        +  | 
                    |
| 567 | 
                        + Args:  | 
                    |
| 568 | 
                        + args:  | 
                    |
| 569 | 
                        + The sequence of complete command-line arguments.  | 
                    |
| 570 | 
                        + incomplete:  | 
                    |
| 571 | 
                        + The final, incomplete, partial argument.  | 
                    |
| 572 | 
                        +  | 
                    |
| 573 | 
                        + """  | 
                    |
| 574 | 
                        + self.args = tuple(args)  | 
                    |
| 575 | 
                        + self.incomplete = incomplete  | 
                    |
| 576 | 
                        +  | 
                    |
| 577 | 
                        + def __call__(self) -> Sequence[click.shell_completion.CompletionItem]:  | 
                    |
| 578 | 
                        + """Return the completion items."""  | 
                    |
| 579 | 
                        + args = list(self.args)  | 
                    |
| 580 | 
                        + completion = click.shell_completion.ShellComplete(  | 
                    |
| 581 | 
                        + cli=cli.derivepassphrase,  | 
                    |
| 582 | 
                        +                ctx_args={},
                       | 
                    |
| 583 | 
                        + prog_name="derivepassphrase",  | 
                    |
| 584 | 
                        + complete_var="_DERIVEPASSPHRASE_COMPLETE",  | 
                    |
| 585 | 
                        + )  | 
                    |
| 586 | 
                        + return completion.get_completions(args, self.incomplete)  | 
                    |
| 587 | 
                        +  | 
                    |
| 588 | 
                        + def get_words(self) -> Sequence[str]:  | 
                    |
| 589 | 
                        + """Return the completion items' values, as a sequence."""  | 
                    |
| 590 | 
                        + return tuple(c.value for c in self())  | 
                    |
| 591 | 
                        +  | 
                    |
| 592 | 
                        + @Parametrize.COMPLETABLE_ITEMS  | 
                    |
| 593 | 
                        + def test_100_is_completable_item(  | 
                    |
| 594 | 
                        + self,  | 
                    |
| 595 | 
                        + partial: str,  | 
                    |
| 596 | 
                        + is_completable: bool,  | 
                    |
| 597 | 
                        + ) -> None:  | 
                    |
| 598 | 
                        + """Our `_is_completable_item` predicate for service names works."""  | 
                    |
| 599 | 
                        + assert cli_helpers.is_completable_item(partial) == is_completable  | 
                    |
| 600 | 
                        +  | 
                    |
| 601 | 
                        + @Parametrize.COMPLETABLE_OPTIONS  | 
                    |
| 602 | 
                        + def test_200_options(  | 
                    |
| 603 | 
                        + self,  | 
                    |
| 604 | 
                        + command_prefix: Sequence[str],  | 
                    |
| 605 | 
                        + incomplete: str,  | 
                    |
| 606 | 
                        + completions: AbstractSet[str],  | 
                    |
| 607 | 
                        + ) -> None:  | 
                    |
| 608 | 
                        + """Our completion machinery works for all commands' options."""  | 
                    |
| 609 | 
                        + comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 610 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 611 | 
                        +  | 
                    |
| 612 | 
                        + @Parametrize.COMPLETABLE_SUBCOMMANDS  | 
                    |
| 613 | 
                        + def test_201_subcommands(  | 
                    |
| 614 | 
                        + self,  | 
                    |
| 615 | 
                        + command_prefix: Sequence[str],  | 
                    |
| 616 | 
                        + incomplete: str,  | 
                    |
| 617 | 
                        + completions: AbstractSet[str],  | 
                    |
| 618 | 
                        + ) -> None:  | 
                    |
| 619 | 
                        + """Our completion machinery works for all commands' subcommands."""  | 
                    |
| 620 | 
                        + comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 621 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 622 | 
                        +  | 
                    |
| 623 | 
                        + @Parametrize.COMPLETABLE_PATH_ARGUMENT  | 
                    |
| 624 | 
                        + @Parametrize.INCOMPLETE  | 
                    |
| 625 | 
                        + def test_202_paths(  | 
                    |
| 626 | 
                        + self,  | 
                    |
| 627 | 
                        + command_prefix: Sequence[str],  | 
                    |
| 628 | 
                        + incomplete: str,  | 
                    |
| 629 | 
                        + ) -> None:  | 
                    |
| 630 | 
                        + """Our completion machinery works for all commands' paths."""  | 
                    |
| 631 | 
                        +        file = click.shell_completion.CompletionItem("", type="file")
                       | 
                    |
| 632 | 
                        +        completions = frozenset({(file.type, file.value, file.help)})
                       | 
                    |
| 633 | 
                        + comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 634 | 
                        + assert (  | 
                    |
| 635 | 
                        + frozenset((x.type, x.value, x.help) for x in comp()) == completions  | 
                    |
| 636 | 
                        + )  | 
                    |
| 637 | 
                        +  | 
                    |
| 638 | 
                        + @Parametrize.COMPLETABLE_SERVICE_NAMES  | 
                    |
| 639 | 
                        + def test_203_service_names(  | 
                    |
| 640 | 
                        + self,  | 
                    |
| 641 | 
                        + config: _types.VaultConfig,  | 
                    |
| 642 | 
                        + incomplete: str,  | 
                    |
| 643 | 
                        + completions: AbstractSet[str],  | 
                    |
| 644 | 
                        + ) -> None:  | 
                    |
| 645 | 
                        + """Our completion machinery works for vault service names."""  | 
                    |
| 646 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 647 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 648 | 
                        + # with-statements.  | 
                    |
| 649 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 650 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 651 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 652 | 
                        + stack.enter_context(  | 
                    |
| 653 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 654 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 655 | 
                        + runner=runner,  | 
                    |
| 656 | 
                        + vault_config=config,  | 
                    |
| 657 | 
                        + )  | 
                    |
| 658 | 
                        + )  | 
                    |
| 659 | 
                        + comp = self.Completions(["vault"], incomplete)  | 
                    |
| 660 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 661 | 
                        +  | 
                    |
| 662 | 
                        + @Parametrize.SHELL_FORMATTER  | 
                    |
| 663 | 
                        + @Parametrize.COMPLETION_FUNCTION_INPUTS  | 
                    |
| 664 | 
                        + def test_300_shell_completion_formatting(  | 
                    |
| 665 | 
                        + self,  | 
                    |
| 666 | 
                        + shell: str,  | 
                    |
| 667 | 
                        + format_func: Callable[[click.shell_completion.CompletionItem], str],  | 
                    |
| 668 | 
                        + config: _types.VaultConfig,  | 
                    |
| 669 | 
                        + comp_func: Callable[  | 
                    |
| 670 | 
                        + [click.Context, click.Parameter, str],  | 
                    |
| 671 | 
                        + list[str | click.shell_completion.CompletionItem],  | 
                    |
| 672 | 
                        + ],  | 
                    |
| 673 | 
                        + args: list[str],  | 
                    |
| 674 | 
                        + incomplete: str,  | 
                    |
| 675 | 
                        + results: list[str | click.shell_completion.CompletionItem],  | 
                    |
| 676 | 
                        + ) -> None:  | 
                    |
| 677 | 
                        + """Custom completion functions work for all shells."""  | 
                    |
| 678 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 679 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 680 | 
                        + # with-statements.  | 
                    |
| 681 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 682 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 683 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 684 | 
                        + stack.enter_context(  | 
                    |
| 685 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 686 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 687 | 
                        + runner=runner,  | 
                    |
| 688 | 
                        + vault_config=config,  | 
                    |
| 689 | 
                        + )  | 
                    |
| 690 | 
                        + )  | 
                    |
| 691 | 
                        + expected_items = [assertable_item(item) for item in results]  | 
                    |
| 692 | 
                        + expected_string = "\n".join(  | 
                    |
| 693 | 
                        + format_func(completion_item(item)) for item in results  | 
                    |
| 694 | 
                        + )  | 
                    |
| 695 | 
                        + manual_raw_items = comp_func(  | 
                    |
| 696 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 697 | 
                        + click.Argument(["sample_parameter"]),  | 
                    |
| 698 | 
                        + incomplete,  | 
                    |
| 699 | 
                        + )  | 
                    |
| 700 | 
                        + manual_items = [assertable_item(item) for item in manual_raw_items]  | 
                    |
| 701 | 
                        + manual_string = "\n".join(  | 
                    |
| 702 | 
                        + format_func(completion_item(item)) for item in manual_raw_items  | 
                    |
| 703 | 
                        + )  | 
                    |
| 704 | 
                        + assert manual_items == expected_items  | 
                    |
| 705 | 
                        + assert manual_string == expected_string  | 
                    |
| 706 | 
                        + comp_class = click.shell_completion.get_completion_class(shell)  | 
                    |
| 707 | 
                        + assert comp_class is not None  | 
                    |
| 708 | 
                        + comp = comp_class(  | 
                    |
| 709 | 
                        + cli.derivepassphrase,  | 
                    |
| 710 | 
                        +                {},
                       | 
                    |
| 711 | 
                        + "derivepassphrase",  | 
                    |
| 712 | 
                        + "_DERIVEPASSPHRASE_COMPLETE",  | 
                    |
| 713 | 
                        + )  | 
                    |
| 714 | 
                        + monkeypatch.setattr(  | 
                    |
| 715 | 
                        + comp,  | 
                    |
| 716 | 
                        + "get_completion_args",  | 
                    |
| 717 | 
                        + lambda *_a, **_kw: (args, incomplete),  | 
                    |
| 718 | 
                        + )  | 
                    |
| 719 | 
                        + actual_raw_items = comp.get_completions(  | 
                    |
| 720 | 
                        + *comp.get_completion_args()  | 
                    |
| 721 | 
                        + )  | 
                    |
| 722 | 
                        + actual_items = [assertable_item(item) for item in actual_raw_items]  | 
                    |
| 723 | 
                        + actual_string = comp.complete()  | 
                    |
| 724 | 
                        + assert actual_items == expected_items  | 
                    |
| 725 | 
                        + assert actual_string == expected_string  | 
                    |
| 726 | 
                        +  | 
                    |
| 727 | 
                        + @Parametrize.CONFIG_SETTING_MODE  | 
                    |
| 728 | 
                        + @Parametrize.SERVICE_NAME_COMPLETION_INPUTS  | 
                    |
| 729 | 
                        + def test_400_incompletable_service_names(  | 
                    |
| 730 | 
                        + self,  | 
                    |
| 731 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 732 | 
                        + mode: Literal["config", "import"],  | 
                    |
| 733 | 
                        + config: _types.VaultConfig,  | 
                    |
| 734 | 
                        + key: str,  | 
                    |
| 735 | 
                        + incomplete: str,  | 
                    |
| 736 | 
                        + completions: AbstractSet[str],  | 
                    |
| 737 | 
                        + ) -> None:  | 
                    |
| 738 | 
                        + """Completion skips incompletable items."""  | 
                    |
| 739 | 
                        +        vault_config = config if mode == "config" else {"services": {}}
                       | 
                    |
| 740 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 741 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 742 | 
                        + # with-statements.  | 
                    |
| 743 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 744 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 745 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 746 | 
                        + stack.enter_context(  | 
                    |
| 747 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 748 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 749 | 
                        + runner=runner,  | 
                    |
| 750 | 
                        + vault_config=vault_config,  | 
                    |
| 751 | 
                        + )  | 
                    |
| 752 | 
                        + )  | 
                    |
| 753 | 
                        + if mode == "config":  | 
                    |
| 754 | 
                        + result = runner.invoke(  | 
                    |
| 755 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 756 | 
                        + ["--config", "--length=10", "--", key],  | 
                    |
| 757 | 
                        + catch_exceptions=False,  | 
                    |
| 758 | 
                        + )  | 
                    |
| 759 | 
                        + else:  | 
                    |
| 760 | 
                        + result = runner.invoke(  | 
                    |
| 761 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 762 | 
                        + ["--import", "-"],  | 
                    |
| 763 | 
                        + catch_exceptions=False,  | 
                    |
| 764 | 
                        + input=json.dumps(config),  | 
                    |
| 765 | 
                        + )  | 
                    |
| 766 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 767 | 
                        + assert machinery.warning_emitted(  | 
                    |
| 768 | 
                        + "contains an ASCII control character", caplog.record_tuples  | 
                    |
| 769 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 770 | 
                        + assert machinery.warning_emitted(  | 
                    |
| 771 | 
                        + "not be available for completion", caplog.record_tuples  | 
                    |
| 772 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 773 | 
                        + assert cli_helpers.load_config() == config  | 
                    |
| 774 | 
                        + comp = self.Completions(["vault"], incomplete)  | 
                    |
| 775 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 776 | 
                        +  | 
                    |
| 777 | 
                        + def test_410a_service_name_exceptions_not_found(  | 
                    |
| 778 | 
                        + self,  | 
                    |
| 779 | 
                        + ) -> None:  | 
                    |
| 780 | 
                        + """Service name completion quietly fails on missing configuration."""  | 
                    |
| 781 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 782 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 783 | 
                        + # with-statements.  | 
                    |
| 784 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 785 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 786 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 787 | 
                        + stack.enter_context(  | 
                    |
| 788 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 789 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 790 | 
                        + runner=runner,  | 
                    |
| 791 | 
                        +                    vault_config={
                       | 
                    |
| 792 | 
                        +                        "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 793 | 
                        + },  | 
                    |
| 794 | 
                        + )  | 
                    |
| 795 | 
                        + )  | 
                    |
| 796 | 
                        + cli_helpers.config_filename(subsystem="vault").unlink(  | 
                    |
| 797 | 
                        + missing_ok=True  | 
                    |
| 798 | 
                        + )  | 
                    |
| 799 | 
                        + assert not cli_helpers.shell_complete_service(  | 
                    |
| 800 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 801 | 
                        + click.Argument(["some_parameter"]),  | 
                    |
| 802 | 
                        + "",  | 
                    |
| 803 | 
                        + )  | 
                    |
| 804 | 
                        +  | 
                    |
| 805 | 
                        + @Parametrize.SERVICE_NAME_EXCEPTIONS  | 
                    |
| 806 | 
                        + def test_410b_service_name_exceptions_custom_error(  | 
                    |
| 807 | 
                        + self,  | 
                    |
| 808 | 
                        + exc_type: type[Exception],  | 
                    |
| 809 | 
                        + ) -> None:  | 
                    |
| 810 | 
                        + """Service name completion quietly fails on configuration errors."""  | 
                    |
| 811 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 812 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 813 | 
                        + # with-statements.  | 
                    |
| 814 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 815 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 816 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 817 | 
                        + stack.enter_context(  | 
                    |
| 818 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 819 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 820 | 
                        + runner=runner,  | 
                    |
| 821 | 
                        +                    vault_config={
                       | 
                    |
| 822 | 
                        +                        "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
                       | 
                    |
| 823 | 
                        + },  | 
                    |
| 824 | 
                        + )  | 
                    |
| 825 | 
                        + )  | 
                    |
| 826 | 
                        +  | 
                    |
| 827 | 
                        + def raiser(*_a: Any, **_kw: Any) -> NoReturn:  | 
                    |
| 828 | 
                        +                raise exc_type("just being difficult")  # noqa: EM101,TRY003
                       | 
                    |
| 829 | 
                        +  | 
                    |
| 830 | 
                        + monkeypatch.setattr(cli_helpers, "load_config", raiser)  | 
                    |
| 831 | 
                        + assert not cli_helpers.shell_complete_service(  | 
                    |
| 832 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 833 | 
                        + click.Argument(["some_parameter"]),  | 
                    |
| 834 | 
                        + "",  | 
                    |
| 835 | 
                        + )  | 
                    
| ... | ... | 
                      @@ -0,0 +1,428 @@  | 
                  
| 1 | 
                        +# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>  | 
                    |
| 2 | 
                        +#  | 
                    |
| 3 | 
                        +# SPDX-License-Identifier: Zlib  | 
                    |
| 4 | 
                        +  | 
                    |
| 5 | 
                        +# TODO(the-13th-letter): Remove this module in v1.0.  | 
                    |
| 6 | 
                        +# https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#upgrading-to-v1.0  | 
                    |
| 7 | 
                        +  | 
                    |
| 8 | 
                        +from __future__ import annotations  | 
                    |
| 9 | 
                        +  | 
                    |
| 10 | 
                        +import contextlib  | 
                    |
| 11 | 
                        +import errno  | 
                    |
| 12 | 
                        +import json  | 
                    |
| 13 | 
                        +import logging  | 
                    |
| 14 | 
                        +import os  | 
                    |
| 15 | 
                        +import pathlib  | 
                    |
| 16 | 
                        +  | 
                    |
| 17 | 
                        +import click.testing  | 
                    |
| 18 | 
                        +import pytest  | 
                    |
| 19 | 
                        +from typing_extensions import Any  | 
                    |
| 20 | 
                        +  | 
                    |
| 21 | 
                        +from derivepassphrase import cli, vault  | 
                    |
| 22 | 
                        +from derivepassphrase._internals import (  | 
                    |
| 23 | 
                        + cli_helpers,  | 
                    |
| 24 | 
                        +)  | 
                    |
| 25 | 
                        +from tests import data, machinery, test_derivepassphrase_cli  | 
                    |
| 26 | 
                        +from tests.data import callables  | 
                    |
| 27 | 
                        +from tests.machinery import pytest as pytest_machinery  | 
                    |
| 28 | 
                        +from tests.test_derivepassphrase_cli import test_utils  | 
                    |
| 29 | 
                        +  | 
                    |
| 30 | 
                        +DUMMY_SERVICE = data.DUMMY_SERVICE  | 
                    |
| 31 | 
                        +DUMMY_PASSPHRASE = data.DUMMY_PASSPHRASE  | 
                    |
| 32 | 
                        +DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS  | 
                    |
| 33 | 
                        +  | 
                    |
| 34 | 
                        +  | 
                    |
| 35 | 
                        +class Parametrize(  | 
                    |
| 36 | 
                        + test_derivepassphrase_cli.Parametrize, test_utils.Parametrize  | 
                    |
| 37 | 
                        +):  | 
                    |
| 38 | 
                        + """Common test parametrizations."""  | 
                    |
| 39 | 
                        +  | 
                    |
| 40 | 
                        + BAD_CONFIGS = pytest.mark.parametrize(  | 
                    |
| 41 | 
                        + "config",  | 
                    |
| 42 | 
                        + [  | 
                    |
| 43 | 
                        +            {"global": "", "services": {}},
                       | 
                    |
| 44 | 
                        +            {"global": 0, "services": {}},
                       | 
                    |
| 45 | 
                        +            {
                       | 
                    |
| 46 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 47 | 
                        + "services": False,  | 
                    |
| 48 | 
                        + },  | 
                    |
| 49 | 
                        +            {
                       | 
                    |
| 50 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 51 | 
                        + "services": True,  | 
                    |
| 52 | 
                        + },  | 
                    |
| 53 | 
                        +            {
                       | 
                    |
| 54 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 55 | 
                        + "services": None,  | 
                    |
| 56 | 
                        + },  | 
                    |
| 57 | 
                        + ],  | 
                    |
| 58 | 
                        + )  | 
                    |
| 59 | 
                        +  | 
                    |
| 60 | 
                        +  | 
                    |
| 61 | 
                        +class TestCLITransition:  | 
                    |
| 62 | 
                        + """Transition tests for the command-line interface up to v1.0."""  | 
                    |
| 63 | 
                        +  | 
                    |
| 64 | 
                        + @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 65 | 
                        + def test_110_load_config_backup(  | 
                    |
| 66 | 
                        + self,  | 
                    |
| 67 | 
                        + config: Any,  | 
                    |
| 68 | 
                        + ) -> None:  | 
                    |
| 69 | 
                        + """Loading the old settings file works."""  | 
                    |
| 70 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 71 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 72 | 
                        + # with-statements.  | 
                    |
| 73 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 74 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 75 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 76 | 
                        + stack.enter_context(  | 
                    |
| 77 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 78 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 79 | 
                        + runner=runner,  | 
                    |
| 80 | 
                        + )  | 
                    |
| 81 | 
                        + )  | 
                    |
| 82 | 
                        + cli_helpers.config_filename(  | 
                    |
| 83 | 
                        + subsystem="old settings.json"  | 
                    |
| 84 | 
                        + ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 85 | 
                        + assert cli_helpers.migrate_and_load_old_config()[0] == config  | 
                    |
| 86 | 
                        +  | 
                    |
| 87 | 
                        + @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 88 | 
                        + def test_111_migrate_config(  | 
                    |
| 89 | 
                        + self,  | 
                    |
| 90 | 
                        + config: Any,  | 
                    |
| 91 | 
                        + ) -> None:  | 
                    |
| 92 | 
                        + """Migrating the old settings file works."""  | 
                    |
| 93 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 94 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 95 | 
                        + # with-statements.  | 
                    |
| 96 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 97 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 98 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 99 | 
                        + stack.enter_context(  | 
                    |
| 100 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 101 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 102 | 
                        + runner=runner,  | 
                    |
| 103 | 
                        + )  | 
                    |
| 104 | 
                        + )  | 
                    |
| 105 | 
                        + cli_helpers.config_filename(  | 
                    |
| 106 | 
                        + subsystem="old settings.json"  | 
                    |
| 107 | 
                        + ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 108 | 
                        + assert cli_helpers.migrate_and_load_old_config() == (config, None)  | 
                    |
| 109 | 
                        +  | 
                    |
| 110 | 
                        + @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 111 | 
                        + def test_112_migrate_config_error(  | 
                    |
| 112 | 
                        + self,  | 
                    |
| 113 | 
                        + config: Any,  | 
                    |
| 114 | 
                        + ) -> None:  | 
                    |
| 115 | 
                        + """Migrating the old settings file atop a directory fails."""  | 
                    |
| 116 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 117 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 118 | 
                        + # with-statements.  | 
                    |
| 119 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 120 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 121 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 122 | 
                        + stack.enter_context(  | 
                    |
| 123 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 124 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 125 | 
                        + runner=runner,  | 
                    |
| 126 | 
                        + )  | 
                    |
| 127 | 
                        + )  | 
                    |
| 128 | 
                        + cli_helpers.config_filename(  | 
                    |
| 129 | 
                        + subsystem="old settings.json"  | 
                    |
| 130 | 
                        + ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 131 | 
                        + cli_helpers.config_filename(subsystem="vault").mkdir(  | 
                    |
| 132 | 
                        + parents=True, exist_ok=True  | 
                    |
| 133 | 
                        + )  | 
                    |
| 134 | 
                        + config2, err = cli_helpers.migrate_and_load_old_config()  | 
                    |
| 135 | 
                        + assert config2 == config  | 
                    |
| 136 | 
                        + assert isinstance(err, OSError)  | 
                    |
| 137 | 
                        + # The Annoying OS uses EEXIST, other OSes use EISDIR.  | 
                    |
| 138 | 
                        +            assert err.errno in {errno.EISDIR, errno.EEXIST}
                       | 
                    |
| 139 | 
                        +  | 
                    |
| 140 | 
                        + @Parametrize.BAD_CONFIGS  | 
                    |
| 141 | 
                        + def test_113_migrate_config_error_bad_config_value(  | 
                    |
| 142 | 
                        + self,  | 
                    |
| 143 | 
                        + config: Any,  | 
                    |
| 144 | 
                        + ) -> None:  | 
                    |
| 145 | 
                        + """Migrating an invalid old settings file fails."""  | 
                    |
| 146 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 147 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 148 | 
                        + # with-statements.  | 
                    |
| 149 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 150 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 151 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 152 | 
                        + stack.enter_context(  | 
                    |
| 153 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 154 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 155 | 
                        + runner=runner,  | 
                    |
| 156 | 
                        + )  | 
                    |
| 157 | 
                        + )  | 
                    |
| 158 | 
                        + cli_helpers.config_filename(  | 
                    |
| 159 | 
                        + subsystem="old settings.json"  | 
                    |
| 160 | 
                        + ).write_text(json.dumps(config, indent=2) + "\n", encoding="UTF-8")  | 
                    |
| 161 | 
                        + with pytest.raises(  | 
                    |
| 162 | 
                        + ValueError, match=cli_helpers.INVALID_VAULT_CONFIG  | 
                    |
| 163 | 
                        + ):  | 
                    |
| 164 | 
                        + cli_helpers.migrate_and_load_old_config()  | 
                    |
| 165 | 
                        +  | 
                    |
| 166 | 
                        + def test_200_forward_export_vault_path_parameter(  | 
                    |
| 167 | 
                        + self,  | 
                    |
| 168 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 169 | 
                        + ) -> None:  | 
                    |
| 170 | 
                        + """Forwarding arguments from "export" to "export vault" works."""  | 
                    |
| 171 | 
                        +        pytest.importorskip("cryptography", minversion="38.0")
                       | 
                    |
| 172 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 173 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 174 | 
                        + # with-statements.  | 
                    |
| 175 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 176 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 177 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 178 | 
                        + stack.enter_context(  | 
                    |
| 179 | 
                        + pytest_machinery.isolated_vault_exporter_config(  | 
                    |
| 180 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 181 | 
                        + runner=runner,  | 
                    |
| 182 | 
                        + vault_config=data.VAULT_V03_CONFIG,  | 
                    |
| 183 | 
                        + vault_key=data.VAULT_MASTER_KEY,  | 
                    |
| 184 | 
                        + )  | 
                    |
| 185 | 
                        + )  | 
                    |
| 186 | 
                        +            monkeypatch.setenv("VAULT_KEY", data.VAULT_MASTER_KEY)
                       | 
                    |
| 187 | 
                        + result = runner.invoke(  | 
                    |
| 188 | 
                        + cli.derivepassphrase,  | 
                    |
| 189 | 
                        + ["export", "VAULT_PATH"],  | 
                    |
| 190 | 
                        + )  | 
                    |
| 191 | 
                        + assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 192 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 193 | 
                        + "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 194 | 
                        + )  | 
                    |
| 195 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 196 | 
                        + 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 197 | 
                        + )  | 
                    |
| 198 | 
                        + assert json.loads(result.stdout) == data.VAULT_V03_CONFIG_DATA  | 
                    |
| 199 | 
                        +  | 
                    |
| 200 | 
                        + def test_201_forward_export_vault_empty_commandline(  | 
                    |
| 201 | 
                        + self,  | 
                    |
| 202 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 203 | 
                        + ) -> None:  | 
                    |
| 204 | 
                        + """Deferring from "export" to "export vault" works."""  | 
                    |
| 205 | 
                        +        pytest.importorskip("cryptography", minversion="38.0")
                       | 
                    |
| 206 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 207 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 208 | 
                        + # with-statements.  | 
                    |
| 209 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 210 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 211 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 212 | 
                        + stack.enter_context(  | 
                    |
| 213 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 214 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 215 | 
                        + runner=runner,  | 
                    |
| 216 | 
                        + )  | 
                    |
| 217 | 
                        + )  | 
                    |
| 218 | 
                        + result = runner.invoke(  | 
                    |
| 219 | 
                        + cli.derivepassphrase,  | 
                    |
| 220 | 
                        + ["export"],  | 
                    |
| 221 | 
                        + )  | 
                    |
| 222 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 223 | 
                        + "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 224 | 
                        + )  | 
                    |
| 225 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 226 | 
                        + 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 227 | 
                        + )  | 
                    |
| 228 | 
                        + assert result.error_exit(error="Missing argument 'PATH'"), (  | 
                    |
| 229 | 
                        + "expected error exit and known error type"  | 
                    |
| 230 | 
                        + )  | 
                    |
| 231 | 
                        +  | 
                    |
| 232 | 
                        + @Parametrize.CHARSET_NAME  | 
                    |
| 233 | 
                        + def test_210_forward_vault_disable_character_set(  | 
                    |
| 234 | 
                        + self,  | 
                    |
| 235 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 236 | 
                        + charset_name: str,  | 
                    |
| 237 | 
                        + ) -> None:  | 
                    |
| 238 | 
                        + """Forwarding arguments from top-level to "vault" works."""  | 
                    |
| 239 | 
                        +        option = f"--{charset_name}"
                       | 
                    |
| 240 | 
                        +        charset = vault.Vault.CHARSETS[charset_name].decode("ascii")
                       | 
                    |
| 241 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 242 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 243 | 
                        + # with-statements.  | 
                    |
| 244 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 245 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 246 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 247 | 
                        + stack.enter_context(  | 
                    |
| 248 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 249 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 250 | 
                        + runner=runner,  | 
                    |
| 251 | 
                        + )  | 
                    |
| 252 | 
                        + )  | 
                    |
| 253 | 
                        + monkeypatch.setattr(  | 
                    |
| 254 | 
                        + cli_helpers,  | 
                    |
| 255 | 
                        + "prompt_for_passphrase",  | 
                    |
| 256 | 
                        + callables.auto_prompt,  | 
                    |
| 257 | 
                        + )  | 
                    |
| 258 | 
                        + result = runner.invoke(  | 
                    |
| 259 | 
                        + cli.derivepassphrase,  | 
                    |
| 260 | 
                        + [option, "0", "-p", "--", DUMMY_SERVICE],  | 
                    |
| 261 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 262 | 
                        + catch_exceptions=False,  | 
                    |
| 263 | 
                        + )  | 
                    |
| 264 | 
                        + assert result.clean_exit(empty_stderr=False), "expected clean exit"  | 
                    |
| 265 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 266 | 
                        + "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 267 | 
                        + )  | 
                    |
| 268 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 269 | 
                        + 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 270 | 
                        + )  | 
                    |
| 271 | 
                        + for c in charset:  | 
                    |
| 272 | 
                        + assert c not in result.stdout, (  | 
                    |
| 273 | 
                        +                f"derived password contains forbidden character {c!r}"
                       | 
                    |
| 274 | 
                        + )  | 
                    |
| 275 | 
                        +  | 
                    |
| 276 | 
                        + def test_211_forward_vault_empty_command_line(  | 
                    |
| 277 | 
                        + self,  | 
                    |
| 278 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 279 | 
                        + ) -> None:  | 
                    |
| 280 | 
                        + """Deferring from top-level to "vault" works."""  | 
                    |
| 281 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 282 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 283 | 
                        + # with-statements.  | 
                    |
| 284 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 285 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 286 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 287 | 
                        + stack.enter_context(  | 
                    |
| 288 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 289 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 290 | 
                        + runner=runner,  | 
                    |
| 291 | 
                        + )  | 
                    |
| 292 | 
                        + )  | 
                    |
| 293 | 
                        + result = runner.invoke(  | 
                    |
| 294 | 
                        + cli.derivepassphrase,  | 
                    |
| 295 | 
                        + [],  | 
                    |
| 296 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 297 | 
                        + catch_exceptions=False,  | 
                    |
| 298 | 
                        + )  | 
                    |
| 299 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 300 | 
                        + "A subcommand will be required here in v1.0", caplog.record_tuples  | 
                    |
| 301 | 
                        + )  | 
                    |
| 302 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 303 | 
                        + 'Defaulting to subcommand "vault"', caplog.record_tuples  | 
                    |
| 304 | 
                        + )  | 
                    |
| 305 | 
                        + assert result.error_exit(  | 
                    |
| 306 | 
                        + error="Deriving a passphrase requires a SERVICE."  | 
                    |
| 307 | 
                        + ), "expected error exit and known error type"  | 
                    |
| 308 | 
                        +  | 
                    |
| 309 | 
                        + def test_300_export_using_old_config_file(  | 
                    |
| 310 | 
                        + self,  | 
                    |
| 311 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 312 | 
                        + ) -> None:  | 
                    |
| 313 | 
                        + """Exporting from (and migrating) the old settings file works."""  | 
                    |
| 314 | 
                        + caplog.set_level(logging.INFO)  | 
                    |
| 315 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 316 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 317 | 
                        + # with-statements.  | 
                    |
| 318 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 319 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 320 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 321 | 
                        + stack.enter_context(  | 
                    |
| 322 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 323 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 324 | 
                        + runner=runner,  | 
                    |
| 325 | 
                        + )  | 
                    |
| 326 | 
                        + )  | 
                    |
| 327 | 
                        + cli_helpers.config_filename(  | 
                    |
| 328 | 
                        + subsystem="old settings.json"  | 
                    |
| 329 | 
                        + ).write_text(  | 
                    |
| 330 | 
                        + json.dumps(  | 
                    |
| 331 | 
                        +                    {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    |
| 332 | 
                        + indent=2,  | 
                    |
| 333 | 
                        + )  | 
                    |
| 334 | 
                        + + "\n",  | 
                    |
| 335 | 
                        + encoding="UTF-8",  | 
                    |
| 336 | 
                        + )  | 
                    |
| 337 | 
                        + result = runner.invoke(  | 
                    |
| 338 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 339 | 
                        + ["--export", "-"],  | 
                    |
| 340 | 
                        + catch_exceptions=False,  | 
                    |
| 341 | 
                        + )  | 
                    |
| 342 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 343 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 344 | 
                        + "v0.1-style config file", caplog.record_tuples  | 
                    |
| 345 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 346 | 
                        + assert machinery.deprecation_info_emitted(  | 
                    |
| 347 | 
                        + "Successfully migrated to ", caplog.record_tuples  | 
                    |
| 348 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 349 | 
                        +  | 
                    |
| 350 | 
                        + def test_300a_export_using_old_config_file_migration_error(  | 
                    |
| 351 | 
                        + self,  | 
                    |
| 352 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 353 | 
                        + ) -> None:  | 
                    |
| 354 | 
                        + """Exporting from (and not migrating) the old settings file fails."""  | 
                    |
| 355 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 356 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 357 | 
                        + # with-statements.  | 
                    |
| 358 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 359 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 360 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 361 | 
                        + stack.enter_context(  | 
                    |
| 362 | 
                        + pytest_machinery.isolated_config(  | 
                    |
| 363 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 364 | 
                        + runner=runner,  | 
                    |
| 365 | 
                        + )  | 
                    |
| 366 | 
                        + )  | 
                    |
| 367 | 
                        + cli_helpers.config_filename(  | 
                    |
| 368 | 
                        + subsystem="old settings.json"  | 
                    |
| 369 | 
                        + ).write_text(  | 
                    |
| 370 | 
                        + json.dumps(  | 
                    |
| 371 | 
                        +                    {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    |
| 372 | 
                        + indent=2,  | 
                    |
| 373 | 
                        + )  | 
                    |
| 374 | 
                        + + "\n",  | 
                    |
| 375 | 
                        + encoding="UTF-8",  | 
                    |
| 376 | 
                        + )  | 
                    |
| 377 | 
                        +  | 
                    |
| 378 | 
                        + def raiser(*_args: Any, **_kwargs: Any) -> None:  | 
                    |
| 379 | 
                        + raise OSError(  | 
                    |
| 380 | 
                        + errno.EACCES,  | 
                    |
| 381 | 
                        + os.strerror(errno.EACCES),  | 
                    |
| 382 | 
                        + cli_helpers.config_filename(subsystem="vault"),  | 
                    |
| 383 | 
                        + )  | 
                    |
| 384 | 
                        +  | 
                    |
| 385 | 
                        + monkeypatch.setattr(os, "replace", raiser)  | 
                    |
| 386 | 
                        + monkeypatch.setattr(pathlib.Path, "rename", raiser)  | 
                    |
| 387 | 
                        + result = runner.invoke(  | 
                    |
| 388 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 389 | 
                        + ["--export", "-"],  | 
                    |
| 390 | 
                        + catch_exceptions=False,  | 
                    |
| 391 | 
                        + )  | 
                    |
| 392 | 
                        + assert result.clean_exit(), "expected clean exit"  | 
                    |
| 393 | 
                        + assert machinery.deprecation_warning_emitted(  | 
                    |
| 394 | 
                        + "v0.1-style config file", caplog.record_tuples  | 
                    |
| 395 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 396 | 
                        + assert machinery.warning_emitted(  | 
                    |
| 397 | 
                        + "Failed to migrate to ", caplog.record_tuples  | 
                    |
| 398 | 
                        + ), "expected known warning message in stderr"  | 
                    |
| 399 | 
                        +  | 
                    |
| 400 | 
                        + def test_400_completion_service_name_old_config_file(  | 
                    |
| 401 | 
                        + self,  | 
                    |
| 402 | 
                        + ) -> None:  | 
                    |
| 403 | 
                        + """Completing service names from the old settings file works."""  | 
                    |
| 404 | 
                        +        config = {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}
                       | 
                    |
| 405 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 406 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 407 | 
                        + # with-statements.  | 
                    |
| 408 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 409 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 410 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 411 | 
                        + stack.enter_context(  | 
                    |
| 412 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 413 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 414 | 
                        + runner=runner,  | 
                    |
| 415 | 
                        + vault_config=config,  | 
                    |
| 416 | 
                        + )  | 
                    |
| 417 | 
                        + )  | 
                    |
| 418 | 
                        + old_name = cli_helpers.config_filename(  | 
                    |
| 419 | 
                        + subsystem="old settings.json"  | 
                    |
| 420 | 
                        + )  | 
                    |
| 421 | 
                        + new_name = cli_helpers.config_filename(subsystem="vault")  | 
                    |
| 422 | 
                        + old_name.unlink(missing_ok=True)  | 
                    |
| 423 | 
                        + new_name.rename(old_name)  | 
                    |
| 424 | 
                        + assert cli_helpers.shell_complete_service(  | 
                    |
| 425 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 426 | 
                        + click.Argument(["some_parameter"]),  | 
                    |
| 427 | 
                        + "",  | 
                    |
| 428 | 
                        + ) == [DUMMY_SERVICE]  | 
                    
| ... | ... | 
                      @@ -0,0 +1,1433 @@  | 
                  
| 1 | 
                        +# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>  | 
                    |
| 2 | 
                        +#  | 
                    |
| 3 | 
                        +# SPDX-License-Identifier: Zlib  | 
                    |
| 4 | 
                        +  | 
                    |
| 5 | 
                        +from __future__ import annotations  | 
                    |
| 6 | 
                        +  | 
                    |
| 7 | 
                        +import base64  | 
                    |
| 8 | 
                        +import contextlib  | 
                    |
| 9 | 
                        +import ctypes  | 
                    |
| 10 | 
                        +import enum  | 
                    |
| 11 | 
                        +import errno  | 
                    |
| 12 | 
                        +import io  | 
                    |
| 13 | 
                        +import json  | 
                    |
| 14 | 
                        +import logging  | 
                    |
| 15 | 
                        +import operator  | 
                    |
| 16 | 
                        +import os  | 
                    |
| 17 | 
                        +import pathlib  | 
                    |
| 18 | 
                        +import shlex  | 
                    |
| 19 | 
                        +import shutil  | 
                    |
| 20 | 
                        +import socket  | 
                    |
| 21 | 
                        +import tempfile  | 
                    |
| 22 | 
                        +import types  | 
                    |
| 23 | 
                        +import warnings  | 
                    |
| 24 | 
                        +from typing import TYPE_CHECKING  | 
                    |
| 25 | 
                        +  | 
                    |
| 26 | 
                        +import click.testing  | 
                    |
| 27 | 
                        +import hypothesis  | 
                    |
| 28 | 
                        +import pytest  | 
                    |
| 29 | 
                        +from hypothesis import strategies  | 
                    |
| 30 | 
                        +from typing_extensions import Any  | 
                    |
| 31 | 
                        +  | 
                    |
| 32 | 
                        +from derivepassphrase import _types, cli, ssh_agent, vault  | 
                    |
| 33 | 
                        +from derivepassphrase._internals import (  | 
                    |
| 34 | 
                        + cli_helpers,  | 
                    |
| 35 | 
                        + cli_machinery,  | 
                    |
| 36 | 
                        +)  | 
                    |
| 37 | 
                        +from derivepassphrase.ssh_agent import socketprovider  | 
                    |
| 38 | 
                        +from tests import data, machinery  | 
                    |
| 39 | 
                        +from tests.data import callables  | 
                    |
| 40 | 
                        +from tests.machinery import hypothesis as hypothesis_machinery  | 
                    |
| 41 | 
                        +from tests.machinery import pytest as pytest_machinery  | 
                    |
| 42 | 
                        +  | 
                    |
| 43 | 
                        +if TYPE_CHECKING:  | 
                    |
| 44 | 
                        + from collections.abc import Callable, Iterable, Iterator  | 
                    |
| 45 | 
                        + from typing import NoReturn  | 
                    |
| 46 | 
                        +  | 
                    |
| 47 | 
                        +  | 
                    |
| 48 | 
                        +DUMMY_SERVICE = data.DUMMY_SERVICE  | 
                    |
| 49 | 
                        +DUMMY_PASSPHRASE = data.DUMMY_PASSPHRASE  | 
                    |
| 50 | 
                        +DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS  | 
                    |
| 51 | 
                        +DUMMY_RESULT_PASSPHRASE = data.DUMMY_RESULT_PASSPHRASE  | 
                    |
| 52 | 
                        +DUMMY_RESULT_KEY1 = data.DUMMY_RESULT_KEY1  | 
                    |
| 53 | 
                        +DUMMY_PHRASE_FROM_KEY1_RAW = data.DUMMY_PHRASE_FROM_KEY1_RAW  | 
                    |
| 54 | 
                        +DUMMY_PHRASE_FROM_KEY1 = data.DUMMY_PHRASE_FROM_KEY1  | 
                    |
| 55 | 
                        +  | 
                    |
| 56 | 
                        +DUMMY_KEY1 = data.DUMMY_KEY1  | 
                    |
| 57 | 
                        +DUMMY_KEY1_B64 = data.DUMMY_KEY1_B64  | 
                    |
| 58 | 
                        +DUMMY_KEY2 = data.DUMMY_KEY2  | 
                    |
| 59 | 
                        +DUMMY_KEY2_B64 = data.DUMMY_KEY2_B64  | 
                    |
| 60 | 
                        +DUMMY_KEY3 = data.DUMMY_KEY3  | 
                    |
| 61 | 
                        +DUMMY_KEY3_B64 = data.DUMMY_KEY3_B64  | 
                    |
| 62 | 
                        +  | 
                    |
| 63 | 
                        +TEST_CONFIGS = data.TEST_CONFIGS  | 
                    |
| 64 | 
                        +  | 
                    |
| 65 | 
                        +  | 
                    |
| 66 | 
                        +def vault_config_exporter_shell_interpreter( # noqa: C901  | 
                    |
| 67 | 
                        + script: str | Iterable[str],  | 
                    |
| 68 | 
                        + /,  | 
                    |
| 69 | 
                        + *,  | 
                    |
| 70 | 
                        + prog_name_list: list[str] | None = None,  | 
                    |
| 71 | 
                        + command: click.BaseCommand | None = None,  | 
                    |
| 72 | 
                        + runner: machinery.CliRunner | None = None,  | 
                    |
| 73 | 
                        +) -> Iterator[machinery.ReadableResult]:  | 
                    |
| 74 | 
                        + """A rudimentary sh(1) interpreter for `--export-as=sh` output.  | 
                    |
| 75 | 
                        +  | 
                    |
| 76 | 
                        + Assumes a script as emitted by `derivepassphrase vault  | 
                    |
| 77 | 
                        + --export-as=sh --export -` and interprets the calls to  | 
                    |
| 78 | 
                        + `derivepassphrase vault` within. (One call per line, skips all  | 
                    |
| 79 | 
                        + other lines.) Also has rudimentary support for (quoted)  | 
                    |
| 80 | 
                        + here-documents using `HERE` as the marker.  | 
                    |
| 81 | 
                        +  | 
                    |
| 82 | 
                        + """  | 
                    |
| 83 | 
                        + if isinstance(script, str): # pragma: no cover  | 
                    |
| 84 | 
                        + script = script.splitlines(False)  | 
                    |
| 85 | 
                        + if prog_name_list is None: # pragma: no cover  | 
                    |
| 86 | 
                        + prog_name_list = ["derivepassphrase", "vault"]  | 
                    |
| 87 | 
                        + if command is None: # pragma: no cover  | 
                    |
| 88 | 
                        + command = cli.derivepassphrase_vault  | 
                    |
| 89 | 
                        + if runner is None: # pragma: no cover  | 
                    |
| 90 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 91 | 
                        + n = len(prog_name_list)  | 
                    |
| 92 | 
                        + it = iter(script)  | 
                    |
| 93 | 
                        + while True:  | 
                    |
| 94 | 
                        + try:  | 
                    |
| 95 | 
                        + raw_line = next(it)  | 
                    |
| 96 | 
                        + except StopIteration:  | 
                    |
| 97 | 
                        + break  | 
                    |
| 98 | 
                        + else:  | 
                    |
| 99 | 
                        + line = shlex.split(raw_line)  | 
                    |
| 100 | 
                        + input_buffer: list[str] = []  | 
                    |
| 101 | 
                        + if line[:n] != prog_name_list:  | 
                    |
| 102 | 
                        + continue  | 
                    |
| 103 | 
                        + line[:n] = []  | 
                    |
| 104 | 
                        + if line and line[-1] == "<<HERE":  | 
                    |
| 105 | 
                        + # naive HERE document support  | 
                    |
| 106 | 
                        + while True:  | 
                    |
| 107 | 
                        + try:  | 
                    |
| 108 | 
                        + raw_line = next(it)  | 
                    |
| 109 | 
                        + except StopIteration as exc: # pragma: no cover  | 
                    |
| 110 | 
                        + msg = "incomplete here document"  | 
                    |
| 111 | 
                        + raise EOFError(msg) from exc  | 
                    |
| 112 | 
                        + else:  | 
                    |
| 113 | 
                        + if raw_line == "HERE":  | 
                    |
| 114 | 
                        + break  | 
                    |
| 115 | 
                        + input_buffer.append(raw_line)  | 
                    |
| 116 | 
                        + line.pop()  | 
                    |
| 117 | 
                        + yield runner.invoke(  | 
                    |
| 118 | 
                        + command,  | 
                    |
| 119 | 
                        + line,  | 
                    |
| 120 | 
                        + catch_exceptions=False,  | 
                    |
| 121 | 
                        +            input=("".join(x + "\n" for x in input_buffer) or None),
                       | 
                    |
| 122 | 
                        + )  | 
                    |
| 123 | 
                        +  | 
                    |
| 124 | 
                        +  | 
                    |
| 125 | 
                        +class ListKeysAction(str, enum.Enum):  | 
                    |
| 126 | 
                        + """Test fixture settings for [`ssh_agent.SSHAgentClient.list_keys`][].  | 
                    |
| 127 | 
                        +  | 
                    |
| 128 | 
                        + Attributes:  | 
                    |
| 129 | 
                        + EMPTY: Return an empty key list.  | 
                    |
| 130 | 
                        + FAIL: Raise an [`ssh_agent.SSHAgentFailedError`][].  | 
                    |
| 131 | 
                        + FAIL_RUNTIME: Raise an [`ssh_agent.TrailingDataError`][].  | 
                    |
| 132 | 
                        +  | 
                    |
| 133 | 
                        + """  | 
                    |
| 134 | 
                        +  | 
                    |
| 135 | 
                        + EMPTY = enum.auto()  | 
                    |
| 136 | 
                        + """"""  | 
                    |
| 137 | 
                        + FAIL = enum.auto()  | 
                    |
| 138 | 
                        + """"""  | 
                    |
| 139 | 
                        + FAIL_RUNTIME = enum.auto()  | 
                    |
| 140 | 
                        + """"""  | 
                    |
| 141 | 
                        +  | 
                    |
| 142 | 
                        + def __call__(self, *_args: Any, **_kwargs: Any) -> Any:  | 
                    |
| 143 | 
                        + """Execute the respective action."""  | 
                    |
| 144 | 
                        + # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 145 | 
                        + # matching.  | 
                    |
| 146 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 147 | 
                        + if self == self.EMPTY:  | 
                    |
| 148 | 
                        + return []  | 
                    |
| 149 | 
                        + if self == self.FAIL:  | 
                    |
| 150 | 
                        + raise ssh_agent.SSHAgentFailedError(  | 
                    |
| 151 | 
                        + _types.SSH_AGENT.FAILURE.value, b""  | 
                    |
| 152 | 
                        + )  | 
                    |
| 153 | 
                        + if self == self.FAIL_RUNTIME:  | 
                    |
| 154 | 
                        + raise ssh_agent.TrailingDataError()  | 
                    |
| 155 | 
                        + raise AssertionError()  | 
                    |
| 156 | 
                        +  | 
                    |
| 157 | 
                        +  | 
                    |
| 158 | 
                        +class SignAction(str, enum.Enum):  | 
                    |
| 159 | 
                        + """Test fixture settings for [`ssh_agent.SSHAgentClient.sign`][].  | 
                    |
| 160 | 
                        +  | 
                    |
| 161 | 
                        + Attributes:  | 
                    |
| 162 | 
                        + FAIL: Raise an [`ssh_agent.SSHAgentFailedError`][].  | 
                    |
| 163 | 
                        + FAIL_RUNTIME: Raise an [`ssh_agent.TrailingDataError`][].  | 
                    |
| 164 | 
                        +  | 
                    |
| 165 | 
                        + """  | 
                    |
| 166 | 
                        +  | 
                    |
| 167 | 
                        + FAIL = enum.auto()  | 
                    |
| 168 | 
                        + """"""  | 
                    |
| 169 | 
                        + FAIL_RUNTIME = enum.auto()  | 
                    |
| 170 | 
                        + """"""  | 
                    |
| 171 | 
                        +  | 
                    |
| 172 | 
                        + def __call__(self, *_args: Any, **_kwargs: Any) -> Any:  | 
                    |
| 173 | 
                        + """Execute the respective action."""  | 
                    |
| 174 | 
                        + # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 175 | 
                        + # matching.  | 
                    |
| 176 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 177 | 
                        + if self == self.FAIL:  | 
                    |
| 178 | 
                        + raise ssh_agent.SSHAgentFailedError(  | 
                    |
| 179 | 
                        + _types.SSH_AGENT.FAILURE.value, b""  | 
                    |
| 180 | 
                        + )  | 
                    |
| 181 | 
                        + if self == self.FAIL_RUNTIME:  | 
                    |
| 182 | 
                        + raise ssh_agent.TrailingDataError()  | 
                    |
| 183 | 
                        + raise AssertionError()  | 
                    |
| 184 | 
                        +  | 
                    |
| 185 | 
                        +  | 
                    |
| 186 | 
                        +class SocketAddressAction(str, enum.Enum):  | 
                    |
| 187 | 
                        + """Test fixture settings for the SSH agent socket address.  | 
                    |
| 188 | 
                        +  | 
                    |
| 189 | 
                        + Attributes:  | 
                    |
| 190 | 
                        + MANGLE_ANNOYING_OS_NAMED_PIPE:  | 
                    |
| 191 | 
                        + Mangle the address for the Annoying OS named pipe endpoint.  | 
                    |
| 192 | 
                        + MANGLE_SSH_AUTH_SOCK:  | 
                    |
| 193 | 
                        + Mangle the address for the UNIX domain socket (the  | 
                    |
| 194 | 
                        + `SSH_AUTH_SOCK` environment variable).  | 
                    |
| 195 | 
                        + UNSET_ANNOYING_OS_NAMED_PIPE:  | 
                    |
| 196 | 
                        + Unset the address for the Annoying OS named pipe endpoint.  | 
                    |
| 197 | 
                        + UNSET_SSH_AUTH_SOCK:  | 
                    |
| 198 | 
                        + Unset the `SSH_AUTH_SOCK` environment variable (the address  | 
                    |
| 199 | 
                        + for the UNIX domain socket).  | 
                    |
| 200 | 
                        +  | 
                    |
| 201 | 
                        + """  | 
                    |
| 202 | 
                        +  | 
                    |
| 203 | 
                        + MANGLE_ANNOYING_OS_NAMED_PIPE = enum.auto()  | 
                    |
| 204 | 
                        + """"""  | 
                    |
| 205 | 
                        + MANGLE_SSH_AUTH_SOCK = enum.auto()  | 
                    |
| 206 | 
                        + """"""  | 
                    |
| 207 | 
                        + UNSET_ANNOYING_OS_NAMED_PIPE = enum.auto()  | 
                    |
| 208 | 
                        + """"""  | 
                    |
| 209 | 
                        + UNSET_SSH_AUTH_SOCK = enum.auto()  | 
                    |
| 210 | 
                        + """"""  | 
                    |
| 211 | 
                        +  | 
                    |
| 212 | 
                        + def __call__(  | 
                    |
| 213 | 
                        + self, monkeypatch: pytest.MonkeyPatch, /, *_args: Any, **_kwargs: Any  | 
                    |
| 214 | 
                        + ) -> None:  | 
                    |
| 215 | 
                        + """Execute the respective action."""  | 
                    |
| 216 | 
                        + # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 217 | 
                        + # matching.  | 
                    |
| 218 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 219 | 
                        +        if self in {
                       | 
                    |
| 220 | 
                        + self.MANGLE_ANNOYING_OS_NAMED_PIPE,  | 
                    |
| 221 | 
                        + self.UNSET_ANNOYING_OS_NAMED_PIPE,  | 
                    |
| 222 | 
                        + }: # pragma: no cover [unused]  | 
                    |
| 223 | 
                        + pass  | 
                    |
| 224 | 
                        + elif self == self.MANGLE_SSH_AUTH_SOCK:  | 
                    |
| 225 | 
                        + monkeypatch.setenv(  | 
                    |
| 226 | 
                        + "SSH_AUTH_SOCK", os.environ["SSH_AUTH_SOCK"] + "~"  | 
                    |
| 227 | 
                        + )  | 
                    |
| 228 | 
                        + elif self == self.UNSET_SSH_AUTH_SOCK:  | 
                    |
| 229 | 
                        +            monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
                       | 
                    |
| 230 | 
                        + else:  | 
                    |
| 231 | 
                        + raise AssertionError()  | 
                    |
| 232 | 
                        +  | 
                    |
| 233 | 
                        +  | 
                    |
| 234 | 
                        +class SystemSupportAction(str, enum.Enum):  | 
                    |
| 235 | 
                        + """Test fixture settings for [`ssh_agent.SSHAgentClient`][] system support.  | 
                    |
| 236 | 
                        +  | 
                    |
| 237 | 
                        + Attributes:  | 
                    |
| 238 | 
                        + UNSET_AF_UNIX:  | 
                    |
| 239 | 
                        + Ensure lack of support for UNIX domain sockets.  | 
                    |
| 240 | 
                        + UNSET_AF_UNIX_AND_ENSURE_USE:  | 
                    |
| 241 | 
                        + Ensure lack of support for UNIX domain sockets, and that the  | 
                    |
| 242 | 
                        + agent will use this socket provider.  | 
                    |
| 243 | 
                        + UNSET_NATIVE:  | 
                    |
| 244 | 
                        + Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`.  | 
                    |
| 245 | 
                        + UNSET_NATIVE_AND_ENSURE_USE:  | 
                    |
| 246 | 
                        + Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`, and that the  | 
                    |
| 247 | 
                        + agent will use the native socket provider.  | 
                    |
| 248 | 
                        + UNSET_PROVIDER_LIST:  | 
                    |
| 249 | 
                        + Ensure an empty list of SSH agent socket providers.  | 
                    |
| 250 | 
                        + UNSET_WINDLL:  | 
                    |
| 251 | 
                        + Ensure lack of support for The Annoying OS named pipes.  | 
                    |
| 252 | 
                        + UNSET_WINDLL_AND_ENSURE_USE:  | 
                    |
| 253 | 
                        + Ensure lack of support for The Annoying OS named pipes, and  | 
                    |
| 254 | 
                        + that the agent will use this socket provider.  | 
                    |
| 255 | 
                        +  | 
                    |
| 256 | 
                        + """  | 
                    |
| 257 | 
                        +  | 
                    |
| 258 | 
                        + UNSET_AF_UNIX = enum.auto()  | 
                    |
| 259 | 
                        + """"""  | 
                    |
| 260 | 
                        + UNSET_AF_UNIX_AND_ENSURE_USE = enum.auto()  | 
                    |
| 261 | 
                        + """"""  | 
                    |
| 262 | 
                        + UNSET_NATIVE = enum.auto()  | 
                    |
| 263 | 
                        + """"""  | 
                    |
| 264 | 
                        + UNSET_NATIVE_AND_ENSURE_USE = enum.auto()  | 
                    |
| 265 | 
                        + """"""  | 
                    |
| 266 | 
                        + UNSET_PROVIDER_LIST = enum.auto()  | 
                    |
| 267 | 
                        + """"""  | 
                    |
| 268 | 
                        + UNSET_WINDLL = enum.auto()  | 
                    |
| 269 | 
                        + """"""  | 
                    |
| 270 | 
                        + UNSET_WINDLL_AND_ENSURE_USE = enum.auto()  | 
                    |
| 271 | 
                        + """"""  | 
                    |
| 272 | 
                        +  | 
                    |
| 273 | 
                        + def __call__(  | 
                    |
| 274 | 
                        + self, monkeypatch: pytest.MonkeyPatch, /, *_args: Any, **_kwargs: Any  | 
                    |
| 275 | 
                        + ) -> None:  | 
                    |
| 276 | 
                        + """Execute the respective action.  | 
                    |
| 277 | 
                        +  | 
                    |
| 278 | 
                        + Args:  | 
                    |
| 279 | 
                        + monkeypatch: The current monkeypatch context.  | 
                    |
| 280 | 
                        +  | 
                    |
| 281 | 
                        + """  | 
                    |
| 282 | 
                        + # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 283 | 
                        + # matching.  | 
                    |
| 284 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 285 | 
                        + if self == self.UNSET_PROVIDER_LIST:  | 
                    |
| 286 | 
                        + monkeypatch.setattr(  | 
                    |
| 287 | 
                        + ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", []  | 
                    |
| 288 | 
                        + )  | 
                    |
| 289 | 
                        +        elif self in {self.UNSET_NATIVE, self.UNSET_NATIVE_AND_ENSURE_USE}:
                       | 
                    |
| 290 | 
                        + self.check_or_ensure_use(  | 
                    |
| 291 | 
                        + "native",  | 
                    |
| 292 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 293 | 
                        + ensure_use=(self == self.UNSET_NATIVE_AND_ENSURE_USE),  | 
                    |
| 294 | 
                        + )  | 
                    |
| 295 | 
                        + monkeypatch.delattr(socket, "AF_UNIX", raising=False)  | 
                    |
| 296 | 
                        + monkeypatch.delattr(ctypes, "WinDLL", raising=False)  | 
                    |
| 297 | 
                        + monkeypatch.delattr(ctypes, "windll", raising=False)  | 
                    |
| 298 | 
                        +        elif self in {self.UNSET_AF_UNIX, self.UNSET_AF_UNIX_AND_ENSURE_USE}:
                       | 
                    |
| 299 | 
                        + self.check_or_ensure_use(  | 
                    |
| 300 | 
                        + "posix",  | 
                    |
| 301 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 302 | 
                        + ensure_use=(self == self.UNSET_AF_UNIX_AND_ENSURE_USE),  | 
                    |
| 303 | 
                        + )  | 
                    |
| 304 | 
                        + monkeypatch.delattr(socket, "AF_UNIX", raising=False)  | 
                    |
| 305 | 
                        +        elif self in {self.UNSET_WINDLL, self.UNSET_WINDLL_AND_ENSURE_USE}:
                       | 
                    |
| 306 | 
                        + self.check_or_ensure_use(  | 
                    |
| 307 | 
                        + "the_annoying_os",  | 
                    |
| 308 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 309 | 
                        + ensure_use=(self == self.UNSET_WINDLL_AND_ENSURE_USE),  | 
                    |
| 310 | 
                        + )  | 
                    |
| 311 | 
                        + monkeypatch.delattr(ctypes, "WinDLL", raising=False)  | 
                    |
| 312 | 
                        + monkeypatch.delattr(ctypes, "windll", raising=False)  | 
                    |
| 313 | 
                        + else:  | 
                    |
| 314 | 
                        + raise AssertionError()  | 
                    |
| 315 | 
                        +  | 
                    |
| 316 | 
                        + @staticmethod  | 
                    |
| 317 | 
                        + def check_or_ensure_use(  | 
                    |
| 318 | 
                        + provider: str, /, *, monkeypatch: pytest.MonkeyPatch, ensure_use: bool  | 
                    |
| 319 | 
                        + ) -> None:  | 
                    |
| 320 | 
                        + """Check that the named SSH agent socket provider will be used.  | 
                    |
| 321 | 
                        +  | 
                    |
| 322 | 
                        + Either ensure that the socket provider will definitely be used,  | 
                    |
| 323 | 
                        + or, upon detecting that it won't be used, skip the test.  | 
                    |
| 324 | 
                        +  | 
                    |
| 325 | 
                        + Args:  | 
                    |
| 326 | 
                        + provider:  | 
                    |
| 327 | 
                        + The provider to check for.  | 
                    |
| 328 | 
                        + ensure_use:  | 
                    |
| 329 | 
                        + If true, ensure that the socket provider will definitely  | 
                    |
| 330 | 
                        + be used. If false, then check for whether it will be  | 
                    |
| 331 | 
                        + used, and skip this test if not.  | 
                    |
| 332 | 
                        + monkeypatch:  | 
                    |
| 333 | 
                        + The monkeypatch context within which the fixture  | 
                    |
| 334 | 
                        + adjustments should be executed.  | 
                    |
| 335 | 
                        +  | 
                    |
| 336 | 
                        + """  | 
                    |
| 337 | 
                        + if ensure_use:  | 
                    |
| 338 | 
                        + monkeypatch.setattr(  | 
                    |
| 339 | 
                        + ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", [provider]  | 
                    |
| 340 | 
                        + )  | 
                    |
| 341 | 
                        + else: # pragma: no cover [external]  | 
                    |
| 342 | 
                        + # This branch operates completely on instrumented or on  | 
                    |
| 343 | 
                        + # externally defined, non-deterministic state.  | 
                    |
| 344 | 
                        + intended: (  | 
                    |
| 345 | 
                        + _types.SSHAgentSocketProvider  | 
                    |
| 346 | 
                        + | socketprovider.NoSuchProviderError  | 
                    |
| 347 | 
                        + | None  | 
                    |
| 348 | 
                        + )  | 
                    |
| 349 | 
                        + try:  | 
                    |
| 350 | 
                        + intended = socketprovider.SocketProvider.lookup(provider)  | 
                    |
| 351 | 
                        + except socketprovider.NoSuchProviderError as exc:  | 
                    |
| 352 | 
                        + intended = exc  | 
                    |
| 353 | 
                        + actual: (  | 
                    |
| 354 | 
                        + _types.SSHAgentSocketProvider  | 
                    |
| 355 | 
                        + | socketprovider.NoSuchProviderError  | 
                    |
| 356 | 
                        + | None  | 
                    |
| 357 | 
                        + )  | 
                    |
| 358 | 
                        + for name in ssh_agent.SSHAgentClient.SOCKET_PROVIDERS:  | 
                    |
| 359 | 
                        + try:  | 
                    |
| 360 | 
                        + actual = socketprovider.SocketProvider.lookup(name)  | 
                    |
| 361 | 
                        + except socketprovider.NoSuchProviderError as exc:  | 
                    |
| 362 | 
                        + actual = exc  | 
                    |
| 363 | 
                        + if actual is None:  | 
                    |
| 364 | 
                        + continue  | 
                    |
| 365 | 
                        + break  | 
                    |
| 366 | 
                        + else:  | 
                    |
| 367 | 
                        + actual = None  | 
                    |
| 368 | 
                        + if intended != actual:  | 
                    |
| 369 | 
                        + pytest.skip(  | 
                    |
| 370 | 
                        +                    f"{provider!r} SSH agent socket provider "
                       | 
                    |
| 371 | 
                        + f"is not currently in use"  | 
                    |
| 372 | 
                        + )  | 
                    |
| 373 | 
                        +  | 
                    |
| 374 | 
                        +  | 
                    |
| 375 | 
                        +class Parametrize(types.SimpleNamespace):  | 
                    |
| 376 | 
                        + """Common test parametrizations."""  | 
                    |
| 377 | 
                        +  | 
                    |
| 378 | 
                        + DELETE_CONFIG_INPUT = pytest.mark.parametrize(  | 
                    |
| 379 | 
                        + ["command_line", "config", "result_config"],  | 
                    |
| 380 | 
                        + [  | 
                    |
| 381 | 
                        + pytest.param(  | 
                    |
| 382 | 
                        + ["--delete-globals"],  | 
                    |
| 383 | 
                        +                {"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 384 | 
                        +                {"services": {}},
                       | 
                    |
| 385 | 
                        + id="globals",  | 
                    |
| 386 | 
                        + ),  | 
                    |
| 387 | 
                        + pytest.param(  | 
                    |
| 388 | 
                        + ["--delete", "--", DUMMY_SERVICE],  | 
                    |
| 389 | 
                        +                {
                       | 
                    |
| 390 | 
                        +                    "global": {"phrase": "abc"},
                       | 
                    |
| 391 | 
                        +                    "services": {DUMMY_SERVICE: {"notes": "..."}},
                       | 
                    |
| 392 | 
                        + },  | 
                    |
| 393 | 
                        +                {"global": {"phrase": "abc"}, "services": {}},
                       | 
                    |
| 394 | 
                        + id="service",  | 
                    |
| 395 | 
                        + ),  | 
                    |
| 396 | 
                        + pytest.param(  | 
                    |
| 397 | 
                        + ["--clear"],  | 
                    |
| 398 | 
                        +                {
                       | 
                    |
| 399 | 
                        +                    "global": {"phrase": "abc"},
                       | 
                    |
| 400 | 
                        +                    "services": {DUMMY_SERVICE: {"notes": "..."}},
                       | 
                    |
| 401 | 
                        + },  | 
                    |
| 402 | 
                        +                {"services": {}},
                       | 
                    |
| 403 | 
                        + id="all",  | 
                    |
| 404 | 
                        + ),  | 
                    |
| 405 | 
                        + ],  | 
                    |
| 406 | 
                        + )  | 
                    |
| 407 | 
                        + BASE_CONFIG_VARIATIONS = pytest.mark.parametrize(  | 
                    |
| 408 | 
                        + "config",  | 
                    |
| 409 | 
                        + [  | 
                    |
| 410 | 
                        +            {"global": {"phrase": "my passphrase"}, "services": {}},
                       | 
                    |
| 411 | 
                        +            {"global": {"key": DUMMY_KEY1_B64}, "services": {}},
                       | 
                    |
| 412 | 
                        +            {
                       | 
                    |
| 413 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 414 | 
                        +                "services": {"sv": {"phrase": "my passphrase"}},
                       | 
                    |
| 415 | 
                        + },  | 
                    |
| 416 | 
                        +            {
                       | 
                    |
| 417 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 418 | 
                        +                "services": {"sv": {"key": DUMMY_KEY1_B64}},
                       | 
                    |
| 419 | 
                        + },  | 
                    |
| 420 | 
                        +            {
                       | 
                    |
| 421 | 
                        +                "global": {"phrase": "abc"},
                       | 
                    |
| 422 | 
                        +                "services": {"sv": {"key": DUMMY_KEY1_B64, "length": 15}},
                       | 
                    |
| 423 | 
                        + },  | 
                    |
| 424 | 
                        + ],  | 
                    |
| 425 | 
                        + )  | 
                    |
| 426 | 
                        + CONNECTION_HINTS = pytest.mark.parametrize(  | 
                    |
| 427 | 
                        + "conn_hint", ["none", "socket", "client"]  | 
                    |
| 428 | 
                        + )  | 
                    |
| 429 | 
                        + KEY_TO_PHRASE_SETTINGS = pytest.mark.parametrize(  | 
                    |
| 430 | 
                        + [  | 
                    |
| 431 | 
                        + "list_keys_action",  | 
                    |
| 432 | 
                        + "address_action",  | 
                    |
| 433 | 
                        + "system_support_action",  | 
                    |
| 434 | 
                        + "sign_action",  | 
                    |
| 435 | 
                        + "pattern",  | 
                    |
| 436 | 
                        + ],  | 
                    |
| 437 | 
                        + [  | 
                    |
| 438 | 
                        + pytest.param(  | 
                    |
| 439 | 
                        + ListKeysAction.EMPTY,  | 
                    |
| 440 | 
                        + None,  | 
                    |
| 441 | 
                        + None,  | 
                    |
| 442 | 
                        + SignAction.FAIL,  | 
                    |
| 443 | 
                        + "not loaded into the agent",  | 
                    |
| 444 | 
                        + id="key-not-loaded",  | 
                    |
| 445 | 
                        + ),  | 
                    |
| 446 | 
                        + pytest.param(  | 
                    |
| 447 | 
                        + ListKeysAction.FAIL,  | 
                    |
| 448 | 
                        + None,  | 
                    |
| 449 | 
                        + None,  | 
                    |
| 450 | 
                        + SignAction.FAIL,  | 
                    |
| 451 | 
                        + "SSH agent failed to or refused to",  | 
                    |
| 452 | 
                        + id="list-keys-refused",  | 
                    |
| 453 | 
                        + ),  | 
                    |
| 454 | 
                        + pytest.param(  | 
                    |
| 455 | 
                        + ListKeysAction.FAIL_RUNTIME,  | 
                    |
| 456 | 
                        + None,  | 
                    |
| 457 | 
                        + None,  | 
                    |
| 458 | 
                        + SignAction.FAIL,  | 
                    |
| 459 | 
                        + "SSH agent failed to or refused to",  | 
                    |
| 460 | 
                        + id="list-keys-protocol-error",  | 
                    |
| 461 | 
                        + ),  | 
                    |
| 462 | 
                        + pytest.param(  | 
                    |
| 463 | 
                        + None,  | 
                    |
| 464 | 
                        + SocketAddressAction.UNSET_SSH_AUTH_SOCK,  | 
                    |
| 465 | 
                        + None,  | 
                    |
| 466 | 
                        + SignAction.FAIL,  | 
                    |
| 467 | 
                        + "Cannot find any running SSH agent",  | 
                    |
| 468 | 
                        + id="agent-address-missing",  | 
                    |
| 469 | 
                        + ),  | 
                    |
| 470 | 
                        + pytest.param(  | 
                    |
| 471 | 
                        + None,  | 
                    |
| 472 | 
                        + SocketAddressAction.MANGLE_SSH_AUTH_SOCK,  | 
                    |
| 473 | 
                        + None,  | 
                    |
| 474 | 
                        + SignAction.FAIL,  | 
                    |
| 475 | 
                        + "Cannot connect to the SSH agent",  | 
                    |
| 476 | 
                        + id="agent-address-mangled",  | 
                    |
| 477 | 
                        + ),  | 
                    |
| 478 | 
                        + pytest.param(  | 
                    |
| 479 | 
                        + None,  | 
                    |
| 480 | 
                        + None,  | 
                    |
| 481 | 
                        + SystemSupportAction.UNSET_NATIVE,  | 
                    |
| 482 | 
                        + SignAction.FAIL,  | 
                    |
| 483 | 
                        + "does not support communicating with it",  | 
                    |
| 484 | 
                        + id="no-agent-support",  | 
                    |
| 485 | 
                        + ),  | 
                    |
| 486 | 
                        + pytest.param(  | 
                    |
| 487 | 
                        + None,  | 
                    |
| 488 | 
                        + None,  | 
                    |
| 489 | 
                        + SystemSupportAction.UNSET_PROVIDER_LIST,  | 
                    |
| 490 | 
                        + SignAction.FAIL,  | 
                    |
| 491 | 
                        + "does not support communicating with it",  | 
                    |
| 492 | 
                        + id="no-agent-support",  | 
                    |
| 493 | 
                        + ),  | 
                    |
| 494 | 
                        + pytest.param(  | 
                    |
| 495 | 
                        + None,  | 
                    |
| 496 | 
                        + None,  | 
                    |
| 497 | 
                        + SystemSupportAction.UNSET_AF_UNIX_AND_ENSURE_USE,  | 
                    |
| 498 | 
                        + SignAction.FAIL,  | 
                    |
| 499 | 
                        + "does not support communicating with it",  | 
                    |
| 500 | 
                        + id="no-agent-support",  | 
                    |
| 501 | 
                        + ),  | 
                    |
| 502 | 
                        + pytest.param(  | 
                    |
| 503 | 
                        + None,  | 
                    |
| 504 | 
                        + None,  | 
                    |
| 505 | 
                        + SystemSupportAction.UNSET_WINDLL_AND_ENSURE_USE,  | 
                    |
| 506 | 
                        + SignAction.FAIL,  | 
                    |
| 507 | 
                        + "does not support communicating with it",  | 
                    |
| 508 | 
                        + id="no-agent-support",  | 
                    |
| 509 | 
                        + ),  | 
                    |
| 510 | 
                        + pytest.param(  | 
                    |
| 511 | 
                        + None,  | 
                    |
| 512 | 
                        + None,  | 
                    |
| 513 | 
                        + None,  | 
                    |
| 514 | 
                        + SignAction.FAIL_RUNTIME,  | 
                    |
| 515 | 
                        + "violates the communication protocol",  | 
                    |
| 516 | 
                        + id="sign-violates-protocol",  | 
                    |
| 517 | 
                        + ),  | 
                    |
| 518 | 
                        + ],  | 
                    |
| 519 | 
                        + )  | 
                    |
| 520 | 
                        + VALIDATION_FUNCTION_INPUT = pytest.mark.parametrize(  | 
                    |
| 521 | 
                        + ["vfunc", "input"],  | 
                    |
| 522 | 
                        + [  | 
                    |
| 523 | 
                        + (cli_machinery.validate_occurrence_constraint, 20),  | 
                    |
| 524 | 
                        + (cli_machinery.validate_length, 20),  | 
                    |
| 525 | 
                        + ],  | 
                    |
| 526 | 
                        + )  | 
                    |
| 527 | 
                        +  | 
                    |
| 528 | 
                        +  | 
                    |
| 529 | 
                        +class TestCLIUtils:  | 
                    |
| 530 | 
                        + """Tests for command-line utility functions."""  | 
                    |
| 531 | 
                        +  | 
                    |
| 532 | 
                        + @Parametrize.BASE_CONFIG_VARIATIONS  | 
                    |
| 533 | 
                        + def test_100_load_config(  | 
                    |
| 534 | 
                        + self,  | 
                    |
| 535 | 
                        + config: Any,  | 
                    |
| 536 | 
                        + ) -> None:  | 
                    |
| 537 | 
                        + """[`cli_helpers.load_config`][] works for valid configurations."""  | 
                    |
| 538 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 539 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 540 | 
                        + # with-statements.  | 
                    |
| 541 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 542 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 543 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 544 | 
                        + stack.enter_context(  | 
                    |
| 545 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 546 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 547 | 
                        + runner=runner,  | 
                    |
| 548 | 
                        + vault_config=config,  | 
                    |
| 549 | 
                        + )  | 
                    |
| 550 | 
                        + )  | 
                    |
| 551 | 
                        + config_filename = cli_helpers.config_filename(subsystem="vault")  | 
                    |
| 552 | 
                        + with config_filename.open(encoding="UTF-8") as fileobj:  | 
                    |
| 553 | 
                        + assert json.load(fileobj) == config  | 
                    |
| 554 | 
                        + assert cli_helpers.load_config() == config  | 
                    |
| 555 | 
                        +  | 
                    |
| 556 | 
                        + def test_110_save_bad_config(  | 
                    |
| 557 | 
                        + self,  | 
                    |
| 558 | 
                        + ) -> None:  | 
                    |
| 559 | 
                        + """[`cli_helpers.save_config`][] fails for bad configurations."""  | 
                    |
| 560 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 561 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 562 | 
                        + # with-statements.  | 
                    |
| 563 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 564 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 565 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 566 | 
                        + stack.enter_context(  | 
                    |
| 567 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 568 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 569 | 
                        + runner=runner,  | 
                    |
| 570 | 
                        +                    vault_config={},
                       | 
                    |
| 571 | 
                        + )  | 
                    |
| 572 | 
                        + )  | 
                    |
| 573 | 
                        + stack.enter_context(  | 
                    |
| 574 | 
                        + pytest.raises(ValueError, match="Invalid vault config")  | 
                    |
| 575 | 
                        + )  | 
                    |
| 576 | 
                        + cli_helpers.save_config(None) # type: ignore[arg-type]  | 
                    |
| 577 | 
                        +  | 
                    |
| 578 | 
                        + def test_111_prompt_for_selection_multiple(self) -> None:  | 
                    |
| 579 | 
                        + """[`cli_helpers.prompt_for_selection`][] works in the "multiple" case."""  | 
                    |
| 580 | 
                        +  | 
                    |
| 581 | 
                        + @click.command()  | 
                    |
| 582 | 
                        +        @click.option("--heading", default="Our menu:")
                       | 
                    |
| 583 | 
                        +        @click.argument("items", nargs=-1)
                       | 
                    |
| 584 | 
                        + def driver(heading: str, items: list[str]) -> None:  | 
                    |
| 585 | 
                        + # from https://montypython.fandom.com/wiki/Spam#The_menu  | 
                    |
| 586 | 
                        + items = items or [  | 
                    |
| 587 | 
                        + "Egg and bacon",  | 
                    |
| 588 | 
                        + "Egg, sausage and bacon",  | 
                    |
| 589 | 
                        + "Egg and spam",  | 
                    |
| 590 | 
                        + "Egg, bacon and spam",  | 
                    |
| 591 | 
                        + "Egg, bacon, sausage and spam",  | 
                    |
| 592 | 
                        + "Spam, bacon, sausage and spam",  | 
                    |
| 593 | 
                        + "Spam, egg, spam, spam, bacon and spam",  | 
                    |
| 594 | 
                        + "Spam, spam, spam, egg and spam",  | 
                    |
| 595 | 
                        + (  | 
                    |
| 596 | 
                        + "Spam, spam, spam, spam, spam, spam, baked beans, "  | 
                    |
| 597 | 
                        + "spam, spam, spam and spam"  | 
                    |
| 598 | 
                        + ),  | 
                    |
| 599 | 
                        + (  | 
                    |
| 600 | 
                        + "Lobster thermidor aux crevettes with a mornay sauce "  | 
                    |
| 601 | 
                        + "garnished with truffle paté, brandy "  | 
                    |
| 602 | 
                        + "and a fried egg on top and spam"  | 
                    |
| 603 | 
                        + ),  | 
                    |
| 604 | 
                        + ]  | 
                    |
| 605 | 
                        + index = cli_helpers.prompt_for_selection(items, heading=heading)  | 
                    |
| 606 | 
                        +            click.echo("A fine choice: ", nl=False)
                       | 
                    |
| 607 | 
                        + click.echo(items[index])  | 
                    |
| 608 | 
                        +            click.echo("(Note: Vikings strictly optional.)")
                       | 
                    |
| 609 | 
                        +  | 
                    |
| 610 | 
                        + runner = machinery.CliRunner(mix_stderr=True)  | 
                    |
| 611 | 
                        + result = runner.invoke(driver, [], input="9")  | 
                    |
| 612 | 
                        + assert result.clean_exit(  | 
                    |
| 613 | 
                        + output="""\  | 
                    |
| 614 | 
                        +Our menu:  | 
                    |
| 615 | 
                        +[1] Egg and bacon  | 
                    |
| 616 | 
                        +[2] Egg, sausage and bacon  | 
                    |
| 617 | 
                        +[3] Egg and spam  | 
                    |
| 618 | 
                        +[4] Egg, bacon and spam  | 
                    |
| 619 | 
                        +[5] Egg, bacon, sausage and spam  | 
                    |
| 620 | 
                        +[6] Spam, bacon, sausage and spam  | 
                    |
| 621 | 
                        +[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 622 | 
                        +[8] Spam, spam, spam, egg and spam  | 
                    |
| 623 | 
                        +[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 624 | 
                        +[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 625 | 
                        +Your selection? (1-10, leave empty to abort): 9  | 
                    |
| 626 | 
                        +A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 627 | 
                        +(Note: Vikings strictly optional.)  | 
                    |
| 628 | 
                        +"""  | 
                    |
| 629 | 
                        + ), "expected clean exit"  | 
                    |
| 630 | 
                        + result = runner.invoke(  | 
                    |
| 631 | 
                        + driver, ["--heading="], input="\n", catch_exceptions=True  | 
                    |
| 632 | 
                        + )  | 
                    |
| 633 | 
                        + assert result.error_exit(error=IndexError), (  | 
                    |
| 634 | 
                        + "expected error exit and known error type"  | 
                    |
| 635 | 
                        + )  | 
                    |
| 636 | 
                        + assert (  | 
                    |
| 637 | 
                        + result.stdout  | 
                    |
| 638 | 
                        + == """\  | 
                    |
| 639 | 
                        +[1] Egg and bacon  | 
                    |
| 640 | 
                        +[2] Egg, sausage and bacon  | 
                    |
| 641 | 
                        +[3] Egg and spam  | 
                    |
| 642 | 
                        +[4] Egg, bacon and spam  | 
                    |
| 643 | 
                        +[5] Egg, bacon, sausage and spam  | 
                    |
| 644 | 
                        +[6] Spam, bacon, sausage and spam  | 
                    |
| 645 | 
                        +[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 646 | 
                        +[8] Spam, spam, spam, egg and spam  | 
                    |
| 647 | 
                        +[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 648 | 
                        +[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 649 | 
                        +Your selection? (1-10, leave empty to abort):\x20  | 
                    |
| 650 | 
                        +"""  | 
                    |
| 651 | 
                        + ), "expected known output"  | 
                    |
| 652 | 
                        + # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the  | 
                    |
| 653 | 
                        + # click prompting machinery, meaning that the mixed output will  | 
                    |
| 654 | 
                        + # incorrectly contain a line break, contrary to what the  | 
                    |
| 655 | 
                        + # documentation for click.prompt prescribes.  | 
                    |
| 656 | 
                        + result = runner.invoke(  | 
                    |
| 657 | 
                        + driver, ["--heading="], input="", catch_exceptions=True  | 
                    |
| 658 | 
                        + )  | 
                    |
| 659 | 
                        + assert result.error_exit(error=IndexError), (  | 
                    |
| 660 | 
                        + "expected error exit and known error type"  | 
                    |
| 661 | 
                        + )  | 
                    |
| 662 | 
                        +        assert result.stdout in {
                       | 
                    |
| 663 | 
                        + """\  | 
                    |
| 664 | 
                        +[1] Egg and bacon  | 
                    |
| 665 | 
                        +[2] Egg, sausage and bacon  | 
                    |
| 666 | 
                        +[3] Egg and spam  | 
                    |
| 667 | 
                        +[4] Egg, bacon and spam  | 
                    |
| 668 | 
                        +[5] Egg, bacon, sausage and spam  | 
                    |
| 669 | 
                        +[6] Spam, bacon, sausage and spam  | 
                    |
| 670 | 
                        +[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 671 | 
                        +[8] Spam, spam, spam, egg and spam  | 
                    |
| 672 | 
                        +[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 673 | 
                        +[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 674 | 
                        +Your selection? (1-10, leave empty to abort):\x20  | 
                    |
| 675 | 
                        +""",  | 
                    |
| 676 | 
                        + """\  | 
                    |
| 677 | 
                        +[1] Egg and bacon  | 
                    |
| 678 | 
                        +[2] Egg, sausage and bacon  | 
                    |
| 679 | 
                        +[3] Egg and spam  | 
                    |
| 680 | 
                        +[4] Egg, bacon and spam  | 
                    |
| 681 | 
                        +[5] Egg, bacon, sausage and spam  | 
                    |
| 682 | 
                        +[6] Spam, bacon, sausage and spam  | 
                    |
| 683 | 
                        +[7] Spam, egg, spam, spam, bacon and spam  | 
                    |
| 684 | 
                        +[8] Spam, spam, spam, egg and spam  | 
                    |
| 685 | 
                        +[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam  | 
                    |
| 686 | 
                        +[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam  | 
                    |
| 687 | 
                        +Your selection? (1-10, leave empty to abort): """,  | 
                    |
| 688 | 
                        + }, "expected known output"  | 
                    |
| 689 | 
                        +  | 
                    |
| 690 | 
                        + def test_112_prompt_for_selection_single(self) -> None:  | 
                    |
| 691 | 
                        + """[`cli_helpers.prompt_for_selection`][] works in the "single" case."""  | 
                    |
| 692 | 
                        +  | 
                    |
| 693 | 
                        + @click.command()  | 
                    |
| 694 | 
                        +        @click.option("--item", default="baked beans")
                       | 
                    |
| 695 | 
                        +        @click.argument("prompt")
                       | 
                    |
| 696 | 
                        + def driver(item: str, prompt: str) -> None:  | 
                    |
| 697 | 
                        + try:  | 
                    |
| 698 | 
                        + cli_helpers.prompt_for_selection(  | 
                    |
| 699 | 
                        + [item], heading="", single_choice_prompt=prompt  | 
                    |
| 700 | 
                        + )  | 
                    |
| 701 | 
                        + except IndexError:  | 
                    |
| 702 | 
                        +                click.echo("Boo.")
                       | 
                    |
| 703 | 
                        + raise  | 
                    |
| 704 | 
                        + else:  | 
                    |
| 705 | 
                        +                click.echo("Great!")
                       | 
                    |
| 706 | 
                        +  | 
                    |
| 707 | 
                        + runner = machinery.CliRunner(mix_stderr=True)  | 
                    |
| 708 | 
                        + result = runner.invoke(  | 
                    |
| 709 | 
                        + driver, ["Will replace with spam. Confirm, y/n?"], input="y"  | 
                    |
| 710 | 
                        + )  | 
                    |
| 711 | 
                        + assert result.clean_exit(  | 
                    |
| 712 | 
                        + output="""\  | 
                    |
| 713 | 
                        +[1] baked beans  | 
                    |
| 714 | 
                        +Will replace with spam. Confirm, y/n? y  | 
                    |
| 715 | 
                        +Great!  | 
                    |
| 716 | 
                        +"""  | 
                    |
| 717 | 
                        + ), "expected clean exit"  | 
                    |
| 718 | 
                        + result = runner.invoke(  | 
                    |
| 719 | 
                        + driver,  | 
                    |
| 720 | 
                        + ['Will replace with spam, okay? (Please say "y" or "n".)'],  | 
                    |
| 721 | 
                        + input="\n",  | 
                    |
| 722 | 
                        + )  | 
                    |
| 723 | 
                        + assert result.error_exit(error=IndexError), (  | 
                    |
| 724 | 
                        + "expected error exit and known error type"  | 
                    |
| 725 | 
                        + )  | 
                    |
| 726 | 
                        + assert (  | 
                    |
| 727 | 
                        + result.stdout  | 
                    |
| 728 | 
                        + == """\  | 
                    |
| 729 | 
                        +[1] baked beans  | 
                    |
| 730 | 
                        +Will replace with spam, okay? (Please say "y" or "n".):\x20  | 
                    |
| 731 | 
                        +Boo.  | 
                    |
| 732 | 
                        +"""  | 
                    |
| 733 | 
                        + ), "expected known output"  | 
                    |
| 734 | 
                        + # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the  | 
                    |
| 735 | 
                        + # click prompting machinery, meaning that the mixed output will  | 
                    |
| 736 | 
                        + # incorrectly contain a line break, contrary to what the  | 
                    |
| 737 | 
                        + # documentation for click.prompt prescribes.  | 
                    |
| 738 | 
                        + result = runner.invoke(  | 
                    |
| 739 | 
                        + driver,  | 
                    |
| 740 | 
                        + ['Will replace with spam, okay? (Please say "y" or "n".)'],  | 
                    |
| 741 | 
                        + input="",  | 
                    |
| 742 | 
                        + )  | 
                    |
| 743 | 
                        + assert result.error_exit(error=IndexError), (  | 
                    |
| 744 | 
                        + "expected error exit and known error type"  | 
                    |
| 745 | 
                        + )  | 
                    |
| 746 | 
                        +        assert result.stdout in {
                       | 
                    |
| 747 | 
                        + """\  | 
                    |
| 748 | 
                        +[1] baked beans  | 
                    |
| 749 | 
                        +Will replace with spam, okay? (Please say "y" or "n".):\x20  | 
                    |
| 750 | 
                        +Boo.  | 
                    |
| 751 | 
                        +""",  | 
                    |
| 752 | 
                        + """\  | 
                    |
| 753 | 
                        +[1] baked beans  | 
                    |
| 754 | 
                        +Will replace with spam, okay? (Please say "y" or "n".): Boo.  | 
                    |
| 755 | 
                        +""",  | 
                    |
| 756 | 
                        + }, "expected known output"  | 
                    |
| 757 | 
                        +  | 
                    |
| 758 | 
                        + def test_113_prompt_for_passphrase(  | 
                    |
| 759 | 
                        + self,  | 
                    |
| 760 | 
                        + ) -> None:  | 
                    |
| 761 | 
                        + """[`cli_helpers.prompt_for_passphrase`][] works."""  | 
                    |
| 762 | 
                        + with pytest.MonkeyPatch.context() as monkeypatch:  | 
                    |
| 763 | 
                        + monkeypatch.setattr(  | 
                    |
| 764 | 
                        + click,  | 
                    |
| 765 | 
                        + "prompt",  | 
                    |
| 766 | 
                        +                lambda *a, **kw: json.dumps({"args": a, "kwargs": kw}),
                       | 
                    |
| 767 | 
                        + )  | 
                    |
| 768 | 
                        + res = json.loads(cli_helpers.prompt_for_passphrase())  | 
                    |
| 769 | 
                        + err_msg = "missing arguments to passphrase prompt"  | 
                    |
| 770 | 
                        + assert "args" in res, err_msg  | 
                    |
| 771 | 
                        + assert "kwargs" in res, err_msg  | 
                    |
| 772 | 
                        + assert res["args"][:1] == ["Passphrase"], err_msg  | 
                    |
| 773 | 
                        +        assert res["kwargs"].get("default") == "", err_msg
                       | 
                    |
| 774 | 
                        +        assert not res["kwargs"].get("show_default", True), err_msg
                       | 
                    |
| 775 | 
                        +        assert res["kwargs"].get("err"), err_msg
                       | 
                    |
| 776 | 
                        +        assert res["kwargs"].get("hide_input"), err_msg
                       | 
                    |
| 777 | 
                        +  | 
                    |
| 778 | 
                        + def test_120_standard_logging_context_manager(  | 
                    |
| 779 | 
                        + self,  | 
                    |
| 780 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 781 | 
                        + capsys: pytest.CaptureFixture[str],  | 
                    |
| 782 | 
                        + ) -> None:  | 
                    |
| 783 | 
                        + """The standard logging context manager works.  | 
                    |
| 784 | 
                        +  | 
                    |
| 785 | 
                        + It registers its handlers, once, and emits formatted calls to  | 
                    |
| 786 | 
                        + standard error prefixed with the program name.  | 
                    |
| 787 | 
                        +  | 
                    |
| 788 | 
                        + """  | 
                    |
| 789 | 
                        + prog_name = cli_machinery.StandardCLILogging.prog_name  | 
                    |
| 790 | 
                        + package_name = cli_machinery.StandardCLILogging.package_name  | 
                    |
| 791 | 
                        + logger = logging.getLogger(package_name)  | 
                    |
| 792 | 
                        +        deprecation_logger = logging.getLogger(f"{package_name}.deprecation")
                       | 
                    |
| 793 | 
                        + logging_cm = cli_machinery.StandardCLILogging.ensure_standard_logging()  | 
                    |
| 794 | 
                        + with logging_cm:  | 
                    |
| 795 | 
                        + assert (  | 
                    |
| 796 | 
                        + sum(  | 
                    |
| 797 | 
                        + 1  | 
                    |
| 798 | 
                        + for h in logger.handlers  | 
                    |
| 799 | 
                        + if h is cli_machinery.StandardCLILogging.cli_handler  | 
                    |
| 800 | 
                        + )  | 
                    |
| 801 | 
                        + == 1  | 
                    |
| 802 | 
                        + )  | 
                    |
| 803 | 
                        +            logger.warning("message 1")
                       | 
                    |
| 804 | 
                        + with logging_cm:  | 
                    |
| 805 | 
                        +                deprecation_logger.warning("message 2")
                       | 
                    |
| 806 | 
                        + assert (  | 
                    |
| 807 | 
                        + sum(  | 
                    |
| 808 | 
                        + 1  | 
                    |
| 809 | 
                        + for h in logger.handlers  | 
                    |
| 810 | 
                        + if h is cli_machinery.StandardCLILogging.cli_handler  | 
                    |
| 811 | 
                        + )  | 
                    |
| 812 | 
                        + == 1  | 
                    |
| 813 | 
                        + )  | 
                    |
| 814 | 
                        + assert capsys.readouterr() == (  | 
                    |
| 815 | 
                        + "",  | 
                    |
| 816 | 
                        + (  | 
                    |
| 817 | 
                        +                        f"{prog_name}: Warning: message 1\n"
                       | 
                    |
| 818 | 
                        +                        f"{prog_name}: Deprecation warning: message 2\n"
                       | 
                    |
| 819 | 
                        + ),  | 
                    |
| 820 | 
                        + )  | 
                    |
| 821 | 
                        +            logger.warning("message 3")
                       | 
                    |
| 822 | 
                        + assert (  | 
                    |
| 823 | 
                        + sum(  | 
                    |
| 824 | 
                        + 1  | 
                    |
| 825 | 
                        + for h in logger.handlers  | 
                    |
| 826 | 
                        + if h is cli_machinery.StandardCLILogging.cli_handler  | 
                    |
| 827 | 
                        + )  | 
                    |
| 828 | 
                        + == 1  | 
                    |
| 829 | 
                        + )  | 
                    |
| 830 | 
                        + assert capsys.readouterr() == (  | 
                    |
| 831 | 
                        + "",  | 
                    |
| 832 | 
                        +                f"{prog_name}: Warning: message 3\n",
                       | 
                    |
| 833 | 
                        + )  | 
                    |
| 834 | 
                        + assert caplog.record_tuples == [  | 
                    |
| 835 | 
                        + (package_name, logging.WARNING, "message 1"),  | 
                    |
| 836 | 
                        +                (f"{package_name}.deprecation", logging.WARNING, "message 2"),
                       | 
                    |
| 837 | 
                        + (package_name, logging.WARNING, "message 3"),  | 
                    |
| 838 | 
                        + ]  | 
                    |
| 839 | 
                        +  | 
                    |
| 840 | 
                        + def test_121_standard_logging_warnings_context_manager(  | 
                    |
| 841 | 
                        + self,  | 
                    |
| 842 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 843 | 
                        + capsys: pytest.CaptureFixture[str],  | 
                    |
| 844 | 
                        + ) -> None:  | 
                    |
| 845 | 
                        + """The standard warnings logging context manager works.  | 
                    |
| 846 | 
                        +  | 
                    |
| 847 | 
                        + It registers its handlers, once, and emits formatted calls to  | 
                    |
| 848 | 
                        + standard error prefixed with the program name. It also adheres  | 
                    |
| 849 | 
                        + to the global warnings filter concerning which messages it  | 
                    |
| 850 | 
                        + actually emits to standard error.  | 
                    |
| 851 | 
                        +  | 
                    |
| 852 | 
                        + """  | 
                    |
| 853 | 
                        + warnings_cm = (  | 
                    |
| 854 | 
                        + cli_machinery.StandardCLILogging.ensure_standard_warnings_logging()  | 
                    |
| 855 | 
                        + )  | 
                    |
| 856 | 
                        + THE_FUTURE = "the future will be here sooner than you think" # noqa: N806  | 
                    |
| 857 | 
                        + JUST_TESTING = "just testing whether warnings work" # noqa: N806  | 
                    |
| 858 | 
                        + with warnings_cm:  | 
                    |
| 859 | 
                        + assert (  | 
                    |
| 860 | 
                        + sum(  | 
                    |
| 861 | 
                        + 1  | 
                    |
| 862 | 
                        +                    for h in logging.getLogger("py.warnings").handlers
                       | 
                    |
| 863 | 
                        + if h is cli_machinery.StandardCLILogging.warnings_handler  | 
                    |
| 864 | 
                        + )  | 
                    |
| 865 | 
                        + == 1  | 
                    |
| 866 | 
                        + )  | 
                    |
| 867 | 
                        + warnings.warn(UserWarning(JUST_TESTING), stacklevel=1)  | 
                    |
| 868 | 
                        + with warnings_cm:  | 
                    |
| 869 | 
                        + warnings.warn(FutureWarning(THE_FUTURE), stacklevel=1)  | 
                    |
| 870 | 
                        + _out, err = capsys.readouterr()  | 
                    |
| 871 | 
                        + err_lines = err.splitlines(True)  | 
                    |
| 872 | 
                        + assert any(  | 
                    |
| 873 | 
                        +                    f"UserWarning: {JUST_TESTING}" in line
                       | 
                    |
| 874 | 
                        + for line in err_lines  | 
                    |
| 875 | 
                        + )  | 
                    |
| 876 | 
                        + assert any(  | 
                    |
| 877 | 
                        +                    f"FutureWarning: {THE_FUTURE}" in line
                       | 
                    |
| 878 | 
                        + for line in err_lines  | 
                    |
| 879 | 
                        + )  | 
                    |
| 880 | 
                        + warnings.warn(UserWarning(JUST_TESTING), stacklevel=1)  | 
                    |
| 881 | 
                        + _out, err = capsys.readouterr()  | 
                    |
| 882 | 
                        + err_lines = err.splitlines(True)  | 
                    |
| 883 | 
                        + assert any(  | 
                    |
| 884 | 
                        +                f"UserWarning: {JUST_TESTING}" in line for line in err_lines
                       | 
                    |
| 885 | 
                        + )  | 
                    |
| 886 | 
                        + assert not any(  | 
                    |
| 887 | 
                        +                f"FutureWarning: {THE_FUTURE}" in line for line in err_lines
                       | 
                    |
| 888 | 
                        + )  | 
                    |
| 889 | 
                        + record_tuples = caplog.record_tuples  | 
                    |
| 890 | 
                        + assert [tup[:2] for tup in record_tuples] == [  | 
                    |
| 891 | 
                        +                ("py.warnings", logging.WARNING),
                       | 
                    |
| 892 | 
                        +                ("py.warnings", logging.WARNING),
                       | 
                    |
| 893 | 
                        +                ("py.warnings", logging.WARNING),
                       | 
                    |
| 894 | 
                        + ]  | 
                    |
| 895 | 
                        +            assert f"UserWarning: {JUST_TESTING}" in record_tuples[0][2]
                       | 
                    |
| 896 | 
                        +            assert f"FutureWarning: {THE_FUTURE}" in record_tuples[1][2]
                       | 
                    |
| 897 | 
                        +            assert f"UserWarning: {JUST_TESTING}" in record_tuples[2][2]
                       | 
                    |
| 898 | 
                        +  | 
                    |
| 899 | 
                        + def export_as_sh_helper(  | 
                    |
| 900 | 
                        + self,  | 
                    |
| 901 | 
                        + config: Any,  | 
                    |
| 902 | 
                        + ) -> None:  | 
                    |
| 903 | 
                        + """Emits a config in sh(1) format, then reads it back to verify it.  | 
                    |
| 904 | 
                        +  | 
                    |
| 905 | 
                        + This function exports the configuration, sets up a new  | 
                    |
| 906 | 
                        + enviroment, then calls  | 
                    |
| 907 | 
                        + [`vault_config_exporter_shell_interpreter`][] on the export  | 
                    |
| 908 | 
                        + script, verifying that each command ran successfully and that  | 
                    |
| 909 | 
                        + the final configuration matches the initial one.  | 
                    |
| 910 | 
                        +  | 
                    |
| 911 | 
                        + Args:  | 
                    |
| 912 | 
                        + config:  | 
                    |
| 913 | 
                        + The configuration to emit and read back.  | 
                    |
| 914 | 
                        +  | 
                    |
| 915 | 
                        + """  | 
                    |
| 916 | 
                        +        prog_name_list = ("derivepassphrase", "vault")
                       | 
                    |
| 917 | 
                        + with io.StringIO() as outfile:  | 
                    |
| 918 | 
                        + cli_helpers.print_config_as_sh_script(  | 
                    |
| 919 | 
                        + config, outfile=outfile, prog_name_list=prog_name_list  | 
                    |
| 920 | 
                        + )  | 
                    |
| 921 | 
                        + script = outfile.getvalue()  | 
                    |
| 922 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 923 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 924 | 
                        + # with-statements.  | 
                    |
| 925 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 926 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 927 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 928 | 
                        + stack.enter_context(  | 
                    |
| 929 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 930 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 931 | 
                        + runner=runner,  | 
                    |
| 932 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 933 | 
                        + )  | 
                    |
| 934 | 
                        + )  | 
                    |
| 935 | 
                        + for result in vault_config_exporter_shell_interpreter(script):  | 
                    |
| 936 | 
                        + assert result.clean_exit()  | 
                    |
| 937 | 
                        + assert cli_helpers.load_config() == config  | 
                    |
| 938 | 
                        +  | 
                    |
| 939 | 
                        + @hypothesis.given(  | 
                    |
| 940 | 
                        + global_config_settable=hypothesis_machinery.vault_full_service_config(),  | 
                    |
| 941 | 
                        + global_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 942 | 
                        +            {},
                       | 
                    |
| 943 | 
                        +            optional={
                       | 
                    |
| 944 | 
                        + "key": strategies.text(  | 
                    |
| 945 | 
                        + alphabet=strategies.characters(  | 
                    |
| 946 | 
                        + min_codepoint=32,  | 
                    |
| 947 | 
                        + max_codepoint=126,  | 
                    |
| 948 | 
                        + ),  | 
                    |
| 949 | 
                        + max_size=128,  | 
                    |
| 950 | 
                        + ),  | 
                    |
| 951 | 
                        + "phrase": strategies.text(  | 
                    |
| 952 | 
                        + alphabet=strategies.characters(  | 
                    |
| 953 | 
                        + min_codepoint=32,  | 
                    |
| 954 | 
                        + max_codepoint=126,  | 
                    |
| 955 | 
                        + ),  | 
                    |
| 956 | 
                        + max_size=64,  | 
                    |
| 957 | 
                        + ),  | 
                    |
| 958 | 
                        + },  | 
                    |
| 959 | 
                        + ),  | 
                    |
| 960 | 
                        + )  | 
                    |
| 961 | 
                        + def test_130a_export_as_sh_global(  | 
                    |
| 962 | 
                        + self,  | 
                    |
| 963 | 
                        + global_config_settable: _types.VaultConfigServicesSettings,  | 
                    |
| 964 | 
                        + global_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 965 | 
                        + ) -> None:  | 
                    |
| 966 | 
                        + """Exporting configurations as sh(1) script works.  | 
                    |
| 967 | 
                        +  | 
                    |
| 968 | 
                        + Here, we check global-only configurations which use both  | 
                    |
| 969 | 
                        + settings settable via `--config` and settings requiring  | 
                    |
| 970 | 
                        + `--import`.  | 
                    |
| 971 | 
                        +  | 
                    |
| 972 | 
                        + The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 973 | 
                        +  | 
                    |
| 974 | 
                        + """  | 
                    |
| 975 | 
                        +        config: _types.VaultConfig = {
                       | 
                    |
| 976 | 
                        + "global": global_config_settable | global_config_importable,  | 
                    |
| 977 | 
                        +            "services": {},
                       | 
                    |
| 978 | 
                        + }  | 
                    |
| 979 | 
                        + assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 980 | 
                        + assert _types.is_vault_config(config)  | 
                    |
| 981 | 
                        + return self.export_as_sh_helper(config)  | 
                    |
| 982 | 
                        +  | 
                    |
| 983 | 
                        + @hypothesis.given(  | 
                    |
| 984 | 
                        + global_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 985 | 
                        +            {},
                       | 
                    |
| 986 | 
                        +            optional={
                       | 
                    |
| 987 | 
                        + "key": strategies.text(  | 
                    |
| 988 | 
                        + alphabet=strategies.characters(  | 
                    |
| 989 | 
                        + min_codepoint=32,  | 
                    |
| 990 | 
                        + max_codepoint=126,  | 
                    |
| 991 | 
                        + ),  | 
                    |
| 992 | 
                        + max_size=128,  | 
                    |
| 993 | 
                        + ),  | 
                    |
| 994 | 
                        + "phrase": strategies.text(  | 
                    |
| 995 | 
                        + alphabet=strategies.characters(  | 
                    |
| 996 | 
                        + min_codepoint=32,  | 
                    |
| 997 | 
                        + max_codepoint=126,  | 
                    |
| 998 | 
                        + ),  | 
                    |
| 999 | 
                        + max_size=64,  | 
                    |
| 1000 | 
                        + ),  | 
                    |
| 1001 | 
                        + },  | 
                    |
| 1002 | 
                        + ),  | 
                    |
| 1003 | 
                        + )  | 
                    |
| 1004 | 
                        + def test_130b_export_as_sh_global_only_imports(  | 
                    |
| 1005 | 
                        + self,  | 
                    |
| 1006 | 
                        + global_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 1007 | 
                        + ) -> None:  | 
                    |
| 1008 | 
                        + """Exporting configurations as sh(1) script works.  | 
                    |
| 1009 | 
                        +  | 
                    |
| 1010 | 
                        + Here, we check global-only configurations which only use  | 
                    |
| 1011 | 
                        + settings requiring `--import`.  | 
                    |
| 1012 | 
                        +  | 
                    |
| 1013 | 
                        + The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 1014 | 
                        +  | 
                    |
| 1015 | 
                        + """  | 
                    |
| 1016 | 
                        +        config: _types.VaultConfig = {
                       | 
                    |
| 1017 | 
                        + "global": global_config_importable,  | 
                    |
| 1018 | 
                        +            "services": {},
                       | 
                    |
| 1019 | 
                        + }  | 
                    |
| 1020 | 
                        + assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 1021 | 
                        + assert _types.is_vault_config(config)  | 
                    |
| 1022 | 
                        + if not config["global"]:  | 
                    |
| 1023 | 
                        +            config.pop("global")
                       | 
                    |
| 1024 | 
                        + return self.export_as_sh_helper(config)  | 
                    |
| 1025 | 
                        +  | 
                    |
| 1026 | 
                        + @hypothesis.given(  | 
                    |
| 1027 | 
                        + service_name=strategies.text(  | 
                    |
| 1028 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1029 | 
                        + min_codepoint=32,  | 
                    |
| 1030 | 
                        + max_codepoint=126,  | 
                    |
| 1031 | 
                        + ),  | 
                    |
| 1032 | 
                        + min_size=4,  | 
                    |
| 1033 | 
                        + max_size=64,  | 
                    |
| 1034 | 
                        + ),  | 
                    |
| 1035 | 
                        + service_config_settable=hypothesis_machinery.vault_full_service_config(),  | 
                    |
| 1036 | 
                        + service_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 1037 | 
                        +            {},
                       | 
                    |
| 1038 | 
                        +            optional={
                       | 
                    |
| 1039 | 
                        + "key": strategies.text(  | 
                    |
| 1040 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1041 | 
                        + min_codepoint=32,  | 
                    |
| 1042 | 
                        + max_codepoint=126,  | 
                    |
| 1043 | 
                        + ),  | 
                    |
| 1044 | 
                        + max_size=128,  | 
                    |
| 1045 | 
                        + ),  | 
                    |
| 1046 | 
                        + "phrase": strategies.text(  | 
                    |
| 1047 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1048 | 
                        + min_codepoint=32,  | 
                    |
| 1049 | 
                        + max_codepoint=126,  | 
                    |
| 1050 | 
                        + ),  | 
                    |
| 1051 | 
                        + max_size=64,  | 
                    |
| 1052 | 
                        + ),  | 
                    |
| 1053 | 
                        + "notes": strategies.text(  | 
                    |
| 1054 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1055 | 
                        + min_codepoint=32,  | 
                    |
| 1056 | 
                        + max_codepoint=126,  | 
                    |
| 1057 | 
                        +                        include_characters=("\n", "\f", "\t"),
                       | 
                    |
| 1058 | 
                        + ),  | 
                    |
| 1059 | 
                        + max_size=256,  | 
                    |
| 1060 | 
                        + ),  | 
                    |
| 1061 | 
                        + },  | 
                    |
| 1062 | 
                        + ),  | 
                    |
| 1063 | 
                        + )  | 
                    |
| 1064 | 
                        + def test_130c_export_as_sh_service(  | 
                    |
| 1065 | 
                        + self,  | 
                    |
| 1066 | 
                        + service_name: str,  | 
                    |
| 1067 | 
                        + service_config_settable: _types.VaultConfigServicesSettings,  | 
                    |
| 1068 | 
                        + service_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 1069 | 
                        + ) -> None:  | 
                    |
| 1070 | 
                        + """Exporting configurations as sh(1) script works.  | 
                    |
| 1071 | 
                        +  | 
                    |
| 1072 | 
                        + Here, we check service-only configurations which use both  | 
                    |
| 1073 | 
                        + settings settable via `--config` and settings requiring  | 
                    |
| 1074 | 
                        + `--import`.  | 
                    |
| 1075 | 
                        +  | 
                    |
| 1076 | 
                        + The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 1077 | 
                        +  | 
                    |
| 1078 | 
                        + """  | 
                    |
| 1079 | 
                        +        config: _types.VaultConfig = {
                       | 
                    |
| 1080 | 
                        +            "services": {
                       | 
                    |
| 1081 | 
                        + service_name: (  | 
                    |
| 1082 | 
                        + service_config_settable | service_config_importable  | 
                    |
| 1083 | 
                        + ),  | 
                    |
| 1084 | 
                        + },  | 
                    |
| 1085 | 
                        + }  | 
                    |
| 1086 | 
                        + assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 1087 | 
                        + assert _types.is_vault_config(config)  | 
                    |
| 1088 | 
                        + return self.export_as_sh_helper(config)  | 
                    |
| 1089 | 
                        +  | 
                    |
| 1090 | 
                        + @hypothesis.given(  | 
                    |
| 1091 | 
                        + service_name=strategies.text(  | 
                    |
| 1092 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1093 | 
                        + min_codepoint=32,  | 
                    |
| 1094 | 
                        + max_codepoint=126,  | 
                    |
| 1095 | 
                        + ),  | 
                    |
| 1096 | 
                        + min_size=4,  | 
                    |
| 1097 | 
                        + max_size=64,  | 
                    |
| 1098 | 
                        + ),  | 
                    |
| 1099 | 
                        + service_config_importable=strategies.fixed_dictionaries(  | 
                    |
| 1100 | 
                        +            {},
                       | 
                    |
| 1101 | 
                        +            optional={
                       | 
                    |
| 1102 | 
                        + "key": strategies.text(  | 
                    |
| 1103 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1104 | 
                        + min_codepoint=32,  | 
                    |
| 1105 | 
                        + max_codepoint=126,  | 
                    |
| 1106 | 
                        + ),  | 
                    |
| 1107 | 
                        + max_size=128,  | 
                    |
| 1108 | 
                        + ),  | 
                    |
| 1109 | 
                        + "phrase": strategies.text(  | 
                    |
| 1110 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1111 | 
                        + min_codepoint=32,  | 
                    |
| 1112 | 
                        + max_codepoint=126,  | 
                    |
| 1113 | 
                        + ),  | 
                    |
| 1114 | 
                        + max_size=64,  | 
                    |
| 1115 | 
                        + ),  | 
                    |
| 1116 | 
                        + "notes": strategies.text(  | 
                    |
| 1117 | 
                        + alphabet=strategies.characters(  | 
                    |
| 1118 | 
                        + min_codepoint=32,  | 
                    |
| 1119 | 
                        + max_codepoint=126,  | 
                    |
| 1120 | 
                        +                        include_characters=("\n", "\f", "\t"),
                       | 
                    |
| 1121 | 
                        + ),  | 
                    |
| 1122 | 
                        + max_size=256,  | 
                    |
| 1123 | 
                        + ),  | 
                    |
| 1124 | 
                        + },  | 
                    |
| 1125 | 
                        + ),  | 
                    |
| 1126 | 
                        + )  | 
                    |
| 1127 | 
                        + def test_130d_export_as_sh_service_only_imports(  | 
                    |
| 1128 | 
                        + self,  | 
                    |
| 1129 | 
                        + service_name: str,  | 
                    |
| 1130 | 
                        + service_config_importable: _types.VaultConfigServicesSettings,  | 
                    |
| 1131 | 
                        + ) -> None:  | 
                    |
| 1132 | 
                        + """Exporting configurations as sh(1) script works.  | 
                    |
| 1133 | 
                        +  | 
                    |
| 1134 | 
                        + Here, we check service-only configurations which only use  | 
                    |
| 1135 | 
                        + settings requiring `--import`.  | 
                    |
| 1136 | 
                        +  | 
                    |
| 1137 | 
                        + The actual verification is done by [`export_as_sh_helper`][].  | 
                    |
| 1138 | 
                        +  | 
                    |
| 1139 | 
                        + """  | 
                    |
| 1140 | 
                        +        config: _types.VaultConfig = {
                       | 
                    |
| 1141 | 
                        +            "services": {
                       | 
                    |
| 1142 | 
                        + service_name: service_config_importable,  | 
                    |
| 1143 | 
                        + },  | 
                    |
| 1144 | 
                        + }  | 
                    |
| 1145 | 
                        + assert _types.clean_up_falsy_vault_config_values(config) is not None  | 
                    |
| 1146 | 
                        + assert _types.is_vault_config(config)  | 
                    |
| 1147 | 
                        + return self.export_as_sh_helper(config)  | 
                    |
| 1148 | 
                        +  | 
                    |
| 1149 | 
                        + # The Annoying OS appears to silently truncate spaces at the end of  | 
                    |
| 1150 | 
                        + # filenames.  | 
                    |
| 1151 | 
                        + @hypothesis.given(  | 
                    |
| 1152 | 
                        + env_var=strategies.sampled_from(["TMPDIR", "TEMP", "TMP"]),  | 
                    |
| 1153 | 
                        + suffix=strategies.builds(  | 
                    |
| 1154 | 
                        + operator.add,  | 
                    |
| 1155 | 
                        + strategies.text(  | 
                    |
| 1156 | 
                        +                tuple(" 0123456789abcdefghijklmnopqrstuvwxyz"),
                       | 
                    |
| 1157 | 
                        + min_size=11,  | 
                    |
| 1158 | 
                        + max_size=11,  | 
                    |
| 1159 | 
                        + ),  | 
                    |
| 1160 | 
                        + strategies.text(  | 
                    |
| 1161 | 
                        +                tuple("0123456789abcdefghijklmnopqrstuvwxyz"),
                       | 
                    |
| 1162 | 
                        + min_size=1,  | 
                    |
| 1163 | 
                        + max_size=1,  | 
                    |
| 1164 | 
                        + ),  | 
                    |
| 1165 | 
                        + ),  | 
                    |
| 1166 | 
                        + )  | 
                    |
| 1167 | 
                        + @hypothesis.example(env_var="", suffix=".")  | 
                    |
| 1168 | 
                        + def test_140a_get_tempdir(  | 
                    |
| 1169 | 
                        + self,  | 
                    |
| 1170 | 
                        + env_var: str,  | 
                    |
| 1171 | 
                        + suffix: str,  | 
                    |
| 1172 | 
                        + ) -> None:  | 
                    |
| 1173 | 
                        + """[`cli_helpers.get_tempdir`][] returns a temporary directory.  | 
                    |
| 1174 | 
                        +  | 
                    |
| 1175 | 
                        + If it is not the same as the temporary directory determined by  | 
                    |
| 1176 | 
                        + [`tempfile.gettempdir`][], then assert that  | 
                    |
| 1177 | 
                        + `tempfile.gettempdir` returned the current directory and  | 
                    |
| 1178 | 
                        + `cli_helpers.get_tempdir` returned the configuration directory.  | 
                    |
| 1179 | 
                        +  | 
                    |
| 1180 | 
                        + """  | 
                    |
| 1181 | 
                        +  | 
                    |
| 1182 | 
                        + @contextlib.contextmanager  | 
                    |
| 1183 | 
                        + def make_temporary_directory(  | 
                    |
| 1184 | 
                        + path: pathlib.Path,  | 
                    |
| 1185 | 
                        + ) -> Iterator[pathlib.Path]:  | 
                    |
| 1186 | 
                        + try:  | 
                    |
| 1187 | 
                        + path.mkdir()  | 
                    |
| 1188 | 
                        + yield path  | 
                    |
| 1189 | 
                        + finally:  | 
                    |
| 1190 | 
                        + shutil.rmtree(path)  | 
                    |
| 1191 | 
                        +  | 
                    |
| 1192 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 1193 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 1194 | 
                        + # with-statements.  | 
                    |
| 1195 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 1196 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 1197 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 1198 | 
                        + stack.enter_context(  | 
                    |
| 1199 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 1200 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 1201 | 
                        + runner=runner,  | 
                    |
| 1202 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 1203 | 
                        + )  | 
                    |
| 1204 | 
                        + )  | 
                    |
| 1205 | 
                        + old_tempdir = os.fsdecode(tempfile.gettempdir())  | 
                    |
| 1206 | 
                        +            monkeypatch.delenv("TMPDIR", raising=False)
                       | 
                    |
| 1207 | 
                        +            monkeypatch.delenv("TEMP", raising=False)
                       | 
                    |
| 1208 | 
                        +            monkeypatch.delenv("TMP", raising=False)
                       | 
                    |
| 1209 | 
                        + monkeypatch.setattr(tempfile, "tempdir", None)  | 
                    |
| 1210 | 
                        + temp_path = pathlib.Path.cwd() / suffix  | 
                    |
| 1211 | 
                        + if env_var:  | 
                    |
| 1212 | 
                        + monkeypatch.setenv(env_var, os.fsdecode(temp_path))  | 
                    |
| 1213 | 
                        + stack.enter_context(make_temporary_directory(temp_path))  | 
                    |
| 1214 | 
                        + new_tempdir = os.fsdecode(tempfile.gettempdir())  | 
                    |
| 1215 | 
                        + hypothesis.assume(  | 
                    |
| 1216 | 
                        + temp_path.resolve() == pathlib.Path.cwd().resolve()  | 
                    |
| 1217 | 
                        + or old_tempdir != new_tempdir  | 
                    |
| 1218 | 
                        + )  | 
                    |
| 1219 | 
                        + system_tempdir = os.fsdecode(tempfile.gettempdir())  | 
                    |
| 1220 | 
                        + our_tempdir = cli_helpers.get_tempdir()  | 
                    |
| 1221 | 
                        + assert system_tempdir == os.fsdecode(our_tempdir) or (  | 
                    |
| 1222 | 
                        + # TODO(the-13th-letter): `pytest_machinery.isolated_config`  | 
                    |
| 1223 | 
                        + # guarantees that `Path.cwd() == config_filename(None)`.  | 
                    |
| 1224 | 
                        + # So this sub-branch ought to never trigger in our  | 
                    |
| 1225 | 
                        + # tests.  | 
                    |
| 1226 | 
                        + system_tempdir == os.getcwd() # noqa: PTH109  | 
                    |
| 1227 | 
                        + and our_tempdir == cli_helpers.config_filename(subsystem=None)  | 
                    |
| 1228 | 
                        + )  | 
                    |
| 1229 | 
                        +        assert not temp_path.exists(), f"temp path {temp_path} not cleaned up!"
                       | 
                    |
| 1230 | 
                        +  | 
                    |
| 1231 | 
                        + def test_140b_get_tempdir_force_default(self) -> None:  | 
                    |
| 1232 | 
                        + """[`cli_helpers.get_tempdir`][] returns a temporary directory.  | 
                    |
| 1233 | 
                        +  | 
                    |
| 1234 | 
                        + If all candidates are mocked to fail for the standard temporary  | 
                    |
| 1235 | 
                        + directory choices, then we return the `derivepassphrase`  | 
                    |
| 1236 | 
                        + configuration directory.  | 
                    |
| 1237 | 
                        +  | 
                    |
| 1238 | 
                        + """  | 
                    |
| 1239 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 1240 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 1241 | 
                        + # with-statements.  | 
                    |
| 1242 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 1243 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 1244 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 1245 | 
                        + stack.enter_context(  | 
                    |
| 1246 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 1247 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 1248 | 
                        + runner=runner,  | 
                    |
| 1249 | 
                        +                    vault_config={"services": {}},
                       | 
                    |
| 1250 | 
                        + )  | 
                    |
| 1251 | 
                        + )  | 
                    |
| 1252 | 
                        +            monkeypatch.delenv("TMPDIR", raising=False)
                       | 
                    |
| 1253 | 
                        +            monkeypatch.delenv("TEMP", raising=False)
                       | 
                    |
| 1254 | 
                        +            monkeypatch.delenv("TMP", raising=False)
                       | 
                    |
| 1255 | 
                        + config_dir = cli_helpers.config_filename(subsystem=None)  | 
                    |
| 1256 | 
                        +  | 
                    |
| 1257 | 
                        + def is_dir_false(  | 
                    |
| 1258 | 
                        + self: pathlib.Path,  | 
                    |
| 1259 | 
                        + /,  | 
                    |
| 1260 | 
                        + *,  | 
                    |
| 1261 | 
                        + follow_symlinks: bool = False,  | 
                    |
| 1262 | 
                        + ) -> bool:  | 
                    |
| 1263 | 
                        + del self, follow_symlinks  | 
                    |
| 1264 | 
                        + return False  | 
                    |
| 1265 | 
                        +  | 
                    |
| 1266 | 
                        + def is_dir_error(  | 
                    |
| 1267 | 
                        + self: pathlib.Path,  | 
                    |
| 1268 | 
                        + /,  | 
                    |
| 1269 | 
                        + *,  | 
                    |
| 1270 | 
                        + follow_symlinks: bool = False,  | 
                    |
| 1271 | 
                        + ) -> bool:  | 
                    |
| 1272 | 
                        + del follow_symlinks  | 
                    |
| 1273 | 
                        + raise OSError(  | 
                    |
| 1274 | 
                        + errno.EACCES,  | 
                    |
| 1275 | 
                        + os.strerror(errno.EACCES),  | 
                    |
| 1276 | 
                        + str(self),  | 
                    |
| 1277 | 
                        + )  | 
                    |
| 1278 | 
                        +  | 
                    |
| 1279 | 
                        + monkeypatch.setattr(pathlib.Path, "is_dir", is_dir_false)  | 
                    |
| 1280 | 
                        + assert cli_helpers.get_tempdir() == config_dir  | 
                    |
| 1281 | 
                        +  | 
                    |
| 1282 | 
                        + monkeypatch.setattr(pathlib.Path, "is_dir", is_dir_error)  | 
                    |
| 1283 | 
                        + assert cli_helpers.get_tempdir() == config_dir  | 
                    |
| 1284 | 
                        +  | 
                    |
| 1285 | 
                        + @Parametrize.DELETE_CONFIG_INPUT  | 
                    |
| 1286 | 
                        + def test_203_repeated_config_deletion(  | 
                    |
| 1287 | 
                        + self,  | 
                    |
| 1288 | 
                        + command_line: list[str],  | 
                    |
| 1289 | 
                        + config: _types.VaultConfig,  | 
                    |
| 1290 | 
                        + result_config: _types.VaultConfig,  | 
                    |
| 1291 | 
                        + ) -> None:  | 
                    |
| 1292 | 
                        + """Repeatedly removing the same parts of a configuration works."""  | 
                    |
| 1293 | 
                        + for start_config in [config, result_config]:  | 
                    |
| 1294 | 
                        + runner = machinery.CliRunner(mix_stderr=False)  | 
                    |
| 1295 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 1296 | 
                        + # with-statements.  | 
                    |
| 1297 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 1298 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 1299 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 1300 | 
                        + stack.enter_context(  | 
                    |
| 1301 | 
                        + pytest_machinery.isolated_vault_config(  | 
                    |
| 1302 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 1303 | 
                        + runner=runner,  | 
                    |
| 1304 | 
                        + vault_config=start_config,  | 
                    |
| 1305 | 
                        + )  | 
                    |
| 1306 | 
                        + )  | 
                    |
| 1307 | 
                        + result = runner.invoke(  | 
                    |
| 1308 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 1309 | 
                        + command_line,  | 
                    |
| 1310 | 
                        + catch_exceptions=False,  | 
                    |
| 1311 | 
                        + )  | 
                    |
| 1312 | 
                        + assert result.clean_exit(empty_stderr=True), (  | 
                    |
| 1313 | 
                        + "expected clean exit"  | 
                    |
| 1314 | 
                        + )  | 
                    |
| 1315 | 
                        + with cli_helpers.config_filename(subsystem="vault").open(  | 
                    |
| 1316 | 
                        + encoding="UTF-8"  | 
                    |
| 1317 | 
                        + ) as infile:  | 
                    |
| 1318 | 
                        + config_readback = json.load(infile)  | 
                    |
| 1319 | 
                        + assert config_readback == result_config  | 
                    |
| 1320 | 
                        +  | 
                    |
| 1321 | 
                        + def test_204_phrase_from_key_manually(self) -> None:  | 
                    |
| 1322 | 
                        + """The dummy service, key and config settings are consistent."""  | 
                    |
| 1323 | 
                        + assert (  | 
                    |
| 1324 | 
                        + vault.Vault(  | 
                    |
| 1325 | 
                        + phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS  | 
                    |
| 1326 | 
                        + ).generate(DUMMY_SERVICE)  | 
                    |
| 1327 | 
                        + == DUMMY_RESULT_KEY1  | 
                    |
| 1328 | 
                        + )  | 
                    |
| 1329 | 
                        +  | 
                    |
| 1330 | 
                        + @Parametrize.VALIDATION_FUNCTION_INPUT  | 
                    |
| 1331 | 
                        + def test_210a_validate_constraints_manually(  | 
                    |
| 1332 | 
                        + self,  | 
                    |
| 1333 | 
                        + vfunc: Callable[[click.Context, click.Parameter, Any], int | None],  | 
                    |
| 1334 | 
                        + input: int,  | 
                    |
| 1335 | 
                        + ) -> None:  | 
                    |
| 1336 | 
                        + """Command-line argument constraint validation works."""  | 
                    |
| 1337 | 
                        + ctx = cli.derivepassphrase_vault.make_context(cli.PROG_NAME, [])  | 
                    |
| 1338 | 
                        + param = cli.derivepassphrase_vault.params[0]  | 
                    |
| 1339 | 
                        + assert vfunc(ctx, param, input) == input  | 
                    |
| 1340 | 
                        +  | 
                    |
| 1341 | 
                        + @Parametrize.CONNECTION_HINTS  | 
                    |
| 1342 | 
                        + def test_227_get_suitable_ssh_keys(  | 
                    |
| 1343 | 
                        + self,  | 
                    |
| 1344 | 
                        + running_ssh_agent: data.RunningSSHAgentInfo,  | 
                    |
| 1345 | 
                        + conn_hint: str,  | 
                    |
| 1346 | 
                        + ) -> None:  | 
                    |
| 1347 | 
                        + """[`cli_helpers.get_suitable_ssh_keys`][] works."""  | 
                    |
| 1348 | 
                        + with pytest.MonkeyPatch.context() as monkeypatch:  | 
                    |
| 1349 | 
                        + monkeypatch.setattr(  | 
                    |
| 1350 | 
                        + ssh_agent.SSHAgentClient,  | 
                    |
| 1351 | 
                        + "list_keys",  | 
                    |
| 1352 | 
                        + callables.list_keys,  | 
                    |
| 1353 | 
                        + )  | 
                    |
| 1354 | 
                        + hint: ssh_agent.SSHAgentClient | _types.SSHAgentSocket | None  | 
                    |
| 1355 | 
                        + # TODO(the-13th-letter): Rewrite using structural pattern  | 
                    |
| 1356 | 
                        + # matching.  | 
                    |
| 1357 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 1358 | 
                        + if conn_hint == "client":  | 
                    |
| 1359 | 
                        + hint = ssh_agent.SSHAgentClient()  | 
                    |
| 1360 | 
                        + elif conn_hint == "socket":  | 
                    |
| 1361 | 
                        + if isinstance(  | 
                    |
| 1362 | 
                        + running_ssh_agent.socket, str  | 
                    |
| 1363 | 
                        + ): # pragma: no cover  | 
                    |
| 1364 | 
                        + if not hasattr(socket, "AF_UNIX"):  | 
                    |
| 1365 | 
                        +                        pytest.skip("socket module does not support AF_UNIX")
                       | 
                    |
| 1366 | 
                        + # socket.AF_UNIX is not defined everywhere.  | 
                    |
| 1367 | 
                        + hint = socket.socket(family=socket.AF_UNIX) # type: ignore[attr-defined]  | 
                    |
| 1368 | 
                        + hint.connect(running_ssh_agent.socket)  | 
                    |
| 1369 | 
                        + else: # pragma: no cover  | 
                    |
| 1370 | 
                        + hint = running_ssh_agent.socket()  | 
                    |
| 1371 | 
                        + else:  | 
                    |
| 1372 | 
                        + assert conn_hint == "none"  | 
                    |
| 1373 | 
                        + hint = None  | 
                    |
| 1374 | 
                        + exception: Exception | None = None  | 
                    |
| 1375 | 
                        + try:  | 
                    |
| 1376 | 
                        + list(cli_helpers.get_suitable_ssh_keys(hint))  | 
                    |
| 1377 | 
                        + except RuntimeError: # pragma: no cover  | 
                    |
| 1378 | 
                        + pass  | 
                    |
| 1379 | 
                        + except Exception as e: # noqa: BLE001 # pragma: no cover  | 
                    |
| 1380 | 
                        + exception = e  | 
                    |
| 1381 | 
                        + finally:  | 
                    |
| 1382 | 
                        + assert exception is None, (  | 
                    |
| 1383 | 
                        + "exception querying suitable SSH keys"  | 
                    |
| 1384 | 
                        + )  | 
                    |
| 1385 | 
                        +  | 
                    |
| 1386 | 
                        + @Parametrize.KEY_TO_PHRASE_SETTINGS  | 
                    |
| 1387 | 
                        + def test_400_key_to_phrase(  | 
                    |
| 1388 | 
                        + self,  | 
                    |
| 1389 | 
                        + ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,  | 
                    |
| 1390 | 
                        + list_keys_action: ListKeysAction | None,  | 
                    |
| 1391 | 
                        + system_support_action: SystemSupportAction | None,  | 
                    |
| 1392 | 
                        + address_action: SocketAddressAction | None,  | 
                    |
| 1393 | 
                        + sign_action: SignAction,  | 
                    |
| 1394 | 
                        + pattern: str,  | 
                    |
| 1395 | 
                        + ) -> None:  | 
                    |
| 1396 | 
                        + """All errors in [`cli_helpers.key_to_phrase`][] are handled."""  | 
                    |
| 1397 | 
                        +  | 
                    |
| 1398 | 
                        + class ErrCallback(BaseException):  | 
                    |
| 1399 | 
                        + def __init__(self, *args: Any, **kwargs: Any) -> None:  | 
                    |
| 1400 | 
                        + super().__init__(*args[:1])  | 
                    |
| 1401 | 
                        + self.args = args  | 
                    |
| 1402 | 
                        + self.kwargs = kwargs  | 
                    |
| 1403 | 
                        +  | 
                    |
| 1404 | 
                        + def err(*args: Any, **_kwargs: Any) -> NoReturn:  | 
                    |
| 1405 | 
                        + raise ErrCallback(*args, **_kwargs)  | 
                    |
| 1406 | 
                        +  | 
                    |
| 1407 | 
                        + with pytest.MonkeyPatch.context() as monkeypatch:  | 
                    |
| 1408 | 
                        + loaded_keys = list(  | 
                    |
| 1409 | 
                        + ssh_agent_client_with_test_keys_loaded.list_keys()  | 
                    |
| 1410 | 
                        + )  | 
                    |
| 1411 | 
                        + loaded_key = base64.standard_b64encode(loaded_keys[0][0])  | 
                    |
| 1412 | 
                        + monkeypatch.setattr(ssh_agent.SSHAgentClient, "sign", sign_action)  | 
                    |
| 1413 | 
                        + if list_keys_action:  | 
                    |
| 1414 | 
                        + monkeypatch.setattr(  | 
                    |
| 1415 | 
                        + ssh_agent.SSHAgentClient, "list_keys", list_keys_action  | 
                    |
| 1416 | 
                        + )  | 
                    |
| 1417 | 
                        + if address_action:  | 
                    |
| 1418 | 
                        + address_action(monkeypatch)  | 
                    |
| 1419 | 
                        + if system_support_action:  | 
                    |
| 1420 | 
                        + system_support_action(monkeypatch)  | 
                    |
| 1421 | 
                        + with pytest.raises(ErrCallback, match=pattern) as excinfo:  | 
                    |
| 1422 | 
                        + cli_helpers.key_to_phrase(loaded_key, error_callback=err)  | 
                    |
| 1423 | 
                        + if list_keys_action == ListKeysAction.FAIL_RUNTIME:  | 
                    |
| 1424 | 
                        + assert excinfo.value.kwargs  | 
                    |
| 1425 | 
                        + assert isinstance(  | 
                    |
| 1426 | 
                        + excinfo.value.kwargs["exc_info"],  | 
                    |
| 1427 | 
                        + ssh_agent.SSHAgentFailedError,  | 
                    |
| 1428 | 
                        + )  | 
                    |
| 1429 | 
                        + assert excinfo.value.kwargs["exc_info"].__context__ is not None  | 
                    |
| 1430 | 
                        + assert isinstance(  | 
                    |
| 1431 | 
                        + excinfo.value.kwargs["exc_info"].__context__,  | 
                    |
| 1432 | 
                        + ssh_agent.TrailingDataError,  | 
                    |
| 1433 | 
                        + )  | 
                    |
| 0 | 1434 |