Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1) # SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2) #
3) # SPDX-License-Identifier: Zlib
4)
|
Add module docstrings for t...
Marco Ricci authored 2 months ago
|
5) """Tests for the `derivepassphrase` command-line interface: internal utility functions."""
6)
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
7) from __future__ import annotations
8)
9) import base64
10) import contextlib
11) import ctypes
12) import enum
13) import errno
14) import io
15) import json
16) import logging
17) import operator
18) import os
19) import pathlib
20) import shlex
21) import shutil
22) import socket
23) import tempfile
24) import types
25) import warnings
26) from typing import TYPE_CHECKING
27)
28) import click.testing
29) import hypothesis
30) import pytest
31) from hypothesis import strategies
32) from typing_extensions import Any
33)
34) from derivepassphrase import _types, cli, ssh_agent, vault
35) from derivepassphrase._internals import (
36) cli_helpers,
37) cli_machinery,
38) )
39) from derivepassphrase.ssh_agent import socketprovider
40) from tests import data, machinery
41) from tests.data import callables
42) from tests.machinery import hypothesis as hypothesis_machinery
43) from tests.machinery import pytest as pytest_machinery
44)
45) if TYPE_CHECKING:
46) from collections.abc import Callable, Iterable, Iterator
47) from typing import NoReturn
48)
49)
50) DUMMY_SERVICE = data.DUMMY_SERVICE
51) DUMMY_PASSPHRASE = data.DUMMY_PASSPHRASE
52) DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS
|
Fix typing of the dummy vau...
Marco Ricci authored 2 months ago
|
53) DUMMY_CONFIG_SETTINGS_AS_CONSTRUCTOR_ARGS = (
54) data.DUMMY_CONFIG_SETTINGS_AS_CONSTRUCTOR_ARGS
55) )
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
56) DUMMY_RESULT_PASSPHRASE = data.DUMMY_RESULT_PASSPHRASE
57) DUMMY_RESULT_KEY1 = data.DUMMY_RESULT_KEY1
58) DUMMY_PHRASE_FROM_KEY1_RAW = data.DUMMY_PHRASE_FROM_KEY1_RAW
59) DUMMY_PHRASE_FROM_KEY1 = data.DUMMY_PHRASE_FROM_KEY1
60)
61) DUMMY_KEY1 = data.DUMMY_KEY1
62) DUMMY_KEY1_B64 = data.DUMMY_KEY1_B64
63) DUMMY_KEY2 = data.DUMMY_KEY2
64) DUMMY_KEY2_B64 = data.DUMMY_KEY2_B64
65) DUMMY_KEY3 = data.DUMMY_KEY3
66) DUMMY_KEY3_B64 = data.DUMMY_KEY3_B64
67)
68) TEST_CONFIGS = data.TEST_CONFIGS
69)
70)
71) def vault_config_exporter_shell_interpreter( # noqa: C901
72) script: str | Iterable[str],
73) /,
74) *,
75) prog_name_list: list[str] | None = None,
|
Fix type errors due to clic...
Marco Ricci authored 2 months ago
|
76) # The click.BaseCommand abstract base class, previously the base
77) # class of click.Command and click.Group, was removed in click
78) # 8.2.0 without a transition period, and click.Command instated
79) # as the common base class instead. To keep some degree of
80) # compatibility with both old click and new click, we explicitly
81) # list the (somewhat) concrete base classes we actually care
82) # about here.
83) command: click.Command | click.Group | None = None,
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
84) runner: machinery.CliRunner | None = None,
85) ) -> Iterator[machinery.ReadableResult]:
86) """A rudimentary sh(1) interpreter for `--export-as=sh` output.
87)
88) Assumes a script as emitted by `derivepassphrase vault
89) --export-as=sh --export -` and interprets the calls to
90) `derivepassphrase vault` within. (One call per line, skips all
91) other lines.) Also has rudimentary support for (quoted)
92) here-documents using `HERE` as the marker.
93)
94) """
95) if isinstance(script, str): # pragma: no cover
96) script = script.splitlines(False)
97) if prog_name_list is None: # pragma: no cover
98) prog_name_list = ["derivepassphrase", "vault"]
99) if command is None: # pragma: no cover
100) command = cli.derivepassphrase_vault
101) if runner is None: # pragma: no cover
102) runner = machinery.CliRunner(mix_stderr=False)
103) n = len(prog_name_list)
104) it = iter(script)
105) while True:
106) try:
107) raw_line = next(it)
108) except StopIteration:
109) break
110) else:
111) line = shlex.split(raw_line)
112) input_buffer: list[str] = []
113) if line[:n] != prog_name_list:
114) continue
115) line[:n] = []
116) if line and line[-1] == "<<HERE":
117) # naive HERE document support
118) while True:
119) try:
120) raw_line = next(it)
121) except StopIteration as exc: # pragma: no cover
122) msg = "incomplete here document"
123) raise EOFError(msg) from exc
124) else:
125) if raw_line == "HERE":
126) break
127) input_buffer.append(raw_line)
128) line.pop()
129) yield runner.invoke(
130) command,
131) line,
132) catch_exceptions=False,
133) input=("".join(x + "\n" for x in input_buffer) or None),
134) )
135)
136)
137) class ListKeysAction(str, enum.Enum):
138) """Test fixture settings for [`ssh_agent.SSHAgentClient.list_keys`][].
139)
140) Attributes:
141) EMPTY: Return an empty key list.
142) FAIL: Raise an [`ssh_agent.SSHAgentFailedError`][].
143) FAIL_RUNTIME: Raise an [`ssh_agent.TrailingDataError`][].
144)
145) """
146)
147) EMPTY = enum.auto()
148) """"""
149) FAIL = enum.auto()
150) """"""
151) FAIL_RUNTIME = enum.auto()
152) """"""
153)
154) def __call__(self, *_args: Any, **_kwargs: Any) -> Any:
155) """Execute the respective action."""
156) # TODO(the-13th-letter): Rewrite using structural pattern
157) # matching.
158) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
159) if self == self.EMPTY:
160) return []
161) if self == self.FAIL:
162) raise ssh_agent.SSHAgentFailedError(
163) _types.SSH_AGENT.FAILURE.value, b""
164) )
165) if self == self.FAIL_RUNTIME:
166) raise ssh_agent.TrailingDataError()
167) raise AssertionError()
168)
169)
170) class SignAction(str, enum.Enum):
171) """Test fixture settings for [`ssh_agent.SSHAgentClient.sign`][].
172)
173) Attributes:
174) FAIL: Raise an [`ssh_agent.SSHAgentFailedError`][].
175) FAIL_RUNTIME: Raise an [`ssh_agent.TrailingDataError`][].
176)
177) """
178)
179) FAIL = enum.auto()
180) """"""
181) FAIL_RUNTIME = enum.auto()
182) """"""
183)
184) def __call__(self, *_args: Any, **_kwargs: Any) -> Any:
185) """Execute the respective action."""
186) # TODO(the-13th-letter): Rewrite using structural pattern
187) # matching.
188) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
189) if self == self.FAIL:
190) raise ssh_agent.SSHAgentFailedError(
191) _types.SSH_AGENT.FAILURE.value, b""
192) )
193) if self == self.FAIL_RUNTIME:
194) raise ssh_agent.TrailingDataError()
195) raise AssertionError()
196)
197)
198) class SocketAddressAction(str, enum.Enum):
199) """Test fixture settings for the SSH agent socket address.
200)
201) Attributes:
202) MANGLE_ANNOYING_OS_NAMED_PIPE:
203) Mangle the address for the Annoying OS named pipe endpoint.
204) MANGLE_SSH_AUTH_SOCK:
205) Mangle the address for the UNIX domain socket (the
206) `SSH_AUTH_SOCK` environment variable).
207) UNSET_ANNOYING_OS_NAMED_PIPE:
208) Unset the address for the Annoying OS named pipe endpoint.
209) UNSET_SSH_AUTH_SOCK:
210) Unset the `SSH_AUTH_SOCK` environment variable (the address
211) for the UNIX domain socket).
212)
213) """
214)
215) MANGLE_ANNOYING_OS_NAMED_PIPE = enum.auto()
216) """"""
217) MANGLE_SSH_AUTH_SOCK = enum.auto()
218) """"""
219) UNSET_ANNOYING_OS_NAMED_PIPE = enum.auto()
220) """"""
221) UNSET_SSH_AUTH_SOCK = enum.auto()
222) """"""
223)
224) def __call__(
225) self, monkeypatch: pytest.MonkeyPatch, /, *_args: Any, **_kwargs: Any
226) ) -> None:
227) """Execute the respective action."""
228) # TODO(the-13th-letter): Rewrite using structural pattern
229) # matching.
230) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
231) if self in {
232) self.MANGLE_ANNOYING_OS_NAMED_PIPE,
233) self.UNSET_ANNOYING_OS_NAMED_PIPE,
234) }: # pragma: no cover [unused]
235) pass
236) elif self == self.MANGLE_SSH_AUTH_SOCK:
237) monkeypatch.setenv(
238) "SSH_AUTH_SOCK", os.environ["SSH_AUTH_SOCK"] + "~"
239) )
240) elif self == self.UNSET_SSH_AUTH_SOCK:
241) monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
242) else:
243) raise AssertionError()
244)
245)
246) class SystemSupportAction(str, enum.Enum):
247) """Test fixture settings for [`ssh_agent.SSHAgentClient`][] system support.
248)
249) Attributes:
250) UNSET_AF_UNIX:
251) Ensure lack of support for UNIX domain sockets.
252) UNSET_AF_UNIX_AND_ENSURE_USE:
253) Ensure lack of support for UNIX domain sockets, and that the
254) agent will use this socket provider.
255) UNSET_NATIVE:
256) Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`.
257) UNSET_NATIVE_AND_ENSURE_USE:
258) Ensure both `UNSET_AF_UNIX` and `UNSET_WINDLL`, and that the
259) agent will use the native socket provider.
260) UNSET_PROVIDER_LIST:
261) Ensure an empty list of SSH agent socket providers.
262) UNSET_WINDLL:
263) Ensure lack of support for The Annoying OS named pipes.
264) UNSET_WINDLL_AND_ENSURE_USE:
265) Ensure lack of support for The Annoying OS named pipes, and
266) that the agent will use this socket provider.
267)
268) """
269)
270) UNSET_AF_UNIX = enum.auto()
271) """"""
272) UNSET_AF_UNIX_AND_ENSURE_USE = enum.auto()
273) """"""
274) UNSET_NATIVE = enum.auto()
275) """"""
276) UNSET_NATIVE_AND_ENSURE_USE = enum.auto()
277) """"""
278) UNSET_PROVIDER_LIST = enum.auto()
279) """"""
280) UNSET_WINDLL = enum.auto()
281) """"""
282) UNSET_WINDLL_AND_ENSURE_USE = enum.auto()
283) """"""
284)
285) def __call__(
286) self, monkeypatch: pytest.MonkeyPatch, /, *_args: Any, **_kwargs: Any
287) ) -> None:
288) """Execute the respective action.
289)
290) Args:
291) monkeypatch: The current monkeypatch context.
292)
293) """
294) # TODO(the-13th-letter): Rewrite using structural pattern
295) # matching.
296) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
297) if self == self.UNSET_PROVIDER_LIST:
298) monkeypatch.setattr(
299) ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", []
300) )
301) elif self in {self.UNSET_NATIVE, self.UNSET_NATIVE_AND_ENSURE_USE}:
302) self.check_or_ensure_use(
303) "native",
304) monkeypatch=monkeypatch,
305) ensure_use=(self == self.UNSET_NATIVE_AND_ENSURE_USE),
306) )
307) monkeypatch.delattr(socket, "AF_UNIX", raising=False)
308) monkeypatch.delattr(ctypes, "WinDLL", raising=False)
309) monkeypatch.delattr(ctypes, "windll", raising=False)
310) elif self in {self.UNSET_AF_UNIX, self.UNSET_AF_UNIX_AND_ENSURE_USE}:
311) self.check_or_ensure_use(
312) "posix",
313) monkeypatch=monkeypatch,
314) ensure_use=(self == self.UNSET_AF_UNIX_AND_ENSURE_USE),
315) )
316) monkeypatch.delattr(socket, "AF_UNIX", raising=False)
317) elif self in {self.UNSET_WINDLL, self.UNSET_WINDLL_AND_ENSURE_USE}:
318) self.check_or_ensure_use(
319) "the_annoying_os",
320) monkeypatch=monkeypatch,
321) ensure_use=(self == self.UNSET_WINDLL_AND_ENSURE_USE),
322) )
323) monkeypatch.delattr(ctypes, "WinDLL", raising=False)
324) monkeypatch.delattr(ctypes, "windll", raising=False)
325) else:
326) raise AssertionError()
327)
328) @staticmethod
329) def check_or_ensure_use(
330) provider: str, /, *, monkeypatch: pytest.MonkeyPatch, ensure_use: bool
331) ) -> None:
332) """Check that the named SSH agent socket provider will be used.
333)
334) Either ensure that the socket provider will definitely be used,
335) or, upon detecting that it won't be used, skip the test.
336)
337) Args:
338) provider:
339) The provider to check for.
340) ensure_use:
341) If true, ensure that the socket provider will definitely
342) be used. If false, then check for whether it will be
343) used, and skip this test if not.
344) monkeypatch:
345) The monkeypatch context within which the fixture
346) adjustments should be executed.
347)
348) """
349) if ensure_use:
350) monkeypatch.setattr(
351) ssh_agent.SSHAgentClient, "SOCKET_PROVIDERS", [provider]
352) )
353) else: # pragma: no cover [external]
354) # This branch operates completely on instrumented or on
355) # externally defined, non-deterministic state.
356) intended: (
357) _types.SSHAgentSocketProvider
358) | socketprovider.NoSuchProviderError
359) | None
360) )
361) try:
362) intended = socketprovider.SocketProvider.lookup(provider)
363) except socketprovider.NoSuchProviderError as exc:
364) intended = exc
365) actual: (
366) _types.SSHAgentSocketProvider
367) | socketprovider.NoSuchProviderError
368) | None
369) )
370) for name in ssh_agent.SSHAgentClient.SOCKET_PROVIDERS:
371) try:
372) actual = socketprovider.SocketProvider.lookup(name)
373) except socketprovider.NoSuchProviderError as exc:
374) actual = exc
375) if actual is None:
376) continue
377) break
378) else:
379) actual = None
380) if intended != actual:
381) pytest.skip(
382) f"{provider!r} SSH agent socket provider "
383) f"is not currently in use"
384) )
385)
386)
387) class Parametrize(types.SimpleNamespace):
388) """Common test parametrizations."""
389)
390) DELETE_CONFIG_INPUT = pytest.mark.parametrize(
391) ["command_line", "config", "result_config"],
392) [
393) pytest.param(
394) ["--delete-globals"],
395) {"global": {"phrase": "abc"}, "services": {}},
396) {"services": {}},
397) id="globals",
398) ),
399) pytest.param(
400) ["--delete", "--", DUMMY_SERVICE],
401) {
402) "global": {"phrase": "abc"},
403) "services": {DUMMY_SERVICE: {"notes": "..."}},
404) },
405) {"global": {"phrase": "abc"}, "services": {}},
406) id="service",
407) ),
408) pytest.param(
409) ["--clear"],
410) {
411) "global": {"phrase": "abc"},
412) "services": {DUMMY_SERVICE: {"notes": "..."}},
413) },
414) {"services": {}},
415) id="all",
416) ),
417) ],
418) )
419) BASE_CONFIG_VARIATIONS = pytest.mark.parametrize(
420) "config",
421) [
422) {"global": {"phrase": "my passphrase"}, "services": {}},
423) {"global": {"key": DUMMY_KEY1_B64}, "services": {}},
424) {
425) "global": {"phrase": "abc"},
426) "services": {"sv": {"phrase": "my passphrase"}},
427) },
428) {
429) "global": {"phrase": "abc"},
430) "services": {"sv": {"key": DUMMY_KEY1_B64}},
431) },
432) {
433) "global": {"phrase": "abc"},
434) "services": {"sv": {"key": DUMMY_KEY1_B64, "length": 15}},
435) },
436) ],
437) )
438) CONNECTION_HINTS = pytest.mark.parametrize(
439) "conn_hint", ["none", "socket", "client"]
440) )
441) KEY_TO_PHRASE_SETTINGS = pytest.mark.parametrize(
442) [
443) "list_keys_action",
444) "address_action",
445) "system_support_action",
446) "sign_action",
447) "pattern",
448) ],
449) [
450) pytest.param(
451) ListKeysAction.EMPTY,
452) None,
453) None,
454) SignAction.FAIL,
455) "not loaded into the agent",
456) id="key-not-loaded",
457) ),
458) pytest.param(
459) ListKeysAction.FAIL,
460) None,
461) None,
462) SignAction.FAIL,
463) "SSH agent failed to or refused to",
464) id="list-keys-refused",
465) ),
466) pytest.param(
467) ListKeysAction.FAIL_RUNTIME,
468) None,
469) None,
470) SignAction.FAIL,
471) "SSH agent failed to or refused to",
472) id="list-keys-protocol-error",
473) ),
474) pytest.param(
475) None,
476) SocketAddressAction.UNSET_SSH_AUTH_SOCK,
477) None,
478) SignAction.FAIL,
479) "Cannot find any running SSH agent",
480) id="agent-address-missing",
481) ),
482) pytest.param(
483) None,
484) SocketAddressAction.MANGLE_SSH_AUTH_SOCK,
485) None,
486) SignAction.FAIL,
487) "Cannot connect to the SSH agent",
488) id="agent-address-mangled",
489) ),
490) pytest.param(
491) None,
492) None,
493) SystemSupportAction.UNSET_NATIVE,
494) SignAction.FAIL,
495) "does not support communicating with it",
496) id="no-agent-support",
497) ),
498) pytest.param(
499) None,
500) None,
501) SystemSupportAction.UNSET_PROVIDER_LIST,
502) SignAction.FAIL,
503) "does not support communicating with it",
504) id="no-agent-support",
505) ),
506) pytest.param(
507) None,
508) None,
509) SystemSupportAction.UNSET_AF_UNIX_AND_ENSURE_USE,
510) SignAction.FAIL,
511) "does not support communicating with it",
512) id="no-agent-support",
513) ),
514) pytest.param(
515) None,
516) None,
517) SystemSupportAction.UNSET_WINDLL_AND_ENSURE_USE,
518) SignAction.FAIL,
519) "does not support communicating with it",
520) id="no-agent-support",
521) ),
522) pytest.param(
523) None,
524) None,
525) None,
526) SignAction.FAIL_RUNTIME,
527) "violates the communication protocol",
528) id="sign-violates-protocol",
529) ),
530) ],
531) )
532) VALIDATION_FUNCTION_INPUT = pytest.mark.parametrize(
533) ["vfunc", "input"],
534) [
535) (cli_machinery.validate_occurrence_constraint, 20),
536) (cli_machinery.validate_length, 20),
537) ],
538) )
539)
540)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
541) class TestConfigLoadingSaving:
542) """Tests for the config loading and saving utility functions."""
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
543)
544) @Parametrize.BASE_CONFIG_VARIATIONS
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
545) def test_load(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
546) self,
547) config: Any,
548) ) -> None:
549) """[`cli_helpers.load_config`][] works for valid configurations."""
550) runner = machinery.CliRunner(mix_stderr=False)
551) # TODO(the-13th-letter): Rewrite using parenthesized
552) # with-statements.
553) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
554) with contextlib.ExitStack() as stack:
555) monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
556) stack.enter_context(
557) pytest_machinery.isolated_vault_config(
558) monkeypatch=monkeypatch,
559) runner=runner,
560) vault_config=config,
561) )
562) )
563) config_filename = cli_helpers.config_filename(subsystem="vault")
564) with config_filename.open(encoding="UTF-8") as fileobj:
565) assert json.load(fileobj) == config
566) assert cli_helpers.load_config() == config
567)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
568) def test_save_bad_config(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
569) self,
570) ) -> None:
571) """[`cli_helpers.save_config`][] fails for bad configurations."""
572) runner = machinery.CliRunner(mix_stderr=False)
573) # TODO(the-13th-letter): Rewrite using parenthesized
574) # with-statements.
575) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
576) with contextlib.ExitStack() as stack:
577) monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
578) stack.enter_context(
579) pytest_machinery.isolated_vault_config(
580) monkeypatch=monkeypatch,
581) runner=runner,
582) vault_config={},
583) )
584) )
585) stack.enter_context(
586) pytest.raises(ValueError, match="Invalid vault config")
587) )
588) cli_helpers.save_config(None) # type: ignore[arg-type]
589)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
590)
591) class TestPrompting:
592) """Tests for the prompting utility functions."""
593)
594) def test_selection_multiple(self) -> None:
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
595) """[`cli_helpers.prompt_for_selection`][] works in the "multiple" case."""
596)
597) @click.command()
598) @click.option("--heading", default="Our menu:")
599) @click.argument("items", nargs=-1)
600) def driver(heading: str, items: list[str]) -> None:
601) # from https://montypython.fandom.com/wiki/Spam#The_menu
602) items = items or [
603) "Egg and bacon",
604) "Egg, sausage and bacon",
605) "Egg and spam",
606) "Egg, bacon and spam",
607) "Egg, bacon, sausage and spam",
608) "Spam, bacon, sausage and spam",
609) "Spam, egg, spam, spam, bacon and spam",
610) "Spam, spam, spam, egg and spam",
611) (
612) "Spam, spam, spam, spam, spam, spam, baked beans, "
613) "spam, spam, spam and spam"
614) ),
615) (
616) "Lobster thermidor aux crevettes with a mornay sauce "
617) "garnished with truffle paté, brandy "
618) "and a fried egg on top and spam"
619) ),
620) ]
621) index = cli_helpers.prompt_for_selection(items, heading=heading)
622) click.echo("A fine choice: ", nl=False)
623) click.echo(items[index])
624) click.echo("(Note: Vikings strictly optional.)")
625)
626) runner = machinery.CliRunner(mix_stderr=True)
627) result = runner.invoke(driver, [], input="9")
628) assert result.clean_exit(
629) output="""\
630) Our menu:
631) [1] Egg and bacon
632) [2] Egg, sausage and bacon
633) [3] Egg and spam
634) [4] Egg, bacon and spam
635) [5] Egg, bacon, sausage and spam
636) [6] Spam, bacon, sausage and spam
637) [7] Spam, egg, spam, spam, bacon and spam
638) [8] Spam, spam, spam, egg and spam
639) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
640) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
641) Your selection? (1-10, leave empty to abort): 9
642) A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
643) (Note: Vikings strictly optional.)
644) """
645) ), "expected clean exit"
646) result = runner.invoke(
647) driver, ["--heading="], input="\n", catch_exceptions=True
648) )
649) assert result.error_exit(error=IndexError), (
650) "expected error exit and known error type"
651) )
652) assert (
653) result.stdout
654) == """\
655) [1] Egg and bacon
656) [2] Egg, sausage and bacon
657) [3] Egg and spam
658) [4] Egg, bacon and spam
659) [5] Egg, bacon, sausage and spam
660) [6] Spam, bacon, sausage and spam
661) [7] Spam, egg, spam, spam, bacon and spam
662) [8] Spam, spam, spam, egg and spam
663) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
664) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
665) Your selection? (1-10, leave empty to abort):\x20
666) """
667) ), "expected known output"
668) # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the
669) # click prompting machinery, meaning that the mixed output will
670) # incorrectly contain a line break, contrary to what the
671) # documentation for click.prompt prescribes.
672) result = runner.invoke(
673) driver, ["--heading="], input="", catch_exceptions=True
674) )
675) assert result.error_exit(error=IndexError), (
676) "expected error exit and known error type"
677) )
678) assert result.stdout in {
679) """\
680) [1] Egg and bacon
681) [2] Egg, sausage and bacon
682) [3] Egg and spam
683) [4] Egg, bacon and spam
684) [5] Egg, bacon, sausage and spam
685) [6] Spam, bacon, sausage and spam
686) [7] Spam, egg, spam, spam, bacon and spam
687) [8] Spam, spam, spam, egg and spam
688) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
689) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
690) Your selection? (1-10, leave empty to abort):\x20
691) """,
692) """\
693) [1] Egg and bacon
694) [2] Egg, sausage and bacon
695) [3] Egg and spam
696) [4] Egg, bacon and spam
697) [5] Egg, bacon, sausage and spam
698) [6] Spam, bacon, sausage and spam
699) [7] Spam, egg, spam, spam, bacon and spam
700) [8] Spam, spam, spam, egg and spam
701) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
702) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
703) Your selection? (1-10, leave empty to abort): """,
704) }, "expected known output"
705)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
706) def test_selection_single(self) -> None:
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
707) """[`cli_helpers.prompt_for_selection`][] works in the "single" case."""
708)
709) @click.command()
710) @click.option("--item", default="baked beans")
711) @click.argument("prompt")
712) def driver(item: str, prompt: str) -> None:
713) try:
714) cli_helpers.prompt_for_selection(
715) [item], heading="", single_choice_prompt=prompt
716) )
717) except IndexError:
718) click.echo("Boo.")
719) raise
720) else:
721) click.echo("Great!")
722)
723) runner = machinery.CliRunner(mix_stderr=True)
724) result = runner.invoke(
725) driver, ["Will replace with spam. Confirm, y/n?"], input="y"
726) )
727) assert result.clean_exit(
728) output="""\
729) [1] baked beans
730) Will replace with spam. Confirm, y/n? y
731) Great!
732) """
733) ), "expected clean exit"
734) result = runner.invoke(
735) driver,
736) ['Will replace with spam, okay? (Please say "y" or "n".)'],
737) input="\n",
738) )
739) assert result.error_exit(error=IndexError), (
740) "expected error exit and known error type"
741) )
742) assert (
743) result.stdout
744) == """\
745) [1] baked beans
746) Will replace with spam, okay? (Please say "y" or "n".):\x20
747) Boo.
748) """
749) ), "expected known output"
750) # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the
751) # click prompting machinery, meaning that the mixed output will
752) # incorrectly contain a line break, contrary to what the
753) # documentation for click.prompt prescribes.
754) result = runner.invoke(
755) driver,
756) ['Will replace with spam, okay? (Please say "y" or "n".)'],
757) input="",
758) )
759) assert result.error_exit(error=IndexError), (
760) "expected error exit and known error type"
761) )
762) assert result.stdout in {
763) """\
764) [1] baked beans
765) Will replace with spam, okay? (Please say "y" or "n".):\x20
766) Boo.
767) """,
768) """\
769) [1] baked beans
770) Will replace with spam, okay? (Please say "y" or "n".): Boo.
771) """,
772) }, "expected known output"
773)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
774) def test_passphrase(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
775) self,
776) ) -> None:
777) """[`cli_helpers.prompt_for_passphrase`][] works."""
778) with pytest.MonkeyPatch.context() as monkeypatch:
779) monkeypatch.setattr(
780) click,
781) "prompt",
782) lambda *a, **kw: json.dumps({"args": a, "kwargs": kw}),
783) )
784) res = json.loads(cli_helpers.prompt_for_passphrase())
785) err_msg = "missing arguments to passphrase prompt"
786) assert "args" in res, err_msg
787) assert "kwargs" in res, err_msg
788) assert res["args"][:1] == ["Passphrase"], err_msg
789) assert res["kwargs"].get("default") == "", err_msg
790) assert not res["kwargs"].get("show_default", True), err_msg
791) assert res["kwargs"].get("err"), err_msg
792) assert res["kwargs"].get("hide_input"), err_msg
793)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
794)
795) class TestLoggingMachinery:
796) """Tests for the logging utility functions."""
797)
798) def test_standard_logging_context_manager(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
799) self,
800) caplog: pytest.LogCaptureFixture,
801) capsys: pytest.CaptureFixture[str],
802) ) -> None:
803) """The standard logging context manager works.
804)
805) It registers its handlers, once, and emits formatted calls to
806) standard error prefixed with the program name.
807)
808) """
809) prog_name = cli_machinery.StandardCLILogging.prog_name
810) package_name = cli_machinery.StandardCLILogging.package_name
811) logger = logging.getLogger(package_name)
812) deprecation_logger = logging.getLogger(f"{package_name}.deprecation")
813) logging_cm = cli_machinery.StandardCLILogging.ensure_standard_logging()
814) with logging_cm:
815) assert (
816) sum(
817) 1
818) for h in logger.handlers
819) if h is cli_machinery.StandardCLILogging.cli_handler
820) )
821) == 1
822) )
823) logger.warning("message 1")
824) with logging_cm:
825) deprecation_logger.warning("message 2")
826) assert (
827) sum(
828) 1
829) for h in logger.handlers
830) if h is cli_machinery.StandardCLILogging.cli_handler
831) )
832) == 1
833) )
834) assert capsys.readouterr() == (
835) "",
836) (
837) f"{prog_name}: Warning: message 1\n"
838) f"{prog_name}: Deprecation warning: message 2\n"
839) ),
840) )
841) logger.warning("message 3")
842) assert (
843) sum(
844) 1
845) for h in logger.handlers
846) if h is cli_machinery.StandardCLILogging.cli_handler
847) )
848) == 1
849) )
850) assert capsys.readouterr() == (
851) "",
852) f"{prog_name}: Warning: message 3\n",
853) )
854) assert caplog.record_tuples == [
855) (package_name, logging.WARNING, "message 1"),
856) (f"{package_name}.deprecation", logging.WARNING, "message 2"),
857) (package_name, logging.WARNING, "message 3"),
858) ]
859)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
860) def test_warnings_context_manager(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
861) self,
862) caplog: pytest.LogCaptureFixture,
863) capsys: pytest.CaptureFixture[str],
864) ) -> None:
865) """The standard warnings logging context manager works.
866)
867) It registers its handlers, once, and emits formatted calls to
868) standard error prefixed with the program name. It also adheres
869) to the global warnings filter concerning which messages it
870) actually emits to standard error.
871)
872) """
873) warnings_cm = (
874) cli_machinery.StandardCLILogging.ensure_standard_warnings_logging()
875) )
876) THE_FUTURE = "the future will be here sooner than you think" # noqa: N806
877) JUST_TESTING = "just testing whether warnings work" # noqa: N806
878) with warnings_cm:
879) assert (
880) sum(
881) 1
882) for h in logging.getLogger("py.warnings").handlers
883) if h is cli_machinery.StandardCLILogging.warnings_handler
884) )
885) == 1
886) )
887) warnings.warn(UserWarning(JUST_TESTING), stacklevel=1)
888) with warnings_cm:
889) warnings.warn(FutureWarning(THE_FUTURE), stacklevel=1)
890) _out, err = capsys.readouterr()
891) err_lines = err.splitlines(True)
892) assert any(
893) f"UserWarning: {JUST_TESTING}" in line
894) for line in err_lines
895) )
896) assert any(
897) f"FutureWarning: {THE_FUTURE}" in line
898) for line in err_lines
899) )
900) warnings.warn(UserWarning(JUST_TESTING), stacklevel=1)
901) _out, err = capsys.readouterr()
902) err_lines = err.splitlines(True)
903) assert any(
904) f"UserWarning: {JUST_TESTING}" in line for line in err_lines
905) )
906) assert not any(
907) f"FutureWarning: {THE_FUTURE}" in line for line in err_lines
908) )
909) record_tuples = caplog.record_tuples
910) assert [tup[:2] for tup in record_tuples] == [
911) ("py.warnings", logging.WARNING),
912) ("py.warnings", logging.WARNING),
913) ("py.warnings", logging.WARNING),
914) ]
915) assert f"UserWarning: {JUST_TESTING}" in record_tuples[0][2]
916) assert f"FutureWarning: {THE_FUTURE}" in record_tuples[1][2]
917) assert f"UserWarning: {JUST_TESTING}" in record_tuples[2][2]
918)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
919)
920) class TestExportConfigAsShellScript:
921) """Tests the utility functions for exporting configs in `sh` format."""
922)
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
923) def export_as_sh_helper(
924) self,
925) config: Any,
926) ) -> None:
927) """Emits a config in sh(1) format, then reads it back to verify it.
928)
929) This function exports the configuration, sets up a new
930) enviroment, then calls
931) [`vault_config_exporter_shell_interpreter`][] on the export
932) script, verifying that each command ran successfully and that
933) the final configuration matches the initial one.
934)
935) Args:
936) config:
937) The configuration to emit and read back.
938)
939) """
940) prog_name_list = ("derivepassphrase", "vault")
941) with io.StringIO() as outfile:
942) cli_helpers.print_config_as_sh_script(
943) config, outfile=outfile, prog_name_list=prog_name_list
944) )
945) script = outfile.getvalue()
946) runner = machinery.CliRunner(mix_stderr=False)
947) # TODO(the-13th-letter): Rewrite using parenthesized
948) # with-statements.
949) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
950) with contextlib.ExitStack() as stack:
951) monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
952) stack.enter_context(
953) pytest_machinery.isolated_vault_config(
954) monkeypatch=monkeypatch,
955) runner=runner,
956) vault_config={"services": {}},
957) )
958) )
959) for result in vault_config_exporter_shell_interpreter(script):
960) assert result.clean_exit()
961) assert cli_helpers.load_config() == config
962)
963) @hypothesis.given(
964) global_config_settable=hypothesis_machinery.vault_full_service_config(),
965) global_config_importable=strategies.fixed_dictionaries(
966) {},
967) optional={
968) "key": strategies.text(
969) alphabet=strategies.characters(
970) min_codepoint=32,
971) max_codepoint=126,
972) ),
973) max_size=128,
974) ),
975) "phrase": strategies.text(
976) alphabet=strategies.characters(
977) min_codepoint=32,
978) max_codepoint=126,
979) ),
980) max_size=64,
981) ),
982) },
983) ),
984) )
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
985) def test_export_as_sh_global(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
986) self,
987) global_config_settable: _types.VaultConfigServicesSettings,
988) global_config_importable: _types.VaultConfigServicesSettings,
989) ) -> None:
990) """Exporting configurations as sh(1) script works.
991)
992) Here, we check global-only configurations which use both
993) settings settable via `--config` and settings requiring
994) `--import`.
995)
996) The actual verification is done by [`export_as_sh_helper`][].
997)
998) """
999) config: _types.VaultConfig = {
1000) "global": global_config_settable | global_config_importable,
1001) "services": {},
1002) }
1003) assert _types.clean_up_falsy_vault_config_values(config) is not None
1004) assert _types.is_vault_config(config)
1005) return self.export_as_sh_helper(config)
1006)
1007) @hypothesis.given(
1008) global_config_importable=strategies.fixed_dictionaries(
1009) {},
1010) optional={
1011) "key": strategies.text(
1012) alphabet=strategies.characters(
1013) min_codepoint=32,
1014) max_codepoint=126,
1015) ),
1016) max_size=128,
1017) ),
1018) "phrase": strategies.text(
1019) alphabet=strategies.characters(
1020) min_codepoint=32,
1021) max_codepoint=126,
1022) ),
1023) max_size=64,
1024) ),
1025) },
1026) ),
1027) )
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1028) def test_export_as_sh_global_only_imports(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1029) self,
1030) global_config_importable: _types.VaultConfigServicesSettings,
1031) ) -> None:
1032) """Exporting configurations as sh(1) script works.
1033)
1034) Here, we check global-only configurations which only use
1035) settings requiring `--import`.
1036)
1037) The actual verification is done by [`export_as_sh_helper`][].
1038)
1039) """
1040) config: _types.VaultConfig = {
1041) "global": global_config_importable,
1042) "services": {},
1043) }
1044) assert _types.clean_up_falsy_vault_config_values(config) is not None
1045) assert _types.is_vault_config(config)
1046) if not config["global"]:
1047) config.pop("global")
1048) return self.export_as_sh_helper(config)
1049)
1050) @hypothesis.given(
1051) service_name=strategies.text(
1052) alphabet=strategies.characters(
1053) min_codepoint=32,
1054) max_codepoint=126,
1055) ),
1056) min_size=4,
1057) max_size=64,
1058) ),
1059) service_config_settable=hypothesis_machinery.vault_full_service_config(),
1060) service_config_importable=strategies.fixed_dictionaries(
1061) {},
1062) optional={
1063) "key": strategies.text(
1064) alphabet=strategies.characters(
1065) min_codepoint=32,
1066) max_codepoint=126,
1067) ),
1068) max_size=128,
1069) ),
1070) "phrase": strategies.text(
1071) alphabet=strategies.characters(
1072) min_codepoint=32,
1073) max_codepoint=126,
1074) ),
1075) max_size=64,
1076) ),
1077) "notes": strategies.text(
1078) alphabet=strategies.characters(
1079) min_codepoint=32,
1080) max_codepoint=126,
1081) include_characters=("\n", "\f", "\t"),
1082) ),
1083) max_size=256,
1084) ),
1085) },
1086) ),
1087) )
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1088) def test_export_as_sh_service(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1089) self,
1090) service_name: str,
1091) service_config_settable: _types.VaultConfigServicesSettings,
1092) service_config_importable: _types.VaultConfigServicesSettings,
1093) ) -> None:
1094) """Exporting configurations as sh(1) script works.
1095)
1096) Here, we check service-only configurations which use both
1097) settings settable via `--config` and settings requiring
1098) `--import`.
1099)
1100) The actual verification is done by [`export_as_sh_helper`][].
1101)
1102) """
1103) config: _types.VaultConfig = {
1104) "services": {
1105) service_name: (
1106) service_config_settable | service_config_importable
1107) ),
1108) },
1109) }
1110) assert _types.clean_up_falsy_vault_config_values(config) is not None
1111) assert _types.is_vault_config(config)
1112) return self.export_as_sh_helper(config)
1113)
1114) @hypothesis.given(
1115) service_name=strategies.text(
1116) alphabet=strategies.characters(
1117) min_codepoint=32,
1118) max_codepoint=126,
1119) ),
1120) min_size=4,
1121) max_size=64,
1122) ),
1123) service_config_importable=strategies.fixed_dictionaries(
1124) {},
1125) optional={
1126) "key": strategies.text(
1127) alphabet=strategies.characters(
1128) min_codepoint=32,
1129) max_codepoint=126,
1130) ),
1131) max_size=128,
1132) ),
1133) "phrase": strategies.text(
1134) alphabet=strategies.characters(
1135) min_codepoint=32,
1136) max_codepoint=126,
1137) ),
1138) max_size=64,
1139) ),
1140) "notes": strategies.text(
1141) alphabet=strategies.characters(
1142) min_codepoint=32,
1143) max_codepoint=126,
1144) include_characters=("\n", "\f", "\t"),
1145) ),
1146) max_size=256,
1147) ),
1148) },
1149) ),
1150) )
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1151) def test_export_as_sh_service_only_imports(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1152) self,
1153) service_name: str,
1154) service_config_importable: _types.VaultConfigServicesSettings,
1155) ) -> None:
1156) """Exporting configurations as sh(1) script works.
1157)
1158) Here, we check service-only configurations which only use
1159) settings requiring `--import`.
1160)
1161) The actual verification is done by [`export_as_sh_helper`][].
1162)
1163) """
1164) config: _types.VaultConfig = {
1165) "services": {
1166) service_name: service_config_importable,
1167) },
1168) }
1169) assert _types.clean_up_falsy_vault_config_values(config) is not None
1170) assert _types.is_vault_config(config)
1171) return self.export_as_sh_helper(config)
1172)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1173)
1174) class TestTempdir:
1175) """Tests for the temporary directory handling utility functions."""
1176)
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1177) # The Annoying OS appears to silently truncate spaces at the end of
1178) # filenames.
1179) @hypothesis.given(
1180) env_var=strategies.sampled_from(["TMPDIR", "TEMP", "TMP"]),
1181) suffix=strategies.builds(
1182) operator.add,
1183) strategies.text(
1184) tuple(" 0123456789abcdefghijklmnopqrstuvwxyz"),
1185) min_size=11,
1186) max_size=11,
1187) ),
1188) strategies.text(
1189) tuple("0123456789abcdefghijklmnopqrstuvwxyz"),
1190) min_size=1,
1191) max_size=1,
1192) ),
1193) ),
1194) )
1195) @hypothesis.example(env_var="", suffix=".")
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1196) def test_get_tempdir(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1197) self,
1198) env_var: str,
1199) suffix: str,
1200) ) -> None:
1201) """[`cli_helpers.get_tempdir`][] returns a temporary directory.
1202)
1203) If it is not the same as the temporary directory determined by
1204) [`tempfile.gettempdir`][], then assert that
1205) `tempfile.gettempdir` returned the current directory and
1206) `cli_helpers.get_tempdir` returned the configuration directory.
1207)
1208) """
1209)
1210) @contextlib.contextmanager
1211) def make_temporary_directory(
1212) path: pathlib.Path,
1213) ) -> Iterator[pathlib.Path]:
1214) try:
1215) path.mkdir()
1216) yield path
1217) finally:
1218) shutil.rmtree(path)
1219)
1220) runner = machinery.CliRunner(mix_stderr=False)
1221) # TODO(the-13th-letter): Rewrite using parenthesized
1222) # with-statements.
1223) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1224) with contextlib.ExitStack() as stack:
1225) monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1226) stack.enter_context(
1227) pytest_machinery.isolated_vault_config(
1228) monkeypatch=monkeypatch,
1229) runner=runner,
1230) vault_config={"services": {}},
1231) )
1232) )
1233) old_tempdir = os.fsdecode(tempfile.gettempdir())
1234) monkeypatch.delenv("TMPDIR", raising=False)
1235) monkeypatch.delenv("TEMP", raising=False)
1236) monkeypatch.delenv("TMP", raising=False)
1237) monkeypatch.setattr(tempfile, "tempdir", None)
1238) temp_path = pathlib.Path.cwd() / suffix
1239) if env_var:
1240) monkeypatch.setenv(env_var, os.fsdecode(temp_path))
1241) stack.enter_context(make_temporary_directory(temp_path))
1242) new_tempdir = os.fsdecode(tempfile.gettempdir())
1243) hypothesis.assume(
1244) temp_path.resolve() == pathlib.Path.cwd().resolve()
1245) or old_tempdir != new_tempdir
1246) )
1247) system_tempdir = os.fsdecode(tempfile.gettempdir())
1248) our_tempdir = cli_helpers.get_tempdir()
1249) assert system_tempdir == os.fsdecode(our_tempdir) or (
1250) # TODO(the-13th-letter): `pytest_machinery.isolated_config`
1251) # guarantees that `Path.cwd() == config_filename(None)`.
1252) # So this sub-branch ought to never trigger in our
1253) # tests.
1254) system_tempdir == os.getcwd() # noqa: PTH109
1255) and our_tempdir == cli_helpers.config_filename(subsystem=None)
1256) )
1257) assert not temp_path.exists(), f"temp path {temp_path} not cleaned up!"
1258)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1259) def test_get_tempdir_force_default(self) -> None:
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1260) """[`cli_helpers.get_tempdir`][] returns a temporary directory.
1261)
1262) If all candidates are mocked to fail for the standard temporary
1263) directory choices, then we return the `derivepassphrase`
1264) configuration directory.
1265)
1266) """
1267) runner = machinery.CliRunner(mix_stderr=False)
1268) # TODO(the-13th-letter): Rewrite using parenthesized
1269) # with-statements.
1270) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1271) with contextlib.ExitStack() as stack:
1272) monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1273) stack.enter_context(
1274) pytest_machinery.isolated_vault_config(
1275) monkeypatch=monkeypatch,
1276) runner=runner,
1277) vault_config={"services": {}},
1278) )
1279) )
1280) monkeypatch.delenv("TMPDIR", raising=False)
1281) monkeypatch.delenv("TEMP", raising=False)
1282) monkeypatch.delenv("TMP", raising=False)
1283) config_dir = cli_helpers.config_filename(subsystem=None)
1284)
1285) def is_dir_false(
1286) self: pathlib.Path,
1287) /,
1288) *,
1289) follow_symlinks: bool = False,
1290) ) -> bool:
1291) del self, follow_symlinks
1292) return False
1293)
1294) def is_dir_error(
1295) self: pathlib.Path,
1296) /,
1297) *,
1298) follow_symlinks: bool = False,
1299) ) -> bool:
1300) del follow_symlinks
1301) raise OSError(
1302) errno.EACCES,
1303) os.strerror(errno.EACCES),
1304) str(self),
1305) )
1306)
1307) monkeypatch.setattr(pathlib.Path, "is_dir", is_dir_false)
1308) assert cli_helpers.get_tempdir() == config_dir
1309)
1310) monkeypatch.setattr(pathlib.Path, "is_dir", is_dir_error)
1311) assert cli_helpers.get_tempdir() == config_dir
1312)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1313)
1314) # TODO(the-13th-letter): Use a better class name, or consider keeping them
1315) # as top-level functions.
1316) class TestMisc:
1317) """Miscellaneous tests for the command-line utility functions."""
1318)
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1319) @Parametrize.DELETE_CONFIG_INPUT
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1320) def test_repeated_config_deletion(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1321) self,
1322) command_line: list[str],
1323) config: _types.VaultConfig,
1324) result_config: _types.VaultConfig,
1325) ) -> None:
1326) """Repeatedly removing the same parts of a configuration works."""
1327) for start_config in [config, result_config]:
1328) runner = machinery.CliRunner(mix_stderr=False)
1329) # TODO(the-13th-letter): Rewrite using parenthesized
1330) # with-statements.
1331) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1332) with contextlib.ExitStack() as stack:
1333) monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1334) stack.enter_context(
1335) pytest_machinery.isolated_vault_config(
1336) monkeypatch=monkeypatch,
1337) runner=runner,
1338) vault_config=start_config,
1339) )
1340) )
1341) result = runner.invoke(
1342) cli.derivepassphrase_vault,
1343) command_line,
1344) catch_exceptions=False,
1345) )
1346) assert result.clean_exit(empty_stderr=True), (
1347) "expected clean exit"
1348) )
1349) with cli_helpers.config_filename(subsystem="vault").open(
1350) encoding="UTF-8"
1351) ) as infile:
1352) config_readback = json.load(infile)
1353) assert config_readback == result_config
1354)
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1355) def test_phrase_from_key_manually(self) -> None:
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1356) """The dummy service, key and config settings are consistent."""
1357) assert (
1358) vault.Vault(
|
Fix typing of the dummy vau...
Marco Ricci authored 2 months ago
|
1359) phrase=DUMMY_PHRASE_FROM_KEY1,
1360) **DUMMY_CONFIG_SETTINGS_AS_CONSTRUCTOR_ARGS,
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1361) ).generate(DUMMY_SERVICE)
1362) == DUMMY_RESULT_KEY1
1363) )
1364)
1365) @Parametrize.VALIDATION_FUNCTION_INPUT
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1366) def test_validate_constraints_manually(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1367) self,
1368) vfunc: Callable[[click.Context, click.Parameter, Any], int | None],
1369) input: int,
1370) ) -> None:
1371) """Command-line argument constraint validation works."""
1372) ctx = cli.derivepassphrase_vault.make_context(cli.PROG_NAME, [])
1373) param = cli.derivepassphrase_vault.params[0]
1374) assert vfunc(ctx, param, input) == input
1375)
1376) @Parametrize.CONNECTION_HINTS
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1377) def test_get_suitable_ssh_keys(
|
Split the CLI tests into on...
Marco Ricci authored 5 months ago
|
1378) self,
1379) running_ssh_agent: data.RunningSSHAgentInfo,
1380) conn_hint: str,
1381) ) -> None:
1382) """[`cli_helpers.get_suitable_ssh_keys`][] works."""
1383) with pytest.MonkeyPatch.context() as monkeypatch:
1384) monkeypatch.setattr(
1385) ssh_agent.SSHAgentClient,
1386) "list_keys",
1387) callables.list_keys,
1388) )
1389) hint: ssh_agent.SSHAgentClient | _types.SSHAgentSocket | None
1390) # TODO(the-13th-letter): Rewrite using structural pattern
1391) # matching.
1392) # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1393) if conn_hint == "client":
1394) hint = ssh_agent.SSHAgentClient()
1395) elif conn_hint == "socket":
1396) if isinstance(
1397) running_ssh_agent.socket, str
1398) ): # pragma: no cover
1399) if not hasattr(socket, "AF_UNIX"):
1400) pytest.skip("socket module does not support AF_UNIX")
1401) # socket.AF_UNIX is not defined everywhere.
1402) hint = socket.socket(family=socket.AF_UNIX) # type: ignore[attr-defined]
1403) hint.connect(running_ssh_agent.socket)
1404) else: # pragma: no cover
1405) hint = running_ssh_agent.socket()
1406) else:
1407) assert conn_hint == "none"
1408) hint = None
1409) exception: Exception | None = None
1410) try:
1411) list(cli_helpers.get_suitable_ssh_keys(hint))
1412) except RuntimeError: # pragma: no cover
1413) pass
1414) except Exception as e: # noqa: BLE001 # pragma: no cover
1415) exception = e
1416) finally:
1417) assert exception is None, (
1418) "exception querying suitable SSH keys"
1419) )
1420)
1421) @Parametrize.KEY_TO_PHRASE_SETTINGS
|
Regroup the CLI and SSH age...
Marco Ricci authored 5 months ago
|
1422) def test_key_to_phrase(
|