Marco Ricci commited on 2024-12-21 00:23:33
Zeige 3 geänderte Dateien mit 411 Einfügungen und 8 Löschungen.
When exporting `vault` configurations via `--export`, a new option `--export-as=FORMAT` allows selecting the export format (defaulting to `json`). Setting `FORMAT` as `sh` selects the new POSIX shell script export format, which yields a sh(1)-compatible script to reimport the existing configuration as a series of calls to the `derivepassphrase` command. Because the output is very regular, the test suite also includes an interpreter specifically for the sh(1) subset emitted during an `sh` export, on top of the usual adaptations to existing tests for the new functionality.
... | ... |
@@ -18,6 +18,7 @@ import inspect |
18 | 18 |
import json |
19 | 19 |
import logging |
20 | 20 |
import os |
21 |
+import shlex |
|
21 | 22 |
import sys |
22 | 23 |
import unicodedata |
23 | 24 |
import warnings |
... | ... |
@@ -1361,6 +1362,78 @@ def _key_to_phrase( |
1361 | 1362 |
error_callback('Cannot connect to SSH agent: %s', e.strerror) |
1362 | 1363 |
|
1363 | 1364 |
|
1365 |
+def _print_config_as_sh_script( |
|
1366 |
+ config: _types.VaultConfig, |
|
1367 |
+ /, |
|
1368 |
+ *, |
|
1369 |
+ outfile: TextIO, |
|
1370 |
+ prog_name_list: Sequence[str], |
|
1371 |
+) -> None: |
|
1372 |
+ service_keys = ( |
|
1373 |
+ 'length', |
|
1374 |
+ 'repeat', |
|
1375 |
+ 'lower', |
|
1376 |
+ 'upper', |
|
1377 |
+ 'number', |
|
1378 |
+ 'space', |
|
1379 |
+ 'dash', |
|
1380 |
+ 'symbol', |
|
1381 |
+ ) |
|
1382 |
+ print('#!/bin/sh -e', file=outfile) |
|
1383 |
+ print(file=outfile) |
|
1384 |
+ print(shlex.join([*prog_name_list, '--clear']), file=outfile) |
|
1385 |
+ sv_obj_pairs: list[ |
|
1386 |
+ tuple[ |
|
1387 |
+ str | None, |
|
1388 |
+ _types.VaultConfigGlobalSettings |
|
1389 |
+ | _types.VaultConfigServicesSettings, |
|
1390 |
+ ], |
|
1391 |
+ ] = list(config['services'].items()) |
|
1392 |
+ if config.get('global', {}): |
|
1393 |
+ sv_obj_pairs.insert(0, (None, config['global'])) |
|
1394 |
+ for sv, sv_obj in sv_obj_pairs: |
|
1395 |
+ this_service_keys = tuple(k for k in service_keys if k in sv_obj) |
|
1396 |
+ this_other_keys = tuple(k for k in sv_obj if k not in service_keys) |
|
1397 |
+ if this_other_keys: |
|
1398 |
+ other_sv_obj = {k: sv_obj[k] for k in this_other_keys} # type: ignore[literal-required] |
|
1399 |
+ dumped_config = json.dumps( |
|
1400 |
+ ( |
|
1401 |
+ {'services': {sv: other_sv_obj}} |
|
1402 |
+ if sv is not None |
|
1403 |
+ else {'global': other_sv_obj, 'services': {}} |
|
1404 |
+ ), |
|
1405 |
+ ensure_ascii=False, |
|
1406 |
+ indent=None, |
|
1407 |
+ ) |
|
1408 |
+ print( |
|
1409 |
+ shlex.join([*prog_name_list, '--import', '-']) + " <<'HERE'", |
|
1410 |
+ dumped_config, |
|
1411 |
+ 'HERE', |
|
1412 |
+ sep='\n', |
|
1413 |
+ file=outfile, |
|
1414 |
+ ) |
|
1415 |
+ if not this_service_keys and not this_other_keys and sv: |
|
1416 |
+ dumped_config = json.dumps( |
|
1417 |
+ {'services': {sv: {}}}, |
|
1418 |
+ ensure_ascii=False, |
|
1419 |
+ indent=None, |
|
1420 |
+ ) |
|
1421 |
+ print( |
|
1422 |
+ shlex.join([*prog_name_list, '--import', '-']) + " <<'HERE'", |
|
1423 |
+ dumped_config, |
|
1424 |
+ 'HERE', |
|
1425 |
+ sep='\n', |
|
1426 |
+ file=outfile, |
|
1427 |
+ ) |
|
1428 |
+ elif this_service_keys: |
|
1429 |
+ tokens = [*prog_name_list, '--config'] |
|
1430 |
+ for key in this_service_keys: |
|
1431 |
+ tokens.extend([f'--{key}', str(sv_obj[key])]) # type: ignore[literal-required] |
|
1432 |
+ if sv is not None: |
|
1433 |
+ tokens.extend(['--', sv]) |
|
1434 |
+ print(shlex.join(tokens), file=outfile) |
|
1435 |
+ |
|
1436 |
+ |
|
1364 | 1437 |
# Concrete option groups used by this command-line interface. |
1365 | 1438 |
class PasswordGenerationOption(OptionGroupOption): |
1366 | 1439 |
"""Password generation options for the CLI.""" |
... | ... |
@@ -1657,6 +1730,13 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1657 | 1730 |
), |
1658 | 1731 |
cls=CompatibilityOption, |
1659 | 1732 |
) |
1733 |
+@click.option( |
|
1734 |
+ '--export-as', |
|
1735 |
+ type=click.Choice(['JSON', 'sh']), |
|
1736 |
+ default='JSON', |
|
1737 |
+ help='when exporting, export as JSON (default) or POSIX sh', |
|
1738 |
+ cls=CompatibilityOption, |
|
1739 |
+) |
|
1660 | 1740 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
1661 | 1741 |
@standard_logging_options |
1662 | 1742 |
@click.argument('service', required=False) |
... | ... |
@@ -1685,6 +1765,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1685 | 1765 |
import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
1686 | 1766 |
overwrite_config: bool = False, |
1687 | 1767 |
unset_settings: Sequence[str] = (), |
1768 |
+ export_as: Literal['json', 'sh'] = 'json', |
|
1688 | 1769 |
) -> None: |
1689 | 1770 |
"""Derive a passphrase using the vault(1) derivation scheme. |
1690 | 1771 |
|
... | ... |
@@ -1785,6 +1866,10 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1785 | 1866 |
Command-line argument `--unset`. If given together with |
1786 | 1867 |
`--config`, unsets the specified settings (in addition to |
1787 | 1868 |
any other changes requested). |
1869 |
+ export_as: |
|
1870 |
+ Command-line argument `--export-as`. If given together with |
|
1871 |
+ `--export`, selects the format to export the current |
|
1872 |
+ configuration as: JSON ("json", default) or POSIX sh ("sh"). |
|
1788 | 1873 |
|
1789 | 1874 |
""" # noqa: D301 |
1790 | 1875 |
logger = logging.getLogger(PROG_NAME) |
... | ... |
@@ -2120,6 +2205,23 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2120 | 2205 |
# and for programmatic use, our caller may want accurate |
2121 | 2206 |
# error information. |
2122 | 2207 |
with outfile: |
2208 |
+ if export_as == 'sh': |
|
2209 |
+ this_ctx = ctx |
|
2210 |
+ prog_name_pieces = collections.deque([ |
|
2211 |
+ this_ctx.info_name or 'vault', |
|
2212 |
+ ]) |
|
2213 |
+ while ( |
|
2214 |
+ this_ctx.parent is not None |
|
2215 |
+ and this_ctx.parent.info_name is not None |
|
2216 |
+ ): |
|
2217 |
+ prog_name_pieces.appendleft(this_ctx.parent.info_name) |
|
2218 |
+ this_ctx = this_ctx.parent |
|
2219 |
+ _print_config_as_sh_script( |
|
2220 |
+ configuration, |
|
2221 |
+ outfile=outfile, |
|
2222 |
+ prog_name_list=prog_name_pieces, |
|
2223 |
+ ) |
|
2224 |
+ else: |
|
2123 | 2225 |
json.dump(configuration, outfile) |
2124 | 2226 |
except OSError as e: |
2125 | 2227 |
err('Cannot store config: %s: %r', e.strerror, e.filename) |
... | ... |
@@ -8,14 +8,16 @@ import base64 |
8 | 8 |
import contextlib |
9 | 9 |
import copy |
10 | 10 |
import errno |
11 |
+import io |
|
11 | 12 |
import json |
12 | 13 |
import logging |
13 | 14 |
import os |
15 |
+import shlex |
|
14 | 16 |
import shutil |
15 | 17 |
import socket |
16 | 18 |
import textwrap |
17 | 19 |
import warnings |
18 |
-from typing import TYPE_CHECKING, NoReturn |
|
20 |
+from typing import TYPE_CHECKING |
|
19 | 21 |
|
20 | 22 |
import click.testing |
21 | 23 |
import hypothesis |
... | ... |
@@ -27,7 +29,8 @@ import tests |
27 | 29 |
from derivepassphrase import _types, cli, ssh_agent, vault |
28 | 30 |
|
29 | 31 |
if TYPE_CHECKING: |
30 |
- from collections.abc import Callable, Iterable |
|
32 |
+ from collections.abc import Callable, Iterable, Iterator |
|
33 |
+ from typing import NoReturn |
|
31 | 34 |
|
32 | 35 |
DUMMY_SERVICE = tests.DUMMY_SERVICE |
33 | 36 |
DUMMY_PASSPHRASE = tests.DUMMY_PASSPHRASE |
... | ... |
@@ -227,6 +230,56 @@ def is_harmless_config_import_warning(record: tuple[str, int, str]) -> bool: |
227 | 230 |
return any(tests.warning_emitted(w, [record]) for w in possible_warnings) |
228 | 231 |
|
229 | 232 |
|
233 |
+def vault_config_exporter_shell_interpreter( # noqa: C901 |
|
234 |
+ script: str | Iterable[str], |
|
235 |
+ /, |
|
236 |
+ *, |
|
237 |
+ prog_name_list: list[str] | None = None, |
|
238 |
+ command: click.BaseCommand | None = None, |
|
239 |
+ runner: click.testing.CliRunner | None = None, |
|
240 |
+) -> Iterator[click.testing.Result]: |
|
241 |
+ if isinstance(script, str): # pragma: no cover |
|
242 |
+ script = script.splitlines(False) |
|
243 |
+ if prog_name_list is None: # pragma: no cover |
|
244 |
+ prog_name_list = ['derivepassphrase', 'vault'] |
|
245 |
+ if command is None: # pragma: no cover |
|
246 |
+ command = cli.derivepassphrase_vault |
|
247 |
+ if runner is None: # pragma: no cover |
|
248 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
249 |
+ n = len(prog_name_list) |
|
250 |
+ it = iter(script) |
|
251 |
+ while True: |
|
252 |
+ try: |
|
253 |
+ raw_line = next(it) |
|
254 |
+ except StopIteration: |
|
255 |
+ break |
|
256 |
+ else: |
|
257 |
+ line = shlex.split(raw_line) |
|
258 |
+ input_buffer: list[str] = [] |
|
259 |
+ if line[:n] != prog_name_list: |
|
260 |
+ continue |
|
261 |
+ line[:n] = [] |
|
262 |
+ if line and line[-1] == '<<HERE': |
|
263 |
+ # naive HERE document support |
|
264 |
+ while True: |
|
265 |
+ try: |
|
266 |
+ raw_line = next(it) |
|
267 |
+ except StopIteration as exc: # pragma: no cover |
|
268 |
+ msg = 'incomplete here document' |
|
269 |
+ raise EOFError(msg) from exc |
|
270 |
+ else: |
|
271 |
+ if raw_line == 'HERE': |
|
272 |
+ break |
|
273 |
+ input_buffer.append(raw_line) |
|
274 |
+ line.pop() |
|
275 |
+ yield runner.invoke( |
|
276 |
+ command, |
|
277 |
+ line, |
|
278 |
+ catch_exceptions=False, |
|
279 |
+ input=(''.join(x + '\n' for x in input_buffer) or None), |
|
280 |
+ ) |
|
281 |
+ |
|
282 |
+ |
|
230 | 283 |
class TestCLI: |
231 | 284 |
def test_200_help_output(self, monkeypatch: pytest.MonkeyPatch) -> None: |
232 | 285 |
runner = click.testing.CliRunner(mix_stderr=False) |
... | ... |
@@ -866,25 +919,45 @@ class TestCLI: |
866 | 919 |
error=os.strerror(errno.EISDIR) |
867 | 920 |
), 'expected error exit and known error message' |
868 | 921 |
|
922 |
+ @pytest.mark.parametrize( |
|
923 |
+ 'export_options', |
|
924 |
+ [ |
|
925 |
+ [], |
|
926 |
+ ['--export-as=sh'], |
|
927 |
+ ], |
|
928 |
+ ) |
|
869 | 929 |
def test_214_export_settings_no_stored_settings( |
870 | 930 |
self, |
871 | 931 |
monkeypatch: pytest.MonkeyPatch, |
932 |
+ export_options: list[str], |
|
872 | 933 |
) -> None: |
873 | 934 |
runner = click.testing.CliRunner(mix_stderr=False) |
874 | 935 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
875 | 936 |
with contextlib.suppress(FileNotFoundError): |
876 | 937 |
os.remove(cli._config_filename(subsystem='vault')) |
877 | 938 |
_result = runner.invoke( |
878 |
- cli.derivepassphrase_vault, |
|
879 |
- ['--export', '-'], |
|
939 |
+ # Test parent context navigation by not calling |
|
940 |
+ # `cli.derivepassphrase_vault` directly. Used e.g. in |
|
941 |
+ # the `--export-as=sh` section to autoconstruct the |
|
942 |
+ # program name correctly. |
|
943 |
+ cli.derivepassphrase, |
|
944 |
+ ['vault', '--export', '-', *export_options], |
|
880 | 945 |
catch_exceptions=False, |
881 | 946 |
) |
882 | 947 |
result = tests.ReadableResult.parse(_result) |
883 | 948 |
assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
884 | 949 |
|
950 |
+ @pytest.mark.parametrize( |
|
951 |
+ 'export_options', |
|
952 |
+ [ |
|
953 |
+ [], |
|
954 |
+ ['--export-as=sh'], |
|
955 |
+ ], |
|
956 |
+ ) |
|
885 | 957 |
def test_214a_export_settings_bad_stored_config( |
886 | 958 |
self, |
887 | 959 |
monkeypatch: pytest.MonkeyPatch, |
960 |
+ export_options: list[str], |
|
888 | 961 |
) -> None: |
889 | 962 |
runner = click.testing.CliRunner(mix_stderr=False) |
890 | 963 |
with tests.isolated_vault_config( |
... | ... |
@@ -892,7 +965,7 @@ class TestCLI: |
892 | 965 |
): |
893 | 966 |
_result = runner.invoke( |
894 | 967 |
cli.derivepassphrase_vault, |
895 |
- ['--export', '-'], |
|
968 |
+ ['--export', '-', *export_options], |
|
896 | 969 |
input='null', |
897 | 970 |
catch_exceptions=False, |
898 | 971 |
) |
... | ... |
@@ -901,9 +974,17 @@ class TestCLI: |
901 | 974 |
error='Cannot load config' |
902 | 975 |
), 'expected error exit and known error message' |
903 | 976 |
|
977 |
+ @pytest.mark.parametrize( |
|
978 |
+ 'export_options', |
|
979 |
+ [ |
|
980 |
+ [], |
|
981 |
+ ['--export-as=sh'], |
|
982 |
+ ], |
|
983 |
+ ) |
|
904 | 984 |
def test_214b_export_settings_not_a_file( |
905 | 985 |
self, |
906 | 986 |
monkeypatch: pytest.MonkeyPatch, |
987 |
+ export_options: list[str], |
|
907 | 988 |
) -> None: |
908 | 989 |
runner = click.testing.CliRunner(mix_stderr=False) |
909 | 990 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
... | ... |
@@ -912,7 +993,7 @@ class TestCLI: |
912 | 993 |
os.makedirs(cli._config_filename(subsystem='vault')) |
913 | 994 |
_result = runner.invoke( |
914 | 995 |
cli.derivepassphrase_vault, |
915 |
- ['--export', '-'], |
|
996 |
+ ['--export', '-', *export_options], |
|
916 | 997 |
input='null', |
917 | 998 |
catch_exceptions=False, |
918 | 999 |
) |
... | ... |
@@ -921,16 +1002,24 @@ class TestCLI: |
921 | 1002 |
error='Cannot load config' |
922 | 1003 |
), 'expected error exit and known error message' |
923 | 1004 |
|
1005 |
+ @pytest.mark.parametrize( |
|
1006 |
+ 'export_options', |
|
1007 |
+ [ |
|
1008 |
+ [], |
|
1009 |
+ ['--export-as=sh'], |
|
1010 |
+ ], |
|
1011 |
+ ) |
|
924 | 1012 |
def test_214c_export_settings_target_not_a_file( |
925 | 1013 |
self, |
926 | 1014 |
monkeypatch: pytest.MonkeyPatch, |
1015 |
+ export_options: list[str], |
|
927 | 1016 |
) -> None: |
928 | 1017 |
runner = click.testing.CliRunner(mix_stderr=False) |
929 | 1018 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
930 | 1019 |
dname = cli._config_filename(subsystem=None) |
931 | 1020 |
_result = runner.invoke( |
932 | 1021 |
cli.derivepassphrase_vault, |
933 |
- ['--export', os.fsdecode(dname)], |
|
1022 |
+ ['--export', os.fsdecode(dname), *export_options], |
|
934 | 1023 |
input='null', |
935 | 1024 |
catch_exceptions=False, |
936 | 1025 |
) |
... | ... |
@@ -939,9 +1028,17 @@ class TestCLI: |
939 | 1028 |
error='Cannot store config' |
940 | 1029 |
), 'expected error exit and known error message' |
941 | 1030 |
|
1031 |
+ @pytest.mark.parametrize( |
|
1032 |
+ 'export_options', |
|
1033 |
+ [ |
|
1034 |
+ [], |
|
1035 |
+ ['--export-as=sh'], |
|
1036 |
+ ], |
|
1037 |
+ ) |
|
942 | 1038 |
def test_214d_export_settings_settings_directory_not_a_directory( |
943 | 1039 |
self, |
944 | 1040 |
monkeypatch: pytest.MonkeyPatch, |
1041 |
+ export_options: list[str], |
|
945 | 1042 |
) -> None: |
946 | 1043 |
runner = click.testing.CliRunner(mix_stderr=False) |
947 | 1044 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
... | ... |
@@ -951,7 +1048,7 @@ class TestCLI: |
951 | 1048 |
print('Obstruction!!', file=outfile) |
952 | 1049 |
_result = runner.invoke( |
953 | 1050 |
cli.derivepassphrase_vault, |
954 |
- ['--export', '-'], |
|
1051 |
+ ['--export', '-', *export_options], |
|
955 | 1052 |
input='null', |
956 | 1053 |
catch_exceptions=False, |
957 | 1054 |
) |
... | ... |
@@ -2050,6 +2147,204 @@ Boo. |
2050 | 2147 |
assert f'FutureWarning: {THE_FUTURE}' in record_tuples[1][2] |
2051 | 2148 |
assert f'UserWarning: {JUST_TESTING}' in record_tuples[2][2] |
2052 | 2149 |
|
2150 |
+ def _export_as_sh_helper( |
|
2151 |
+ self, |
|
2152 |
+ config: Any, |
|
2153 |
+ ) -> None: |
|
2154 |
+ prog_name_list = ('derivepassphrase', 'vault') |
|
2155 |
+ with io.StringIO() as outfile: |
|
2156 |
+ cli._print_config_as_sh_script( |
|
2157 |
+ config, outfile=outfile, prog_name_list=prog_name_list |
|
2158 |
+ ) |
|
2159 |
+ script = outfile.getvalue() |
|
2160 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
2161 |
+ monkeypatch = pytest.MonkeyPatch() |
|
2162 |
+ with tests.isolated_vault_config( |
|
2163 |
+ runner=runner, |
|
2164 |
+ monkeypatch=monkeypatch, |
|
2165 |
+ vault_config={'services': {}}, |
|
2166 |
+ ): |
|
2167 |
+ for _result in vault_config_exporter_shell_interpreter(script): |
|
2168 |
+ result = tests.ReadableResult.parse(_result) |
|
2169 |
+ assert result.clean_exit() |
|
2170 |
+ assert cli._load_config() == config |
|
2171 |
+ |
|
2172 |
+ @hypothesis.given( |
|
2173 |
+ global_config_settable=tests.vault_full_service_config(), |
|
2174 |
+ global_config_importable=strategies.fixed_dictionaries( |
|
2175 |
+ {}, |
|
2176 |
+ optional={ |
|
2177 |
+ 'key': strategies.text( |
|
2178 |
+ alphabet=strategies.characters( |
|
2179 |
+ min_codepoint=32, |
|
2180 |
+ max_codepoint=126, |
|
2181 |
+ ), |
|
2182 |
+ max_size=128, |
|
2183 |
+ ), |
|
2184 |
+ 'phrase': strategies.text( |
|
2185 |
+ alphabet=strategies.characters( |
|
2186 |
+ min_codepoint=32, |
|
2187 |
+ max_codepoint=126, |
|
2188 |
+ ), |
|
2189 |
+ max_size=64, |
|
2190 |
+ ), |
|
2191 |
+ }, |
|
2192 |
+ ), |
|
2193 |
+ ) |
|
2194 |
+ def test_130a_export_as_sh_global( |
|
2195 |
+ self, |
|
2196 |
+ global_config_settable: _types.VaultConfigServicesSettings, |
|
2197 |
+ global_config_importable: _types.VaultConfigServicesSettings, |
|
2198 |
+ ) -> None: |
|
2199 |
+ config: _types.VaultConfig = { |
|
2200 |
+ 'global': global_config_settable | global_config_importable, |
|
2201 |
+ 'services': {}, |
|
2202 |
+ } |
|
2203 |
+ assert _types.clean_up_falsy_vault_config_values(config) is not None |
|
2204 |
+ assert _types.is_vault_config(config) |
|
2205 |
+ return self._export_as_sh_helper(config) |
|
2206 |
+ |
|
2207 |
+ @hypothesis.given( |
|
2208 |
+ global_config_importable=strategies.fixed_dictionaries( |
|
2209 |
+ {}, |
|
2210 |
+ optional={ |
|
2211 |
+ 'key': strategies.text( |
|
2212 |
+ alphabet=strategies.characters( |
|
2213 |
+ min_codepoint=32, |
|
2214 |
+ max_codepoint=126, |
|
2215 |
+ ), |
|
2216 |
+ max_size=128, |
|
2217 |
+ ), |
|
2218 |
+ 'phrase': strategies.text( |
|
2219 |
+ alphabet=strategies.characters( |
|
2220 |
+ min_codepoint=32, |
|
2221 |
+ max_codepoint=126, |
|
2222 |
+ ), |
|
2223 |
+ max_size=64, |
|
2224 |
+ ), |
|
2225 |
+ }, |
|
2226 |
+ ), |
|
2227 |
+ ) |
|
2228 |
+ def test_130b_export_as_sh_global_only_imports( |
|
2229 |
+ self, |
|
2230 |
+ global_config_importable: _types.VaultConfigServicesSettings, |
|
2231 |
+ ) -> None: |
|
2232 |
+ config: _types.VaultConfig = { |
|
2233 |
+ 'global': global_config_importable, |
|
2234 |
+ 'services': {}, |
|
2235 |
+ } |
|
2236 |
+ assert _types.clean_up_falsy_vault_config_values(config) is not None |
|
2237 |
+ assert _types.is_vault_config(config) |
|
2238 |
+ if not config['global']: |
|
2239 |
+ config.pop('global') |
|
2240 |
+ return self._export_as_sh_helper(config) |
|
2241 |
+ |
|
2242 |
+ @hypothesis.given( |
|
2243 |
+ service_name=strategies.text( |
|
2244 |
+ alphabet=strategies.characters( |
|
2245 |
+ min_codepoint=32, |
|
2246 |
+ max_codepoint=126, |
|
2247 |
+ ), |
|
2248 |
+ min_size=4, |
|
2249 |
+ max_size=64, |
|
2250 |
+ ), |
|
2251 |
+ service_config_settable=tests.vault_full_service_config(), |
|
2252 |
+ service_config_importable=strategies.fixed_dictionaries( |
|
2253 |
+ {}, |
|
2254 |
+ optional={ |
|
2255 |
+ 'key': strategies.text( |
|
2256 |
+ alphabet=strategies.characters( |
|
2257 |
+ min_codepoint=32, |
|
2258 |
+ max_codepoint=126, |
|
2259 |
+ ), |
|
2260 |
+ max_size=128, |
|
2261 |
+ ), |
|
2262 |
+ 'phrase': strategies.text( |
|
2263 |
+ alphabet=strategies.characters( |
|
2264 |
+ min_codepoint=32, |
|
2265 |
+ max_codepoint=126, |
|
2266 |
+ ), |
|
2267 |
+ max_size=64, |
|
2268 |
+ ), |
|
2269 |
+ 'notes': strategies.text( |
|
2270 |
+ alphabet=strategies.characters( |
|
2271 |
+ min_codepoint=32, |
|
2272 |
+ max_codepoint=126, |
|
2273 |
+ include_characters=('\n', '\f', '\t'), |
|
2274 |
+ ), |
|
2275 |
+ max_size=256, |
|
2276 |
+ ), |
|
2277 |
+ }, |
|
2278 |
+ ), |
|
2279 |
+ ) |
|
2280 |
+ def test_130c_export_as_sh_service( |
|
2281 |
+ self, |
|
2282 |
+ service_name: str, |
|
2283 |
+ service_config_settable: _types.VaultConfigServicesSettings, |
|
2284 |
+ service_config_importable: _types.VaultConfigServicesSettings, |
|
2285 |
+ ) -> None: |
|
2286 |
+ config: _types.VaultConfig = { |
|
2287 |
+ 'services': { |
|
2288 |
+ service_name: ( |
|
2289 |
+ service_config_settable | service_config_importable |
|
2290 |
+ ), |
|
2291 |
+ }, |
|
2292 |
+ } |
|
2293 |
+ assert _types.clean_up_falsy_vault_config_values(config) is not None |
|
2294 |
+ assert _types.is_vault_config(config) |
|
2295 |
+ return self._export_as_sh_helper(config) |
|
2296 |
+ |
|
2297 |
+ @hypothesis.given( |
|
2298 |
+ service_name=strategies.text( |
|
2299 |
+ alphabet=strategies.characters( |
|
2300 |
+ min_codepoint=32, |
|
2301 |
+ max_codepoint=126, |
|
2302 |
+ ), |
|
2303 |
+ min_size=4, |
|
2304 |
+ max_size=64, |
|
2305 |
+ ), |
|
2306 |
+ service_config_importable=strategies.fixed_dictionaries( |
|
2307 |
+ {}, |
|
2308 |
+ optional={ |
|
2309 |
+ 'key': strategies.text( |
|
2310 |
+ alphabet=strategies.characters( |
|
2311 |
+ min_codepoint=32, |
|
2312 |
+ max_codepoint=126, |
|
2313 |
+ ), |
|
2314 |
+ max_size=128, |
|
2315 |
+ ), |
|
2316 |
+ 'phrase': strategies.text( |
|
2317 |
+ alphabet=strategies.characters( |
|
2318 |
+ min_codepoint=32, |
|
2319 |
+ max_codepoint=126, |
|
2320 |
+ ), |
|
2321 |
+ max_size=64, |
|
2322 |
+ ), |
|
2323 |
+ 'notes': strategies.text( |
|
2324 |
+ alphabet=strategies.characters( |
|
2325 |
+ min_codepoint=32, |
|
2326 |
+ max_codepoint=126, |
|
2327 |
+ include_characters=('\n', '\f', '\t'), |
|
2328 |
+ ), |
|
2329 |
+ max_size=256, |
|
2330 |
+ ), |
|
2331 |
+ }, |
|
2332 |
+ ), |
|
2333 |
+ ) |
|
2334 |
+ def test_130d_export_as_sh_service_only_imports( |
|
2335 |
+ self, |
|
2336 |
+ service_name: str, |
|
2337 |
+ service_config_importable: _types.VaultConfigServicesSettings, |
|
2338 |
+ ) -> None: |
|
2339 |
+ config: _types.VaultConfig = { |
|
2340 |
+ 'services': { |
|
2341 |
+ service_name: service_config_importable, |
|
2342 |
+ }, |
|
2343 |
+ } |
|
2344 |
+ assert _types.clean_up_falsy_vault_config_values(config) is not None |
|
2345 |
+ assert _types.is_vault_config(config) |
|
2346 |
+ return self._export_as_sh_helper(config) |
|
2347 |
+ |
|
2053 | 2348 |
@pytest.mark.parametrize( |
2054 | 2349 |
['command_line', 'config', 'result_config'], |
2055 | 2350 |
[ |
2056 | 2351 |