Marco Ricci commited on 2024-08-25 23:50:03
Zeige 6 geänderte Dateien mit 515 Einfügungen und 5 Löschungen.
Includes basic testing of command-line functionality, explicit examples of each supported format, and proper hatch environment and dependency selection (with exporter support, or without) to run tests in. The major missing pieces are weird-to-reach error conditions in the internals of the exporter functions (which require invasive mocking) and explicit API testing of the exporter functions (i.e. beyond each exporter's "main" function). Because the internal API is not yet stable, particularly the v0.2 and v0.3 exporters, this is currently left as a TODO. Since we are attempting to support installations without the cryptography module, we must ensure the exporter modules all stay importable without triggering import errors. (This is also relevant for pytest's test collection phase, where those modules too are scanned for doctests.) Currently we stub out each (sub-)module of the cryptography package implicitly, such that calling any method on the stub raises an import error, as well as explicitly through a `STUBBED` attribute on the exporter module.
... | ... |
@@ -74,11 +74,19 @@ packages = ['src/derivepassphrase'] |
74 | 74 |
|
75 | 75 |
[tool.hatch.envs.hatch-test] |
76 | 76 |
default-args = ['src', 'tests'] |
77 |
-features = ["export"] |
|
78 | 77 |
|
79 | 78 |
[[tool.hatch.envs.hatch-test.matrix]] |
80 | 79 |
python = ["3.10", "3.11", "3.12", "pypy3.10"] |
81 | 80 |
|
81 |
+[[tool.hatch.envs.hatch-test.matrix]] |
|
82 |
+python = ["3.10", "3.11", "3.12", "pypy3.10"] |
|
83 |
+feature = ["export"] |
|
84 |
+ |
|
85 |
+[tool.hatch.envs.hatch-test.overrides] |
|
86 |
+matrix.feature.features = [ |
|
87 |
+ { value = "export", if = ["export"] }, |
|
88 |
+] |
|
89 |
+ |
|
82 | 90 |
[tool.hatch.env] |
83 | 91 |
requires = [ |
84 | 92 |
"hatch-mkdocs", |
... | ... |
@@ -147,6 +147,8 @@ def derivepassphrase_export( |
147 | 147 |
module = importlib.import_module( |
148 | 148 |
'derivepassphrase.exporter.vault_v03_and_below' |
149 | 149 |
) |
150 |
+ if module.STUBBED: |
|
151 |
+ raise ModuleNotFoundError |
|
150 | 152 |
with open(path, 'rb') as infile: |
151 | 153 |
contents = base64.standard_b64decode(infile.read()) |
152 | 154 |
return module.V02Reader(contents, key).run() |
... | ... |
@@ -154,6 +156,8 @@ def derivepassphrase_export( |
154 | 156 |
module = importlib.import_module( |
155 | 157 |
'derivepassphrase.exporter.vault_v03_and_below' |
156 | 158 |
) |
159 |
+ if module.STUBBED: |
|
160 |
+ raise ModuleNotFoundError |
|
157 | 161 |
with open(path, 'rb') as infile: |
158 | 162 |
contents = base64.standard_b64decode(infile.read()) |
159 | 163 |
return module.V03Reader(contents, key).run() |
... | ... |
@@ -161,6 +165,8 @@ def derivepassphrase_export( |
161 | 165 |
module = importlib.import_module( |
162 | 166 |
'derivepassphrase.exporter.storeroom' |
163 | 167 |
) |
168 |
+ if module.STUBBED: |
|
169 |
+ raise ModuleNotFoundError |
|
164 | 170 |
return module.export_storeroom_data(path, key) |
165 | 171 |
case _: # pragma: no cover |
166 | 172 |
assert_never(fmt) |
... | ... |
@@ -11,14 +11,41 @@ import os.path |
11 | 11 |
import struct |
12 | 12 |
from typing import TYPE_CHECKING, Any, TypedDict |
13 | 13 |
|
14 |
+from derivepassphrase import exporter |
|
15 |
+ |
|
16 |
+if TYPE_CHECKING: |
|
17 |
+ from collections.abc import Iterator |
|
18 |
+ |
|
14 | 19 |
from cryptography.hazmat.primitives import ciphers, hashes, hmac, padding |
15 | 20 |
from cryptography.hazmat.primitives.ciphers import algorithms, modes |
16 | 21 |
from cryptography.hazmat.primitives.kdf import pbkdf2 |
22 |
+else: |
|
23 |
+ try: |
|
24 |
+ from cryptography.hazmat.primitives import ( |
|
25 |
+ ciphers, |
|
26 |
+ hashes, |
|
27 |
+ hmac, |
|
28 |
+ padding, |
|
29 |
+ ) |
|
30 |
+ from cryptography.hazmat.primitives.ciphers import algorithms, modes |
|
31 |
+ from cryptography.hazmat.primitives.kdf import pbkdf2 |
|
32 |
+ except ModuleNotFoundError as exc: |
|
17 | 33 |
|
18 |
-from derivepassphrase import exporter |
|
34 |
+ class DummyModule: |
|
35 |
+ def __init__(self, exc: type[Exception]) -> None: |
|
36 |
+ self.exc = exc |
|
19 | 37 |
|
20 |
-if TYPE_CHECKING: |
|
21 |
- from collections.abc import Iterator |
|
38 |
+ def __getattr__(self, name: str) -> Any: |
|
39 |
+ def func(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001 |
|
40 |
+ raise self.exc |
|
41 |
+ |
|
42 |
+ return func |
|
43 |
+ |
|
44 |
+ ciphers = hashes = hmac = padding = DummyModule(exc) |
|
45 |
+ algorithms = modes = pbkdf2 = DummyModule(exc) |
|
46 |
+ STUBBED = True |
|
47 |
+ else: |
|
48 |
+ STUBBED = False |
|
22 | 49 |
|
23 | 50 |
STOREROOM_MASTER_KEYS_UUID = b'35b7c7ed-f71e-4adf-9051-02fb0f1e0e17' |
24 | 51 |
VAULT_CIPHER_UUID = b'73e69e8a-cb05-4b50-9f42-59d76a511299' |
... | ... |
@@ -7,13 +7,44 @@ import base64 |
7 | 7 |
import json |
8 | 8 |
import logging |
9 | 9 |
import warnings |
10 |
-from typing import Any |
|
10 |
+from typing import TYPE_CHECKING, Any |
|
11 | 11 |
|
12 |
+if TYPE_CHECKING: |
|
12 | 13 |
from cryptography import exceptions as crypt_exceptions |
13 | 14 |
from cryptography import utils as crypt_utils |
14 | 15 |
from cryptography.hazmat.primitives import ciphers, hashes, hmac, padding |
15 | 16 |
from cryptography.hazmat.primitives.ciphers import algorithms, modes |
16 | 17 |
from cryptography.hazmat.primitives.kdf import pbkdf2 |
18 |
+else: |
|
19 |
+ try: |
|
20 |
+ from cryptography import exceptions as crypt_exceptions |
|
21 |
+ from cryptography import utils as crypt_utils |
|
22 |
+ from cryptography.hazmat.primitives import ( |
|
23 |
+ ciphers, |
|
24 |
+ hashes, |
|
25 |
+ hmac, |
|
26 |
+ padding, |
|
27 |
+ ) |
|
28 |
+ from cryptography.hazmat.primitives.ciphers import algorithms, modes |
|
29 |
+ from cryptography.hazmat.primitives.kdf import pbkdf2 |
|
30 |
+ except ModuleNotFoundError as exc: |
|
31 |
+ |
|
32 |
+ class DummyModule: |
|
33 |
+ def __init__(self, exc: type[Exception]) -> None: |
|
34 |
+ self.exc = exc |
|
35 |
+ |
|
36 |
+ def __getattr__(self, name: str) -> Any: |
|
37 |
+ def func(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001 |
|
38 |
+ raise self.exc |
|
39 |
+ |
|
40 |
+ return func |
|
41 |
+ |
|
42 |
+ crypt_exceptions = crypt_utils = DummyModule(exc) |
|
43 |
+ ciphers = hashes = hmac = padding = DummyModule(exc) |
|
44 |
+ algorithms = modes = pbkdf2 = DummyModule(exc) |
|
45 |
+ STUBBED = True |
|
46 |
+ else: |
|
47 |
+ STUBBED = False |
|
17 | 48 |
|
18 | 49 |
from derivepassphrase import exporter, vault |
19 | 50 |
|
... | ... |
@@ -9,9 +9,12 @@ import contextlib |
9 | 9 |
import json |
10 | 10 |
import os |
11 | 11 |
import stat |
12 |
+import tempfile |
|
13 |
+import zipfile |
|
12 | 14 |
from typing import TYPE_CHECKING |
13 | 15 |
|
14 | 16 |
import pytest |
17 |
+from typing_extensions import assert_never |
|
15 | 18 |
|
16 | 19 |
from derivepassphrase import _types, cli |
17 | 20 |
|
... | ... |
@@ -409,6 +412,60 @@ def isolated_config( |
409 | 412 |
yield |
410 | 413 |
|
411 | 414 |
|
415 |
+@contextlib.contextmanager |
|
416 |
+def isolated_vault_exporter_config( |
|
417 |
+ monkeypatch: pytest.MonkeyPatch, |
|
418 |
+ runner: click.testing.CliRunner, |
|
419 |
+ vault_config: str | bytes | None = None, |
|
420 |
+ vault_key: str | None = None, |
|
421 |
+) -> Iterator[None]: |
|
422 |
+ if TYPE_CHECKING: |
|
423 |
+ chdir = contextlib.chdir |
|
424 |
+ else: |
|
425 |
+ try: |
|
426 |
+ chdir = contextlib.chdir |
|
427 |
+ except AttributeError: |
|
428 |
+ |
|
429 |
+ @contextlib.contextmanager |
|
430 |
+ def chdir(newpath: str) -> Iterator[None]: |
|
431 |
+ oldpath = os.getcwd() |
|
432 |
+ os.chdir(newpath) |
|
433 |
+ yield |
|
434 |
+ os.chdir(oldpath) |
|
435 |
+ |
|
436 |
+ with runner.isolated_filesystem(): |
|
437 |
+ monkeypatch.setenv('HOME', os.getcwd()) |
|
438 |
+ monkeypatch.setenv('USERPROFILE', os.getcwd()) |
|
439 |
+ monkeypatch.delenv('VAULT_PATH', raising=False) |
|
440 |
+ monkeypatch.delenv('VAULT_KEY', raising=False) |
|
441 |
+ monkeypatch.delenv('LOGNAME', raising=False) |
|
442 |
+ monkeypatch.delenv('USER', raising=False) |
|
443 |
+ monkeypatch.delenv('USERNAME', raising=False) |
|
444 |
+ if vault_key is not None: |
|
445 |
+ monkeypatch.setenv('VAULT_KEY', vault_key) |
|
446 |
+ match vault_config: |
|
447 |
+ case str(): |
|
448 |
+ with open('.vault', 'w', encoding='UTF-8') as outfile: |
|
449 |
+ print(vault_config, file=outfile) |
|
450 |
+ case bytes(): |
|
451 |
+ os.makedirs('.vault', mode=0o700, exist_ok=True) |
|
452 |
+ with ( |
|
453 |
+ chdir('.vault'), |
|
454 |
+ tempfile.NamedTemporaryFile(suffix='.zip') as tmpzipfile, |
|
455 |
+ ): |
|
456 |
+ for line in vault_config.splitlines(): |
|
457 |
+ tmpzipfile.write(base64.standard_b64decode(line)) |
|
458 |
+ tmpzipfile.flush() |
|
459 |
+ tmpzipfile.seek(0, 0) |
|
460 |
+ with zipfile.ZipFile(tmpzipfile.file) as zipfileobj: |
|
461 |
+ zipfileobj.extractall() |
|
462 |
+ case None: |
|
463 |
+ pass |
|
464 |
+ case _: # pragma: no cover |
|
465 |
+ assert_never(vault_config) |
|
466 |
+ yield |
|
467 |
+ |
|
468 |
+ |
|
412 | 469 |
def auto_prompt(*args: Any, **kwargs: Any) -> str: |
413 | 470 |
del args, kwargs # Unused. |
414 | 471 |
return DUMMY_PASSPHRASE.decode('UTF-8') |
... | ... |
@@ -0,0 +1,381 @@ |
1 |
+# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
|
2 |
+# |
|
3 |
+# SPDX-License-Identifier: MIT |
|
4 |
+ |
|
5 |
+from __future__ import annotations |
|
6 |
+ |
|
7 |
+import json |
|
8 |
+import os |
|
9 |
+from typing import Any |
|
10 |
+ |
|
11 |
+import click.testing |
|
12 |
+import pytest |
|
13 |
+ |
|
14 |
+import tests |
|
15 |
+from derivepassphrase import exporter |
|
16 |
+ |
|
17 |
+VAULT_MASTER_KEY = 'vault key' |
|
18 |
+VAULT_V02_CONFIG = 'P7xeh5y4jmjpJ2pFq4KUcTVoaE9ZOEkwWmpVTURSSWQxbGt6emN4aFE4eFM3anVPbDRNTGpOLzY3eDF5aE1YTm5LNWh5Q1BwWTMwM3M5S083MWRWRFlmOXNqSFJNcStGMWFOS3c2emhiOUNNenZYTmNNMnZxaUErdlRoOGF2ZHdGT1ZLNTNLOVJQcU9jWmJrR3g5N09VcVBRZ0ZnSFNUQy9HdFVWWnFteVhRVkY3MHNBdnF2ZWFEbFBseWRGelE1c3BFTnVUckRQdWJSL29wNjFxd2Y2ZVpob3VyVzRod3FKTElTenJ1WTZacTJFOFBtK3BnVzh0QWVxcWtyWFdXOXYyenNQeFNZbWt1MDU2Vm1kVGtISWIxWTBpcWRFbyswUVJudVVhZkVlNVpGWDA4WUQ2Q2JTWW81SnlhQ2Zxa3cxNmZoQjJES0Uyd29rNXpSck5iWVBrVmEwOXFya1NpMi9saU5LL3F0M3N3MjZKekNCem9ER2svWkZ0SUJLdmlHRno0VlQzQ3pqZTBWcTM3YmRiNmJjTkhqUHZoQ0NxMW1ldW1XOFVVK3pQMEtUMkRMVGNvNHFlOG40ck5KcGhsYXg1b1VzZ1NYU1B2T3RXdEkwYzg4NWE3YWUzOWI1MDI0MThhMWZjODQ3MDA2OTJmNDQ0MDkxNGFiNmRlMGQ2YjZiNjI5NGMwN2IwMmI4MGZi' # noqa: E501 |
|
19 |
+VAULT_V02_CONFIG_DATA = { |
|
20 |
+ 'global': { |
|
21 |
+ 'phrase': tests.DUMMY_PASSPHRASE.decode('utf-8').rstrip('\n'), |
|
22 |
+ }, |
|
23 |
+ 'services': { |
|
24 |
+ '(meta)': { |
|
25 |
+ 'notes': 'This config was originally in v0.2 format.', |
|
26 |
+ }, |
|
27 |
+ tests.DUMMY_SERVICE: tests.DUMMY_CONFIG_SETTINGS.copy(), |
|
28 |
+ }, |
|
29 |
+} |
|
30 |
+VAULT_V03_CONFIG = 'sBPBrr8BFHPxSJkV/A53zk9zwDQHFxLe6UIusCVvzFQre103pcj5xxmE11lMTA0U2QTYjkhRXKkH5WegSmYpAnzReuRsYZlWWp6N4kkubf+twZ9C3EeggPm7as2Af4TICHVbX4uXpIHeQJf9y1OtqrO+SRBrgPBzgItoxsIxebxVKgyvh1CZQOSkn7BIzt9xKhDng3ubS4hQ91fB0QCumlldTbUl8tj4Xs5JbvsSlUMxRlVzZ0OgAOrSsoWELXmsp6zXFa9K6wIuZa4wQuMLQFHiA64JO1CR3I+rviWCeMlbTOuJNx6vMB5zotKJqA2hIUpN467TQ9vI4g/QTo40m5LT2EQKbIdTvBQAzcV4lOcpr5Lqt4LHED5mKvm/4YfpuuT3I3XCdWfdG5SB7ciiB4Go+xQdddy3zZMiwm1fEwIB8XjFf2cxoJdccLQ2yxf+9diedBP04EsMHrvxKDhQ7/vHl7xF2MMFTDKl3WFd23vvcjpR1JgNAKYprG/e1p/7' # noqa: E501 |
|
31 |
+VAULT_V03_CONFIG_DATA = { |
|
32 |
+ 'global': { |
|
33 |
+ 'phrase': tests.DUMMY_PASSPHRASE.decode('utf-8').rstrip('\n'), |
|
34 |
+ }, |
|
35 |
+ 'services': { |
|
36 |
+ '(meta)': { |
|
37 |
+ 'notes': 'This config was originally in v0.3 format.', |
|
38 |
+ }, |
|
39 |
+ tests.DUMMY_SERVICE: tests.DUMMY_CONFIG_SETTINGS.copy(), |
|
40 |
+ }, |
|
41 |
+} |
|
42 |
+VAULT_STOREROOM_CONFIG_ZIPPED = b""" |
|
43 |
+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 |
|
93 |
+""" |
|
94 |
+VAULT_STOREROOM_CONFIG_DATA = { |
|
95 |
+ 'global': { |
|
96 |
+ 'phrase': tests.DUMMY_PASSPHRASE.decode('utf-8').rstrip('\n'), |
|
97 |
+ }, |
|
98 |
+ 'services': { |
|
99 |
+ '(meta)': { |
|
100 |
+ 'notes': 'This config was originally in storeroom format.', |
|
101 |
+ }, |
|
102 |
+ tests.DUMMY_SERVICE: tests.DUMMY_CONFIG_SETTINGS.copy(), |
|
103 |
+ }, |
|
104 |
+} |
|
105 |
+ |
|
106 |
+try: |
|
107 |
+ from cryptography.hazmat.primitives import ciphers |
|
108 |
+except ModuleNotFoundError: |
|
109 |
+ CRYPTOGRAPHY_SUPPORT = False |
|
110 |
+else: |
|
111 |
+ CRYPTOGRAPHY_SUPPORT = True |
|
112 |
+ del ciphers |
|
113 |
+ |
|
114 |
+CANNOT_LOAD_CRYPTOGRAPHY = ( |
|
115 |
+ b'Cannot load the required Python module "cryptography".' |
|
116 |
+) |
|
117 |
+ |
|
118 |
+ |
|
119 |
+class Test001ExporterUtils: |
|
120 |
+ @pytest.mark.parametrize( |
|
121 |
+ ['expected', 'vault_key', 'logname', 'user', 'username'], |
|
122 |
+ [ |
|
123 |
+ ('4username', None, None, None, '4username'), |
|
124 |
+ ('3user', None, None, '3user', None), |
|
125 |
+ ('3user', None, None, '3user', '4username'), |
|
126 |
+ ('2logname', None, '2logname', None, None), |
|
127 |
+ ('2logname', None, '2logname', None, '4username'), |
|
128 |
+ ('2logname', None, '2logname', '3user', None), |
|
129 |
+ ('2logname', None, '2logname', '3user', '4username'), |
|
130 |
+ ('1vault_key', '1vault_key', None, None, None), |
|
131 |
+ ('1vault_key', '1vault_key', None, None, '4username'), |
|
132 |
+ ('1vault_key', '1vault_key', None, '3user', None), |
|
133 |
+ ('1vault_key', '1vault_key', None, '3user', '4username'), |
|
134 |
+ ('1vault_key', '1vault_key', '2logname', None, None), |
|
135 |
+ ('1vault_key', '1vault_key', '2logname', None, '4username'), |
|
136 |
+ ('1vault_key', '1vault_key', '2logname', '3user', None), |
|
137 |
+ ('1vault_key', '1vault_key', '2logname', '3user', '4username'), |
|
138 |
+ ], |
|
139 |
+ ) |
|
140 |
+ def test200_get_vault_key( |
|
141 |
+ self, |
|
142 |
+ monkeypatch: pytest.MonkeyPatch, |
|
143 |
+ expected: str, |
|
144 |
+ vault_key: str | None, |
|
145 |
+ logname: str | None, |
|
146 |
+ user: str | None, |
|
147 |
+ username: str | None, |
|
148 |
+ ) -> None: |
|
149 |
+ priority_list = [ |
|
150 |
+ ('VAULT_KEY', vault_key), |
|
151 |
+ ('LOGNAME', logname), |
|
152 |
+ ('USER', user), |
|
153 |
+ ('USERNAME', username), |
|
154 |
+ ] |
|
155 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
156 |
+ with tests.isolated_vault_exporter_config( |
|
157 |
+ monkeypatch=monkeypatch, runner=runner |
|
158 |
+ ): |
|
159 |
+ for key, value in priority_list: |
|
160 |
+ if value is not None: |
|
161 |
+ monkeypatch.setenv(key, value) |
|
162 |
+ assert os.fsdecode(exporter.get_vault_key()) == expected |
|
163 |
+ |
|
164 |
+ @pytest.mark.parametrize( |
|
165 |
+ ['expected', 'path'], |
|
166 |
+ [ |
|
167 |
+ ('/tmp', '/tmp'), |
|
168 |
+ ('~', os.path.curdir), |
|
169 |
+ ('~/.vault', None), |
|
170 |
+ ], |
|
171 |
+ ) |
|
172 |
+ def test_210_get_vault_path( |
|
173 |
+ self, |
|
174 |
+ monkeypatch: pytest.MonkeyPatch, |
|
175 |
+ expected: str, |
|
176 |
+ path: str | None, |
|
177 |
+ ) -> None: |
|
178 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
179 |
+ with tests.isolated_vault_exporter_config( |
|
180 |
+ monkeypatch=monkeypatch, runner=runner |
|
181 |
+ ): |
|
182 |
+ if path: |
|
183 |
+ monkeypatch.setenv('VAULT_PATH', path) |
|
184 |
+ assert os.fsdecode( |
|
185 |
+ os.path.realpath(exporter.get_vault_path()) |
|
186 |
+ ) == os.path.realpath(os.path.expanduser(expected)) |
|
187 |
+ |
|
188 |
+ def test_300_get_vault_key_without_envs( |
|
189 |
+ self, monkeypatch: pytest.MonkeyPatch |
|
190 |
+ ) -> None: |
|
191 |
+ monkeypatch.delenv('VAULT_KEY', raising=False) |
|
192 |
+ monkeypatch.delenv('LOGNAME', raising=False) |
|
193 |
+ monkeypatch.delenv('USER', raising=False) |
|
194 |
+ monkeypatch.delenv('USERNAME', raising=False) |
|
195 |
+ with pytest.raises(KeyError, match='VAULT_KEY'): |
|
196 |
+ exporter.get_vault_key() |
|
197 |
+ |
|
198 |
+ def test_310_get_vault_path_without_home( |
|
199 |
+ self, monkeypatch: pytest.MonkeyPatch |
|
200 |
+ ) -> None: |
|
201 |
+ monkeypatch.setattr(os.path, 'expanduser', lambda x: x) |
|
202 |
+ with pytest.raises( |
|
203 |
+ RuntimeError, match='[Cc]annot determine home directory' |
|
204 |
+ ): |
|
205 |
+ exporter.get_vault_path() |
|
206 |
+ |
|
207 |
+ |
|
208 |
+class Test002CLI: |
|
209 |
+ @pytest.mark.xfail( |
|
210 |
+ not CRYPTOGRAPHY_SUPPORT, reason='cryptography module not found' |
|
211 |
+ ) |
|
212 |
+ def test_200_path_parameter(self, monkeypatch: pytest.MonkeyPatch) -> None: |
|
213 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
214 |
+ with tests.isolated_vault_exporter_config( |
|
215 |
+ monkeypatch=monkeypatch, |
|
216 |
+ runner=runner, |
|
217 |
+ vault_config=VAULT_V03_CONFIG, |
|
218 |
+ vault_key=VAULT_MASTER_KEY, |
|
219 |
+ ): |
|
220 |
+ monkeypatch.setenv('VAULT_KEY', VAULT_MASTER_KEY) |
|
221 |
+ result = runner.invoke( |
|
222 |
+ exporter.derivepassphrase_export, |
|
223 |
+ ['VAULT_PATH'], |
|
224 |
+ ) |
|
225 |
+ assert not result.exception |
|
226 |
+ assert (result.exit_code, result.stderr_bytes) == (0, b'') |
|
227 |
+ assert json.loads(result.stdout) == VAULT_V03_CONFIG_DATA |
|
228 |
+ |
|
229 |
+ @pytest.mark.xfail( |
|
230 |
+ not CRYPTOGRAPHY_SUPPORT, reason='cryptography module not found' |
|
231 |
+ ) |
|
232 |
+ def test_201_key_parameter(self, monkeypatch: pytest.MonkeyPatch) -> None: |
|
233 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
234 |
+ with tests.isolated_vault_exporter_config( |
|
235 |
+ monkeypatch=monkeypatch, |
|
236 |
+ runner=runner, |
|
237 |
+ vault_config=VAULT_V03_CONFIG, |
|
238 |
+ ): |
|
239 |
+ result = runner.invoke( |
|
240 |
+ exporter.derivepassphrase_export, |
|
241 |
+ ['-k', VAULT_MASTER_KEY, '.vault'], |
|
242 |
+ ) |
|
243 |
+ assert not result.exception |
|
244 |
+ assert (result.exit_code, result.stderr_bytes) == (0, b'') |
|
245 |
+ assert json.loads(result.stdout) == VAULT_V03_CONFIG_DATA |
|
246 |
+ |
|
247 |
+ @pytest.mark.xfail( |
|
248 |
+ not CRYPTOGRAPHY_SUPPORT, reason='cryptography module not found' |
|
249 |
+ ) |
|
250 |
+ @pytest.mark.parametrize( |
|
251 |
+ ['version', 'config', 'config_data'], |
|
252 |
+ [ |
|
253 |
+ pytest.param( |
|
254 |
+ '0.2', VAULT_V02_CONFIG, VAULT_V02_CONFIG_DATA, id='0.2' |
|
255 |
+ ), |
|
256 |
+ pytest.param( |
|
257 |
+ '0.3', VAULT_V03_CONFIG, VAULT_V03_CONFIG_DATA, id='0.3' |
|
258 |
+ ), |
|
259 |
+ pytest.param( |
|
260 |
+ 'storeroom', |
|
261 |
+ VAULT_STOREROOM_CONFIG_ZIPPED, |
|
262 |
+ VAULT_STOREROOM_CONFIG_DATA, |
|
263 |
+ id='storeroom', |
|
264 |
+ ), |
|
265 |
+ ], |
|
266 |
+ ) |
|
267 |
+ def test_210_load_vault_v02_v03_storeroom( |
|
268 |
+ self, |
|
269 |
+ monkeypatch: pytest.MonkeyPatch, |
|
270 |
+ version: str, |
|
271 |
+ config: str | bytes, |
|
272 |
+ config_data: dict[str, Any], |
|
273 |
+ ) -> None: |
|
274 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
275 |
+ with tests.isolated_vault_exporter_config( |
|
276 |
+ monkeypatch=monkeypatch, |
|
277 |
+ runner=runner, |
|
278 |
+ vault_config=config, |
|
279 |
+ ): |
|
280 |
+ result = runner.invoke( |
|
281 |
+ exporter.derivepassphrase_export, |
|
282 |
+ [ |
|
283 |
+ '-f', |
|
284 |
+ f'v{version}' if version.startswith('0') else version, |
|
285 |
+ '-k', |
|
286 |
+ VAULT_MASTER_KEY, |
|
287 |
+ 'VAULT_PATH', |
|
288 |
+ ], |
|
289 |
+ ) |
|
290 |
+ assert not result.exception |
|
291 |
+ assert (result.exit_code, result.stderr_bytes) == (0, b'') |
|
292 |
+ assert json.loads(result.stdout) == config_data |
|
293 |
+ |
|
294 |
+ def test_300_invalid_format( |
|
295 |
+ self, |
|
296 |
+ monkeypatch: pytest.MonkeyPatch, |
|
297 |
+ ) -> None: |
|
298 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
299 |
+ with tests.isolated_vault_exporter_config( |
|
300 |
+ monkeypatch=monkeypatch, |
|
301 |
+ runner=runner, |
|
302 |
+ vault_config=VAULT_V03_CONFIG, |
|
303 |
+ vault_key=VAULT_MASTER_KEY, |
|
304 |
+ ): |
|
305 |
+ result = runner.invoke( |
|
306 |
+ exporter.derivepassphrase_export, |
|
307 |
+ ['-f', 'INVALID', 'VAULT_PATH'], |
|
308 |
+ catch_exceptions=False, |
|
309 |
+ ) |
|
310 |
+ assert isinstance(result.exception, SystemExit) |
|
311 |
+ assert result.exit_code |
|
312 |
+ assert result.stderr_bytes |
|
313 |
+ assert b'Invalid value for' in result.stderr_bytes |
|
314 |
+ assert b'-f' in result.stderr_bytes |
|
315 |
+ assert b'--format' in result.stderr_bytes |
|
316 |
+ assert b'INVALID' in result.stderr_bytes |
|
317 |
+ |
|
318 |
+ @pytest.mark.xfail( |
|
319 |
+ not CRYPTOGRAPHY_SUPPORT, reason='cryptography module not found' |
|
320 |
+ ) |
|
321 |
+ def test_301_vault_config_not_found( |
|
322 |
+ self, |
|
323 |
+ monkeypatch: pytest.MonkeyPatch, |
|
324 |
+ ) -> None: |
|
325 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
326 |
+ with tests.isolated_vault_exporter_config( |
|
327 |
+ monkeypatch=monkeypatch, |
|
328 |
+ runner=runner, |
|
329 |
+ vault_config=VAULT_V03_CONFIG, |
|
330 |
+ vault_key=VAULT_MASTER_KEY, |
|
331 |
+ ): |
|
332 |
+ result = runner.invoke( |
|
333 |
+ exporter.derivepassphrase_export, |
|
334 |
+ ['does-not-exist.txt'], |
|
335 |
+ ) |
|
336 |
+ assert isinstance(result.exception, SystemExit) |
|
337 |
+ assert result.exit_code |
|
338 |
+ assert result.stderr_bytes |
|
339 |
+ assert ( |
|
340 |
+ b"Cannot parse 'does-not-exist.txt' as a valid config" |
|
341 |
+ in result.stderr_bytes |
|
342 |
+ ) |
|
343 |
+ assert CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes |
|
344 |
+ |
|
345 |
+ @pytest.mark.xfail( |
|
346 |
+ not CRYPTOGRAPHY_SUPPORT, reason='cryptography module not found' |
|
347 |
+ ) |
|
348 |
+ def test_302_vault_config_invalid( |
|
349 |
+ self, |
|
350 |
+ monkeypatch: pytest.MonkeyPatch, |
|
351 |
+ ) -> None: |
|
352 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
353 |
+ with tests.isolated_vault_exporter_config( |
|
354 |
+ monkeypatch=monkeypatch, |
|
355 |
+ runner=runner, |
|
356 |
+ vault_config='', |
|
357 |
+ vault_key=VAULT_MASTER_KEY, |
|
358 |
+ ): |
|
359 |
+ result = runner.invoke( |
|
360 |
+ exporter.derivepassphrase_export, |
|
361 |
+ ['.vault'], |
|
362 |
+ ) |
|
363 |
+ assert isinstance(result.exception, SystemExit) |
|
364 |
+ assert result.exit_code |
|
365 |
+ assert result.stderr_bytes |
|
366 |
+ assert ( |
|
367 |
+ b"Cannot parse '.vault' as a valid config." in result.stderr_bytes |
|
368 |
+ ) |
|
369 |
+ assert CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes |
|
370 |
+ |
|
371 |
+ |
|
372 |
+class TestStoreroomExporter: |
|
373 |
+ pass # TODO(the-13th-letter): Fill in once design is stable. |
|
374 |
+ |
|
375 |
+ |
|
376 |
+class TestV02Exporter: |
|
377 |
+ pass # TODO(the-13th-letter): Fill in once design is stable. |
|
378 |
+ |
|
379 |
+ |
|
380 |
+class TestV03Exporter: |
|
381 |
+ pass # TODO(the-13th-letter): Fill in once design is stable. |
|
0 | 382 |