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