Coverage for tests / test_derivepassphrase_cli / test_shell_completion.py: 100.000%
143 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
1# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-License-Identifier: Zlib
5"""Tests for the `derivepassphrase` command-line interface: shell completion."""
7from __future__ import annotations
9import contextlib
10import json
11import types
12from typing import TYPE_CHECKING
14import click.shell_completion
15import hypothesis
16import pytest
17from typing_extensions import Any
19from derivepassphrase import _types, cli
20from derivepassphrase._internals import cli_helpers
21from derivepassphrase._internals import (
22 cli_machinery as cli_machinery, # noqa: PLC0414
23)
24from tests import data, machinery
25from tests.machinery import hypothesis as hypothesis_machinery
26from tests.machinery import pytest as pytest_machinery
28if TYPE_CHECKING:
29 from collections.abc import Callable, Sequence
30 from collections.abc import Set as AbstractSet
31 from typing import NoReturn
33 from typing_extensions import Literal
35DUMMY_SERVICE = data.DUMMY_SERVICE
36DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS
39def bash_format(item: click.shell_completion.CompletionItem) -> str:
40 """A formatter for `bash`-style shell completion items.
42 The format is `type,value`, and is dictated by
43 [`click`](https://pypi.org/project/click/).
45 """
46 type, value = ( # noqa: A001 1b
47 item.type,
48 item.value,
49 )
50 return f"{type},{value}" 1b
53def fish_format(item: click.shell_completion.CompletionItem) -> str:
54 r"""A formatter for `fish`-style shell completion items.
56 The format is `type,value<tab>help`, and is dictated by
57 [`click`](https://pypi.org/project/click/).
59 """
60 type, value, help = ( # noqa: A001 1b
61 item.type,
62 item.value,
63 item.help,
64 )
65 return f"{type},{value}\t{help}" if help else f"{type},{value}" 1b
68def zsh_format(item: click.shell_completion.CompletionItem) -> str:
69 r"""A formatter for `zsh`-style shell completion items.
71 The format is `type<newline>value<newline>help<newline>`, and is
72 dictated by [`click`](https://pypi.org/project/click/). Upstream
73 `click` up to and including v8.2.1 does not deal with colons in the
74 value correctly when the help text is non-degenerate. Our formatter
75 here does, provided the upstream `zsh` completion script is used;
76 see the [`cli_machinery.ZshComplete`][] class.
78 """
79 empty_help = "_" 1b
80 help_, value = ( 1b
81 (item.help, item.value.replace(":", r"\:"))
82 if item.help and item.help == empty_help
83 else (empty_help, item.value)
84 )
85 return f"{item.type}\n{value}\n{help_}" 1b
88def completion_item(
89 item: str | click.shell_completion.CompletionItem,
90) -> click.shell_completion.CompletionItem:
91 """Convert a string to a completion item, if necessary."""
92 return ( 1b
93 click.shell_completion.CompletionItem(item, type="plain")
94 if isinstance(item, str)
95 else item
96 )
99def assertable_item(
100 item: str | click.shell_completion.CompletionItem,
101) -> tuple[str, Any, str | None]:
102 """Convert a completion item into a pretty-printable item.
104 Intended to make completion items introspectable in pytest's
105 `assert` output.
107 """
108 item = completion_item(item) 1b
109 return (item.type, item.value, item.help) 1b
112class Parametrize(types.SimpleNamespace):
113 """Common test parametrizations."""
115 COMPLETABLE_PATH_ARGUMENT = pytest.mark.parametrize(
116 "command_prefix",
117 [
118 pytest.param(
119 ("export", "vault"),
120 id="derivepassphrase-export-vault",
121 ),
122 pytest.param(
123 ("vault", "--export"),
124 id="derivepassphrase-vault--export",
125 ),
126 pytest.param(
127 ("vault", "--import"),
128 id="derivepassphrase-vault--import",
129 ),
130 ],
131 )
132 COMPLETABLE_OPTIONS = pytest.mark.parametrize(
133 ["command_prefix", "incomplete", "completions"],
134 [
135 pytest.param(
136 (),
137 "-",
138 frozenset({
139 "--help",
140 "-h",
141 "--version",
142 "--debug",
143 "--verbose",
144 "-v",
145 "--quiet",
146 "-q",
147 }),
148 id="derivepassphrase",
149 ),
150 pytest.param(
151 ("export",),
152 "-",
153 frozenset({
154 "--help",
155 "-h",
156 "--version",
157 "--debug",
158 "--verbose",
159 "-v",
160 "--quiet",
161 "-q",
162 }),
163 id="derivepassphrase-export",
164 ),
165 pytest.param(
166 ("export", "vault"),
167 "-",
168 frozenset({
169 "--help",
170 "-h",
171 "--version",
172 "--debug",
173 "--verbose",
174 "-v",
175 "--quiet",
176 "-q",
177 "--format",
178 "-f",
179 "--key",
180 "-k",
181 }),
182 id="derivepassphrase-export-vault",
183 ),
184 pytest.param(
185 ("vault",),
186 "-",
187 frozenset({
188 "--help",
189 "-h",
190 "--version",
191 "--debug",
192 "--verbose",
193 "-v",
194 "--quiet",
195 "-q",
196 "--phrase",
197 "-p",
198 "--key",
199 "-k",
200 "--length",
201 "-l",
202 "--repeat",
203 "-r",
204 "--upper",
205 "--lower",
206 "--number",
207 "--space",
208 "--dash",
209 "--symbol",
210 "--config",
211 "-c",
212 "--notes",
213 "-n",
214 "--delete",
215 "-x",
216 "--delete-globals",
217 "--clear",
218 "-X",
219 "--export",
220 "-e",
221 "--import",
222 "-i",
223 "--overwrite-existing",
224 "--merge-existing",
225 "--unset",
226 "--export-as",
227 "--modern-editor-interface",
228 "--vault-legacy-editor-interface",
229 "--print-notes-before",
230 "--print-notes-after",
231 "--ssh-agent-socket-provider",
232 }),
233 id="derivepassphrase-vault",
234 ),
235 ],
236 )
237 COMPLETABLE_SUBCOMMANDS = hypothesis_machinery.explicit_examples(
238 ["command_prefix", "incomplete", "completions"],
239 [
240 pytest.param(
241 (),
242 "",
243 frozenset({"export", "vault"}),
244 id="derivepassphrase",
245 ),
246 pytest.param(
247 ("export",),
248 "",
249 frozenset({"vault"}),
250 id="derivepassphrase-export",
251 ),
252 ],
253 )
254 COMPLETION_FUNCTION_INPUTS = hypothesis_machinery.explicit_examples(
255 ["config", "comp_func", "args", "incomplete", "results"],
256 [
257 pytest.param(
258 {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
259 cli_helpers.shell_complete_service,
260 ["vault"],
261 "",
262 [DUMMY_SERVICE],
263 id="base_config-service",
264 ),
265 pytest.param(
266 {"services": {}},
267 cli_helpers.shell_complete_service,
268 ["vault"],
269 "",
270 [],
271 id="empty_config-service",
272 ),
273 pytest.param(
274 {
275 "services": {
276 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
277 "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),
278 }
279 },
280 cli_helpers.shell_complete_service,
281 ["vault"],
282 "",
283 [DUMMY_SERVICE],
284 id="incompletable_newline_config-service",
285 ),
286 pytest.param(
287 {
288 "services": {
289 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
290 "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),
291 }
292 },
293 cli_helpers.shell_complete_service,
294 ["vault"],
295 "",
296 [DUMMY_SERVICE],
297 id="incompletable_backspace_config-service",
298 ),
299 pytest.param(
300 {
301 "services": {
302 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
303 "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),
304 }
305 },
306 cli_helpers.shell_complete_service,
307 ["vault"],
308 "",
309 sorted([DUMMY_SERVICE, "colon:in:name"]),
310 id="brittle_colon_config-service",
311 ),
312 pytest.param(
313 {
314 "services": {
315 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
316 "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),
317 "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),
318 "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),
319 "nul\x00in\x00name": DUMMY_CONFIG_SETTINGS.copy(),
320 "del\x7fin\x7fname": DUMMY_CONFIG_SETTINGS.copy(),
321 }
322 },
323 cli_helpers.shell_complete_service,
324 ["vault"],
325 "",
326 sorted([DUMMY_SERVICE, "colon:in:name"]),
327 id="brittle_incompletable_multi_config-service",
328 ),
329 pytest.param(
330 {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
331 cli_helpers.shell_complete_path,
332 ["vault", "--import"],
333 "",
334 [click.shell_completion.CompletionItem("", type="file")],
335 id="base_config-path",
336 ),
337 pytest.param(
338 {"services": {}},
339 cli_helpers.shell_complete_path,
340 ["vault", "--import"],
341 "",
342 [click.shell_completion.CompletionItem("", type="file")],
343 id="empty_config-path",
344 ),
345 ],
346 )
347 COMPLETABLE_SERVICE_NAMES = pytest.mark.parametrize(
348 ["config", "incomplete", "completions"],
349 [
350 pytest.param(
351 {"services": {}},
352 "",
353 frozenset(),
354 id="no_services",
355 ),
356 pytest.param(
357 {"services": {}},
358 "partial",
359 frozenset(),
360 id="no_services_partial",
361 ),
362 pytest.param(
363 {"services": {DUMMY_SERVICE: {"length": 10}}},
364 "",
365 frozenset({DUMMY_SERVICE}),
366 id="one_service",
367 ),
368 pytest.param(
369 {"services": {DUMMY_SERVICE: {"length": 10}}},
370 DUMMY_SERVICE[:4],
371 frozenset({DUMMY_SERVICE}),
372 id="one_service_partial",
373 ),
374 pytest.param(
375 {"services": {DUMMY_SERVICE: {"length": 10}}},
376 DUMMY_SERVICE[-4:],
377 frozenset(),
378 id="one_service_partial_miss",
379 ),
380 ],
381 )
382 SERVICE_NAME_COMPLETION_INPUTS = hypothesis_machinery.explicit_examples(
383 ["config", "key", "incomplete", "completions"],
384 [
385 pytest.param(
386 {
387 "services": {
388 DUMMY_SERVICE: {"length": 10},
389 "newline\nin\nname": {"length": 10},
390 },
391 },
392 "newline\nin\nname",
393 "",
394 frozenset({DUMMY_SERVICE}),
395 id="newline",
396 ),
397 pytest.param(
398 {
399 "services": {
400 DUMMY_SERVICE: {"length": 10},
401 "newline\nin\nname": {"length": 10},
402 },
403 },
404 "newline\nin\nname",
405 "serv",
406 frozenset({DUMMY_SERVICE}),
407 id="newline_partial_other",
408 ),
409 pytest.param(
410 {
411 "services": {
412 DUMMY_SERVICE: {"length": 10},
413 "newline\nin\nname": {"length": 10},
414 },
415 },
416 "newline\nin\nname",
417 "newline",
418 frozenset({}),
419 id="newline_partial_specific",
420 ),
421 pytest.param(
422 {
423 "services": {
424 DUMMY_SERVICE: {"length": 10},
425 "nul\x00in\x00name": {"length": 10},
426 },
427 },
428 "nul\x00in\x00name",
429 "",
430 frozenset({DUMMY_SERVICE}),
431 id="nul",
432 ),
433 pytest.param(
434 {
435 "services": {
436 DUMMY_SERVICE: {"length": 10},
437 "nul\x00in\x00name": {"length": 10},
438 },
439 },
440 "nul\x00in\x00name",
441 "serv",
442 frozenset({DUMMY_SERVICE}),
443 id="nul_partial_other",
444 ),
445 pytest.param(
446 {
447 "services": {
448 DUMMY_SERVICE: {"length": 10},
449 "nul\x00in\x00name": {"length": 10},
450 },
451 },
452 "nul\x00in\x00name",
453 "nul",
454 frozenset({}),
455 id="nul_partial_specific",
456 ),
457 pytest.param(
458 {
459 "services": {
460 DUMMY_SERVICE: {"length": 10},
461 "backspace\bin\bname": {"length": 10},
462 },
463 },
464 "backspace\bin\bname",
465 "",
466 frozenset({DUMMY_SERVICE}),
467 id="backspace",
468 ),
469 pytest.param(
470 {
471 "services": {
472 DUMMY_SERVICE: {"length": 10},
473 "backspace\bin\bname": {"length": 10},
474 },
475 },
476 "backspace\bin\bname",
477 "serv",
478 frozenset({DUMMY_SERVICE}),
479 id="backspace_partial_other",
480 ),
481 pytest.param(
482 {
483 "services": {
484 DUMMY_SERVICE: {"length": 10},
485 "backspace\bin\bname": {"length": 10},
486 },
487 },
488 "backspace\bin\bname",
489 "back",
490 frozenset({}),
491 id="backspace_partial_specific",
492 ),
493 pytest.param(
494 {
495 "services": {
496 DUMMY_SERVICE: {"length": 10},
497 "del\x7fin\x7fname": {"length": 10},
498 },
499 },
500 "del\x7fin\x7fname",
501 "",
502 frozenset({DUMMY_SERVICE}),
503 id="del",
504 ),
505 pytest.param(
506 {
507 "services": {
508 DUMMY_SERVICE: {"length": 10},
509 "del\x7fin\x7fname": {"length": 10},
510 },
511 },
512 "del\x7fin\x7fname",
513 "serv",
514 frozenset({DUMMY_SERVICE}),
515 id="del_partial_other",
516 ),
517 pytest.param(
518 {
519 "services": {
520 DUMMY_SERVICE: {"length": 10},
521 "del\x7fin\x7fname": {"length": 10},
522 },
523 },
524 "del\x7fin\x7fname",
525 "del",
526 frozenset({}),
527 id="del_partial_specific",
528 ),
529 ],
530 settings=hypothesis.settings(
531 parent=hypothesis.settings(),
532 suppress_health_check=[
533 hypothesis.HealthCheck.function_scoped_fixture
534 ],
535 ),
536 )
537 SERVICE_NAME_EXCEPTIONS = pytest.mark.parametrize(
538 "exc_type", [RuntimeError, KeyError, ValueError]
539 )
540 INCOMPLETE = pytest.mark.parametrize("incomplete", ["", "partial"])
541 CONFIG_SETTING_MODE = pytest.mark.parametrize("mode", ["config", "import"])
542 COMPLETABLE_ITEMS = hypothesis_machinery.explicit_examples(
543 ["partial", "is_completable"],
544 [
545 ("", True),
546 (DUMMY_SERVICE, True),
547 ("a\bn", False),
548 ("\b", False),
549 ("\x00", False),
550 ("\x20", True),
551 ("\x7f", False),
552 ("service with spaces", True),
553 ("service\nwith\nnewlines", False),
554 ],
555 )
556 SHELL_FORMATTER = pytest.mark.parametrize(
557 ["shell", "format_func"],
558 [
559 pytest.param("bash", bash_format, id="bash"),
560 pytest.param("fish", fish_format, id="fish"),
561 pytest.param("zsh", zsh_format, id="zsh"),
562 ],
563 )
566class TestShellCompletion:
567 """Tests for the shell completion machinery."""
569 class Completions:
570 """A deferred completion call."""
572 def __init__(
573 self,
574 args: Sequence[str],
575 incomplete: str,
576 ) -> None:
577 """Initialize the object.
579 Args:
580 args:
581 The sequence of complete command-line arguments.
582 incomplete:
583 The final, incomplete, partial argument.
585 """
586 self.args = tuple(args) 1gedcf
587 self.incomplete = incomplete 1gedcf
589 def __call__(self) -> Sequence[click.shell_completion.CompletionItem]:
590 """Return the completion items."""
591 args = list(self.args) 1gedcf
592 completion = click.shell_completion.ShellComplete( 1gedcf
593 cli=cli.derivepassphrase,
594 ctx_args={},
595 prog_name="derivepassphrase",
596 complete_var="_DERIVEPASSPHRASE_COMPLETE",
597 )
598 return completion.get_completions(args, self.incomplete) 1gedcf
600 def get_words(self) -> Sequence[str]:
601 """Return the completion items' values, as a sequence."""
602 return tuple(c.value for c in self()) 1gdcf
605class TestCompletableItems:
606 """Tests for completablility of items."""
608 @Parametrize.COMPLETABLE_ITEMS
609 def test_is_completable_item( 1aj
610 self,
611 partial: str,
612 is_completable: bool,
613 ) -> None:
614 """Our `_is_completable_item` predicate for service names works."""
615 assert cli_helpers.is_completable_item(partial) == is_completable 1j
618class TestCompletableItemClasses(TestShellCompletion):
619 """Tests for the different classes of completable items."""
621 @Parametrize.COMPLETABLE_OPTIONS
622 def test_options(
623 self,
624 command_prefix: Sequence[str],
625 incomplete: str,
626 completions: AbstractSet[str],
627 ) -> None:
628 """Our completion machinery works for all commands' options."""
629 comp = self.Completions(command_prefix, incomplete) 1g
630 assert frozenset(comp.get_words()) == completions 1g
632 @Parametrize.COMPLETABLE_SUBCOMMANDS
633 def test_subcommands( 1af
634 self,
635 command_prefix: Sequence[str],
636 incomplete: str,
637 completions: AbstractSet[str],
638 ) -> None:
639 """Our completion machinery works for all commands' subcommands."""
640 comp = self.Completions(command_prefix, incomplete) 1f
641 assert frozenset(comp.get_words()) == completions 1f
643 @Parametrize.COMPLETABLE_PATH_ARGUMENT
644 @Parametrize.INCOMPLETE
645 def test_paths(
646 self,
647 command_prefix: Sequence[str],
648 incomplete: str,
649 ) -> None:
650 """Our completion machinery works for all commands' paths."""
651 file = click.shell_completion.CompletionItem("", type="file") 1e
652 completions = frozenset({(file.type, file.value, file.help)}) 1e
653 comp = self.Completions(command_prefix, incomplete) 1e
654 assert ( 1e
655 frozenset((x.type, x.value, x.help) for x in comp()) == completions
656 )
658 @Parametrize.COMPLETABLE_SERVICE_NAMES
659 def test_service_names(
660 self,
661 config: _types.VaultConfig,
662 incomplete: str,
663 completions: AbstractSet[str],
664 ) -> None:
665 """Our completion machinery works for vault service names."""
666 runner = machinery.CliRunner(mix_stderr=False) 1d
667 # TODO(the-13th-letter): Rewrite using parenthesized
668 # with-statements.
669 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
670 with contextlib.ExitStack() as stack: 1d
671 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1d
672 stack.enter_context( 1d
673 pytest_machinery.isolated_vault_config(
674 monkeypatch=monkeypatch,
675 runner=runner,
676 vault_config=config,
677 )
678 )
679 comp = self.Completions(["vault"], incomplete) 1d
680 assert frozenset(comp.get_words()) == completions 1d
683class TestCompletionFormatting:
684 """Tests for the formatting of completable items."""
686 @Parametrize.SHELL_FORMATTER
687 @Parametrize.COMPLETION_FUNCTION_INPUTS 1ab
688 def test_shell_completion_formatting(
689 self,
690 shell: str,
691 format_func: Callable[[click.shell_completion.CompletionItem], str],
692 config: _types.VaultConfig,
693 comp_func: Callable[
694 [click.Context, click.Parameter, str],
695 list[str | click.shell_completion.CompletionItem],
696 ],
697 args: list[str],
698 incomplete: str,
699 results: list[str | click.shell_completion.CompletionItem],
700 ) -> None:
701 """Custom completion functions work for all shells."""
702 runner = machinery.CliRunner(mix_stderr=False) 1b
703 # TODO(the-13th-letter): Rewrite using parenthesized
704 # with-statements.
705 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
706 with contextlib.ExitStack() as stack: 1b
707 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1b
708 stack.enter_context( 1b
709 pytest_machinery.isolated_vault_config(
710 monkeypatch=monkeypatch,
711 runner=runner,
712 vault_config=config,
713 )
714 )
715 expected_items = [assertable_item(item) for item in results] 1b
716 expected_string = "\n".join( 1b
717 format_func(completion_item(item)) for item in results
718 )
719 manual_raw_items = comp_func( 1b
720 click.Context(cli.derivepassphrase),
721 click.Argument(["sample_parameter"]),
722 incomplete,
723 )
724 manual_items = [assertable_item(item) for item in manual_raw_items] 1b
725 manual_string = "\n".join( 1b
726 format_func(completion_item(item)) for item in manual_raw_items
727 )
728 assert manual_items == expected_items 1b
729 assert manual_string == expected_string 1b
730 comp_class = click.shell_completion.get_completion_class(shell) 1b
731 assert comp_class is not None 1b
732 comp = comp_class( 1b
733 cli.derivepassphrase,
734 {},
735 "derivepassphrase",
736 "_DERIVEPASSPHRASE_COMPLETE",
737 )
738 monkeypatch.setattr( 1b
739 comp,
740 "get_completion_args",
741 lambda *_a, **_kw: (args, incomplete),
742 )
743 actual_raw_items = comp.get_completions( 1b
744 *comp.get_completion_args()
745 )
746 actual_items = [assertable_item(item) for item in actual_raw_items] 1b
747 actual_string = comp.complete() 1b
748 assert actual_items == expected_items 1b
749 assert actual_string == expected_string 1b
752class TestCLICompletabilityHandling(TestShellCompletion):
753 """Tests for how the command-line interface handles completability."""
755 @Parametrize.CONFIG_SETTING_MODE
756 @Parametrize.SERVICE_NAME_COMPLETION_INPUTS 1ac
757 def test_cli_warns_and_completion_skips_incompletable_service_names(
758 self,
759 caplog: pytest.LogCaptureFixture,
760 mode: Literal["config", "import"],
761 config: _types.VaultConfig,
762 key: str,
763 incomplete: str,
764 completions: AbstractSet[str],
765 ) -> None:
766 """Completion skips incompletable items."""
767 caplog.clear() 1c
768 vault_config = config if mode == "config" else {"services": {}} 1c
769 runner = machinery.CliRunner(mix_stderr=False) 1c
770 # TODO(the-13th-letter): Rewrite using parenthesized
771 # with-statements.
772 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
773 with contextlib.ExitStack() as stack: 1c
774 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1c
775 stack.enter_context( 1c
776 pytest_machinery.isolated_vault_config(
777 monkeypatch=monkeypatch,
778 runner=runner,
779 vault_config=vault_config,
780 )
781 )
782 if mode == "config": 1c
783 result = runner.invoke( 1c
784 cli.derivepassphrase_vault,
785 ["--config", "--length=10", "--", key],
786 catch_exceptions=False,
787 )
788 else:
789 result = runner.invoke( 1c
790 cli.derivepassphrase_vault,
791 ["--import", "-"],
792 catch_exceptions=False,
793 input=json.dumps(config),
794 )
795 assert result.clean_exit(), "expected clean exit" 1c
796 assert machinery.warning_emitted( 1c
797 "contains an ASCII control character", caplog.record_tuples
798 ), "expected known warning message in stderr"
799 assert machinery.warning_emitted( 1c
800 "not be available for completion", caplog.record_tuples
801 ), "expected known warning message in stderr"
802 assert cli_helpers.load_config() == config 1c
803 comp = self.Completions(["vault"], incomplete) 1c
804 assert frozenset(comp.get_words()) == completions 1c
806 def test_handling_nonexistant_service_names(
807 self,
808 ) -> None:
809 """Service name completion quietly fails on missing configuration."""
810 runner = machinery.CliRunner(mix_stderr=False) 1i
811 # TODO(the-13th-letter): Rewrite using parenthesized
812 # with-statements.
813 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
814 with contextlib.ExitStack() as stack: 1i
815 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1i
816 stack.enter_context( 1i
817 pytest_machinery.isolated_vault_config(
818 monkeypatch=monkeypatch,
819 runner=runner,
820 vault_config={
821 "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
822 },
823 )
824 )
825 cli_helpers.config_filename(subsystem="vault").unlink( 1i
826 missing_ok=True
827 )
828 assert not cli_helpers.shell_complete_service( 1i
829 click.Context(cli.derivepassphrase),
830 click.Argument(["some_parameter"]),
831 "",
832 )
834 # TODO(the-13th-letter): Reevaluate whether to keep this as a
835 # non-hypothesis test.
836 @Parametrize.SERVICE_NAME_EXCEPTIONS
837 def test_handling_unexpected_exceptions(
838 self,
839 exc_type: type[Exception],
840 ) -> None:
841 """Service name completion quietly fails on configuration errors."""
842 runner = machinery.CliRunner(mix_stderr=False) 1h
843 # TODO(the-13th-letter): Rewrite using parenthesized
844 # with-statements.
845 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
846 with contextlib.ExitStack() as stack: 1h
847 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1h
848 stack.enter_context( 1h
849 pytest_machinery.isolated_vault_config(
850 monkeypatch=monkeypatch,
851 runner=runner,
852 vault_config={
853 "services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
854 },
855 )
856 )
858 def raiser(*_a: Any, **_kw: Any) -> NoReturn: 1h
859 raise exc_type("just being difficult") # noqa: EM101,TRY003 1h
861 monkeypatch.setattr(cli_helpers, "load_config", raiser) 1h
862 assert not cli_helpers.shell_complete_service( 1h
863 click.Context(cli.derivepassphrase),
864 click.Argument(["some_parameter"]),
865 "",
866 )