Marco Ricci commited on 2024-08-31 21:04:32
Zeige 4 geänderte Dateien mit 152 Einfügungen und 42 Löschungen.
Move the `_load_data` function, which deals with determining the correct vault storage format (with automatic rollover) into the top level, where the function can be tested in isolation, and stubbed out if necessary. Also, use the environments with disabled cryptography support to test for correct failure behavior of the exporter. Use `importlib.util.find_spec` instead of an `import` statement to check for importability of a module, which does not require handling of `ModuleNotFoundError` if restricted to top-level modules/packages.
... | ... |
@@ -31,6 +31,43 @@ __all__ = ('derivepassphrase_export',) |
31 | 31 |
PROG_NAME = 'derivepassphrase_export' |
32 | 32 |
|
33 | 33 |
|
34 |
+def _load_data( |
|
35 |
+ fmt: Literal['v0.2', 'v0.3', 'storeroom'], |
|
36 |
+ path: str | bytes | os.PathLike[str], |
|
37 |
+ key: bytes, |
|
38 |
+) -> Any: |
|
39 |
+ contents: bytes |
|
40 |
+ module: types.ModuleType |
|
41 |
+ match fmt: |
|
42 |
+ case 'v0.2': |
|
43 |
+ module = importlib.import_module( |
|
44 |
+ 'derivepassphrase.exporter.vault_v03_and_below' |
|
45 |
+ ) |
|
46 |
+ if module.STUBBED: |
|
47 |
+ raise ModuleNotFoundError |
|
48 |
+ with open(path, 'rb') as infile: |
|
49 |
+ contents = base64.standard_b64decode(infile.read()) |
|
50 |
+ return module.V02Reader(contents, key).run() |
|
51 |
+ case 'v0.3': |
|
52 |
+ module = importlib.import_module( |
|
53 |
+ 'derivepassphrase.exporter.vault_v03_and_below' |
|
54 |
+ ) |
|
55 |
+ if module.STUBBED: |
|
56 |
+ raise ModuleNotFoundError |
|
57 |
+ with open(path, 'rb') as infile: |
|
58 |
+ contents = base64.standard_b64decode(infile.read()) |
|
59 |
+ return module.V03Reader(contents, key).run() |
|
60 |
+ case 'storeroom': |
|
61 |
+ module = importlib.import_module( |
|
62 |
+ 'derivepassphrase.exporter.storeroom' |
|
63 |
+ ) |
|
64 |
+ if module.STUBBED: |
|
65 |
+ raise ModuleNotFoundError |
|
66 |
+ return module.export_storeroom_data(path, key) |
|
67 |
+ case _: # pragma: no cover |
|
68 |
+ assert_never(fmt) |
|
69 |
+ |
|
70 |
+ |
|
34 | 71 |
@click.command( |
35 | 72 |
context_settings={'help_option_names': ['-h', '--help']}, |
36 | 73 |
) |
... | ... |
@@ -78,42 +115,6 @@ def derivepassphrase_export( |
78 | 115 |
|
79 | 116 |
""" |
80 | 117 |
|
81 |
- def load_data( |
|
82 |
- fmt: Literal['v0.2', 'v0.3', 'storeroom'], |
|
83 |
- path: str | bytes | os.PathLike[str], |
|
84 |
- key: bytes, |
|
85 |
- ) -> Any: |
|
86 |
- contents: bytes |
|
87 |
- module: types.ModuleType |
|
88 |
- match fmt: |
|
89 |
- case 'v0.2': |
|
90 |
- module = importlib.import_module( |
|
91 |
- 'derivepassphrase.exporter.vault_v03_and_below' |
|
92 |
- ) |
|
93 |
- if module.STUBBED: |
|
94 |
- raise ModuleNotFoundError |
|
95 |
- with open(path, 'rb') as infile: |
|
96 |
- contents = base64.standard_b64decode(infile.read()) |
|
97 |
- return module.V02Reader(contents, key).run() |
|
98 |
- case 'v0.3': |
|
99 |
- module = importlib.import_module( |
|
100 |
- 'derivepassphrase.exporter.vault_v03_and_below' |
|
101 |
- ) |
|
102 |
- if module.STUBBED: |
|
103 |
- raise ModuleNotFoundError |
|
104 |
- with open(path, 'rb') as infile: |
|
105 |
- contents = base64.standard_b64decode(infile.read()) |
|
106 |
- return module.V03Reader(contents, key).run() |
|
107 |
- case 'storeroom': |
|
108 |
- module = importlib.import_module( |
|
109 |
- 'derivepassphrase.exporter.storeroom' |
|
110 |
- ) |
|
111 |
- if module.STUBBED: |
|
112 |
- raise ModuleNotFoundError |
|
113 |
- return module.export_storeroom_data(path, key) |
|
114 |
- case _: # pragma: no cover |
|
115 |
- assert_never(fmt) |
|
116 |
- |
|
117 | 118 |
logging.basicConfig() |
118 | 119 |
if path in {'VAULT_PATH', b'VAULT_PATH'}: |
119 | 120 |
path = exporter.get_vault_path() |
... | ... |
@@ -123,7 +124,7 @@ def derivepassphrase_export( |
123 | 124 |
key = key.encode('utf-8') |
124 | 125 |
for fmt in formats: |
125 | 126 |
try: |
126 |
- config = load_data(fmt, path, key) |
|
127 |
+ config = _load_data(fmt, path, key) |
|
127 | 128 |
except ( |
128 | 129 |
IsADirectoryError, |
129 | 130 |
NotADirectoryError, |
... | ... |
@@ -165,3 +166,7 @@ def derivepassphrase_export( |
165 | 166 |
err=True, |
166 | 167 |
) |
167 | 168 |
ctx.exit(1) |
169 |
+ |
|
170 |
+ |
|
171 |
+if __name__ == '__main__': |
|
172 |
+ derivepassphrase_export() |
... | ... |
@@ -6,6 +6,7 @@ from __future__ import annotations |
6 | 6 |
|
7 | 7 |
import base64 |
8 | 8 |
import contextlib |
9 |
+import importlib.util |
|
9 | 10 |
import json |
10 | 11 |
import os |
11 | 12 |
import stat |
... | ... |
@@ -446,6 +447,14 @@ CANNOT_LOAD_CRYPTOGRAPHY = ( |
446 | 447 |
skip_if_no_agent = pytest.mark.skipif( |
447 | 448 |
not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required' |
448 | 449 |
) |
450 |
+skip_if_cryptography_support = pytest.mark.skipif( |
|
451 |
+ importlib.util.find_spec('cryptography') is not None, |
|
452 |
+ reason='cryptography support available; cannot test "no support" scenario', |
|
453 |
+) |
|
454 |
+skip_if_no_cryptography_support = pytest.mark.skipif( |
|
455 |
+ importlib.util.find_spec('cryptography') is None, |
|
456 |
+ reason='no "cryptography" support', |
|
457 |
+) |
|
449 | 458 |
|
450 | 459 |
|
451 | 460 |
def list_keys(self: Any = None) -> list[_types.KeyCommentPair]: |
... | ... |
@@ -4,9 +4,7 @@ |
4 | 4 |
|
5 | 5 |
from __future__ import annotations |
6 | 6 |
|
7 |
-import json |
|
8 | 7 |
import os |
9 |
-from typing import Any |
|
10 | 8 |
|
11 | 9 |
import click.testing |
12 | 10 |
import pytest |
... | ... |
@@ -129,3 +127,51 @@ class Test002CLI: |
129 | 127 |
assert b'-f' in result.stderr_bytes |
130 | 128 |
assert b'--format' in result.stderr_bytes |
131 | 129 |
assert b'INVALID' in result.stderr_bytes |
130 |
+ |
|
131 |
+ @tests.skip_if_cryptography_support |
|
132 |
+ @pytest.mark.parametrize( |
|
133 |
+ ['format', 'config', 'key'], |
|
134 |
+ [ |
|
135 |
+ pytest.param( |
|
136 |
+ 'v0.2', |
|
137 |
+ tests.VAULT_V02_CONFIG, |
|
138 |
+ tests.VAULT_MASTER_KEY, |
|
139 |
+ id='v0.2', |
|
140 |
+ ), |
|
141 |
+ pytest.param( |
|
142 |
+ 'v0.3', |
|
143 |
+ tests.VAULT_V03_CONFIG, |
|
144 |
+ tests.VAULT_MASTER_KEY, |
|
145 |
+ id='v0.3', |
|
146 |
+ ), |
|
147 |
+ pytest.param( |
|
148 |
+ 'storeroom', |
|
149 |
+ tests.VAULT_STOREROOM_CONFIG_ZIPPED, |
|
150 |
+ tests.VAULT_MASTER_KEY, |
|
151 |
+ id='storeroom', |
|
152 |
+ ), |
|
153 |
+ ], |
|
154 |
+ ) |
|
155 |
+ def test_999_no_cryptography_error_message( |
|
156 |
+ self, |
|
157 |
+ monkeypatch: pytest.MonkeyPatch, |
|
158 |
+ format: str, |
|
159 |
+ config: str | bytes, |
|
160 |
+ key: str, |
|
161 |
+ ) -> None: |
|
162 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
163 |
+ with tests.isolated_vault_exporter_config( |
|
164 |
+ monkeypatch=monkeypatch, |
|
165 |
+ runner=runner, |
|
166 |
+ vault_config=config, |
|
167 |
+ vault_key=key, |
|
168 |
+ ): |
|
169 |
+ result = runner.invoke( |
|
170 |
+ cli.derivepassphrase_export, |
|
171 |
+ ['-f', format, 'VAULT_PATH'], |
|
172 |
+ catch_exceptions=False, |
|
173 |
+ ) |
|
174 |
+ assert isinstance(result.exception, SystemExit) |
|
175 |
+ assert result.exit_code |
|
176 |
+ assert result.stderr_bytes |
|
177 |
+ assert tests.CANNOT_LOAD_CRYPTOGRAPHY in result.stderr_bytes |
... | ... |
@@ -5,18 +5,19 @@ |
5 | 5 |
from __future__ import annotations |
6 | 6 |
|
7 | 7 |
import json |
8 |
-import os |
|
9 |
-from typing import Any |
|
8 |
+from typing import TYPE_CHECKING |
|
10 | 9 |
|
11 | 10 |
import click.testing |
12 | 11 |
import pytest |
13 | 12 |
|
14 | 13 |
import tests |
15 |
-from derivepassphrase import exporter |
|
16 | 14 |
from derivepassphrase.exporter import cli |
17 | 15 |
|
18 | 16 |
cryptography = pytest.importorskip('cryptography', minversion='38.0') |
19 | 17 |
|
18 |
+if TYPE_CHECKING: |
|
19 |
+ from typing import Any |
|
20 |
+ |
|
20 | 21 |
|
21 | 22 |
class TestCLI: |
22 | 23 |
def test_200_path_parameter(self, monkeypatch: pytest.MonkeyPatch) -> None: |
... | ... |
@@ -144,3 +145,52 @@ class TestCLI: |
144 | 145 |
b"Cannot parse '.vault' as a valid config." in result.stderr_bytes |
145 | 146 |
) |
146 | 147 |
assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes |
148 |
+ |
|
149 |
+ def test_403_invalid_vault_config_bad_signature( |
|
150 |
+ self, |
|
151 |
+ monkeypatch: pytest.MonkeyPatch, |
|
152 |
+ ) -> None: |
|
153 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
154 |
+ with tests.isolated_vault_exporter_config( |
|
155 |
+ monkeypatch=monkeypatch, |
|
156 |
+ runner=runner, |
|
157 |
+ vault_config=tests.VAULT_V02_CONFIG, |
|
158 |
+ vault_key=tests.VAULT_MASTER_KEY, |
|
159 |
+ ): |
|
160 |
+ result = runner.invoke( |
|
161 |
+ cli.derivepassphrase_export, |
|
162 |
+ ['-f', 'v0.3', '.vault'], |
|
163 |
+ ) |
|
164 |
+ assert isinstance(result.exception, SystemExit) |
|
165 |
+ assert result.exit_code |
|
166 |
+ assert result.stderr_bytes |
|
167 |
+ assert ( |
|
168 |
+ b"Cannot parse '.vault' as a valid config." in result.stderr_bytes |
|
169 |
+ ) |
|
170 |
+ assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes |
|
171 |
+ |
|
172 |
+ def test_500_vault_config_invalid_internal( |
|
173 |
+ self, |
|
174 |
+ monkeypatch: pytest.MonkeyPatch, |
|
175 |
+ ) -> None: |
|
176 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
177 |
+ with tests.isolated_vault_exporter_config( |
|
178 |
+ monkeypatch=monkeypatch, |
|
179 |
+ runner=runner, |
|
180 |
+ vault_config=tests.VAULT_V03_CONFIG, |
|
181 |
+ vault_key=tests.VAULT_MASTER_KEY, |
|
182 |
+ ): |
|
183 |
+ |
|
184 |
+ def _load_data(*_args: Any, **_kwargs: Any) -> None: |
|
185 |
+ return None |
|
186 |
+ |
|
187 |
+ monkeypatch.setattr(cli, '_load_data', _load_data) |
|
188 |
+ result = runner.invoke( |
|
189 |
+ cli.derivepassphrase_export, |
|
190 |
+ ['.vault'], |
|
191 |
+ ) |
|
192 |
+ assert isinstance(result.exception, SystemExit) |
|
193 |
+ assert result.exit_code |
|
194 |
+ assert result.stderr_bytes |
|
195 |
+ assert b'Invalid vault config: ' in result.stderr_bytes |
|
196 |
+ assert tests.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr_bytes |
|
147 | 197 |