Marco Ricci commited on 2025-08-17 16:42:24
Zeige 2 geänderte Dateien mit 299 Einfügungen und 393 Löschungen.
(This is part 4 of a series of refactorings for the test suite.) For the `export vault` command-line interface tests, split the tests into tests for command-line argument support, tests for various command-line or format-related errors, and the already existing groups for "storeroom" and "vault v0.2"/"vault v0.3" format tests. For the former two groups, factor out the common test operation, which in both cases is the whole test content, though with differing call conventions. For the latter two groups, factor out the common environment setup instead. For the `exporter` subpackage tests, collect the data generation strategies in a common class `Strategies` (similar to the `Parametrize` class). Split the existing `TestUtilities` class into a `TestCLIUtilities` and a `TestExportVaultConfigDataHandlerRegistry` class, and factor out the common environment setup for each respective group. Rename the `TestCLI` class into `TestGenericVaultCLIErrors`, and factor out the common environment setup and the CLI call function.
... | ... |
@@ -34,7 +34,7 @@ from cryptography.hazmat.primitives.ciphers import ( # noqa: E402 |
34 | 34 |
) |
35 | 35 |
|
36 | 36 |
if TYPE_CHECKING: |
37 |
- from collections.abc import Callable |
|
37 |
+ from collections.abc import Callable, Iterator |
|
38 | 38 |
from typing import Any |
39 | 39 |
|
40 | 40 |
from typing_extensions import Buffer, Literal |
... | ... |
@@ -175,17 +175,16 @@ class Parametrize(types.SimpleNamespace): |
175 | 175 |
) |
176 | 176 |
|
177 | 177 |
|
178 |
-class TestCLI: |
|
178 |
+class TestCLIParameters: |
|
179 | 179 |
"""Test the command-line interface for `derivepassphrase export vault`.""" |
180 | 180 |
|
181 |
- def test_path_parameter(self) -> None: |
|
182 |
- """The path `VAULT_PATH` is supported. |
|
183 |
- |
|
184 |
- Using `VAULT_PATH` as the path looks up the actual path in the |
|
185 |
- `VAULT_PATH` environment variable. See |
|
186 |
- [`exporter.get_vault_path`][] for details. |
|
187 |
- |
|
188 |
- """ |
|
181 |
+ def _test( |
|
182 |
+ self, |
|
183 |
+ command_line: list[str], |
|
184 |
+ *, |
|
185 |
+ vault_config: str | bytes = data.VAULT_V03_CONFIG, |
|
186 |
+ config_data: dict[str, Any] = data.VAULT_V03_CONFIG_DATA, |
|
187 |
+ ) -> None: |
|
189 | 188 |
runner = machinery.CliRunner(mix_stderr=False) |
190 | 189 |
# TODO(the-13th-letter): Rewrite using parenthesized |
191 | 190 |
# with-statements. |
... | ... |
@@ -196,42 +195,33 @@ class TestCLI: |
196 | 195 |
pytest_machinery.isolated_vault_exporter_config( |
197 | 196 |
monkeypatch=monkeypatch, |
198 | 197 |
runner=runner, |
199 |
- vault_config=data.VAULT_V03_CONFIG, |
|
198 |
+ vault_config=vault_config, |
|
200 | 199 |
vault_key=data.VAULT_MASTER_KEY, |
201 | 200 |
) |
202 | 201 |
) |
203 |
- monkeypatch.setenv("VAULT_KEY", data.VAULT_MASTER_KEY) |
|
204 | 202 |
result = runner.invoke( |
205 | 203 |
cli.derivepassphrase_export_vault, |
206 |
- ["VAULT_PATH"], |
|
204 |
+ command_line, |
|
207 | 205 |
) |
208 | 206 |
assert result.clean_exit(empty_stderr=True), "expected clean exit" |
209 |
- assert json.loads(result.stdout) == data.VAULT_V03_CONFIG_DATA |
|
207 |
+ assert json.loads(result.stdout) == config_data |
|
208 |
+ |
|
209 |
+ def test_path(self) -> None: |
|
210 |
+ """The path `VAULT_PATH` is supported. |
|
211 |
+ |
|
212 |
+ Using `VAULT_PATH` as the path looks up the actual path in the |
|
213 |
+ `VAULT_PATH` environment variable. See |
|
214 |
+ [`exporter.get_vault_path`][] for details. |
|
215 |
+ |
|
216 |
+ """ |
|
217 |
+ self._test(["VAULT_PATH"]) |
|
210 | 218 |
|
211 |
- def test_key_parameter(self) -> None: |
|
219 |
+ def test_key(self) -> None: |
|
212 | 220 |
"""The `--key` option is supported.""" |
213 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
214 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
215 |
- # with-statements. |
|
216 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
217 |
- with contextlib.ExitStack() as stack: |
|
218 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
219 |
- stack.enter_context( |
|
220 |
- pytest_machinery.isolated_vault_exporter_config( |
|
221 |
- monkeypatch=monkeypatch, |
|
222 |
- runner=runner, |
|
223 |
- vault_config=data.VAULT_V03_CONFIG, |
|
224 |
- ) |
|
225 |
- ) |
|
226 |
- result = runner.invoke( |
|
227 |
- cli.derivepassphrase_export_vault, |
|
228 |
- ["-k", data.VAULT_MASTER_KEY, ".vault"], |
|
229 |
- ) |
|
230 |
- assert result.clean_exit(empty_stderr=True), "expected clean exit" |
|
231 |
- assert json.loads(result.stdout) == data.VAULT_V03_CONFIG_DATA |
|
221 |
+ self._test(["-k", data.VAULT_MASTER_KEY, ".vault"]) |
|
232 | 222 |
|
233 | 223 |
@pytest_machinery.Parametrize.VAULT_CONFIG_FORMATS_DATA |
234 |
- def test_load_vault_v02_v03_storeroom( |
|
224 |
+ def test_load_vault_specific_format( |
|
235 | 225 |
self, |
236 | 226 |
config: str | bytes, |
237 | 227 |
format: str, |
... | ... |
@@ -243,37 +233,28 @@ class TestCLI: |
243 | 233 |
vault` to only attempt decoding in that named format. |
244 | 234 |
|
245 | 235 |
""" |
246 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
247 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
248 |
- # with-statements. |
|
249 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
250 |
- with contextlib.ExitStack() as stack: |
|
251 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
252 |
- stack.enter_context( |
|
253 |
- pytest_machinery.isolated_vault_exporter_config( |
|
254 |
- monkeypatch=monkeypatch, |
|
255 |
- runner=runner, |
|
236 |
+ self._test( |
|
237 |
+ ["-f", format, "-k", data.VAULT_MASTER_KEY, "VAULT_PATH"], |
|
256 | 238 |
vault_config=config, |
239 |
+ config_data=config_data, |
|
257 | 240 |
) |
258 |
- ) |
|
259 |
- result = runner.invoke( |
|
260 |
- cli.derivepassphrase_export_vault, |
|
261 |
- [ |
|
262 |
- "-f", |
|
263 |
- format, |
|
264 |
- "-k", |
|
265 |
- data.VAULT_MASTER_KEY, |
|
266 |
- "VAULT_PATH", |
|
267 |
- ], |
|
268 |
- ) |
|
269 |
- assert result.clean_exit(empty_stderr=True), "expected clean exit" |
|
270 |
- assert json.loads(result.stdout) == config_data |
|
271 | 241 |
|
272 |
- def test_vault_config_not_found( |
|
242 |
+ |
|
243 |
+class TestCLIFailures: |
|
244 |
+ """Test the command-line interface for `derivepassphrase export vault`.""" |
|
245 |
+ |
|
246 |
+ @contextlib.contextmanager |
|
247 |
+ def _test( |
|
273 | 248 |
self, |
274 | 249 |
caplog: pytest.LogCaptureFixture, |
275 |
- ) -> None: |
|
276 |
- """Fail when trying to decode non-existant files/directories.""" |
|
250 |
+ command_line: list[str], |
|
251 |
+ /, |
|
252 |
+ error_message: str = "Cannot parse '.vault' " |
|
253 |
+ "as a valid vault-native config", |
|
254 |
+ *, |
|
255 |
+ vault_config: str | bytes = data.VAULT_V03_CONFIG, |
|
256 |
+ vault_key: str = data.VAULT_MASTER_KEY, |
|
257 |
+ ) -> Iterator[pytest.MonkeyPatch]: |
|
277 | 258 |
runner = machinery.CliRunner(mix_stderr=False) |
278 | 259 |
# TODO(the-13th-letter): Rewrite using parenthesized |
279 | 260 |
# with-statements. |
... | ... |
@@ -284,132 +265,72 @@ class TestCLI: |
284 | 265 |
pytest_machinery.isolated_vault_exporter_config( |
285 | 266 |
monkeypatch=monkeypatch, |
286 | 267 |
runner=runner, |
287 |
- vault_config=data.VAULT_V03_CONFIG, |
|
288 |
- vault_key=data.VAULT_MASTER_KEY, |
|
268 |
+ vault_config=vault_config, |
|
269 |
+ vault_key=vault_key, |
|
289 | 270 |
) |
290 | 271 |
) |
272 |
+ yield monkeypatch |
|
291 | 273 |
result = runner.invoke( |
292 | 274 |
cli.derivepassphrase_export_vault, |
293 |
- ["does-not-exist.txt"], |
|
275 |
+ command_line, |
|
294 | 276 |
) |
295 | 277 |
assert result.error_exit( |
296 |
- error=( |
|
297 |
- "Cannot parse 'does-not-exist.txt' " |
|
298 |
- "as a valid vault-native config" |
|
299 |
- ), |
|
278 |
+ error=error_message, |
|
300 | 279 |
record_tuples=caplog.record_tuples, |
301 | 280 |
), "expected error exit and known error message" |
302 | 281 |
assert data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
303 | 282 |
|
304 |
- def test_vault_config_invalid( |
|
283 |
+ def test_file_not_found( |
|
284 |
+ self, |
|
285 |
+ caplog: pytest.LogCaptureFixture, |
|
286 |
+ ) -> None: |
|
287 |
+ """Fail when trying to decode non-existant files/directories.""" |
|
288 |
+ with self._test( |
|
289 |
+ caplog, |
|
290 |
+ ["does-not-exist.txt"], |
|
291 |
+ error_message="Cannot parse 'does-not-exist.txt' " |
|
292 |
+ "as a valid vault-native config", |
|
293 |
+ ): |
|
294 |
+ pass |
|
295 |
+ |
|
296 |
+ def test_invalid_encrypted_contents( |
|
305 | 297 |
self, |
306 | 298 |
caplog: pytest.LogCaptureFixture, |
307 | 299 |
) -> None: |
308 | 300 |
"""Fail to parse invalid vault configurations (files).""" |
309 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
310 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
311 |
- # with-statements. |
|
312 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
313 |
- with contextlib.ExitStack() as stack: |
|
314 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
315 |
- stack.enter_context( |
|
316 |
- pytest_machinery.isolated_vault_exporter_config( |
|
317 |
- monkeypatch=monkeypatch, |
|
318 |
- runner=runner, |
|
319 |
- vault_config="", |
|
320 |
- vault_key=data.VAULT_MASTER_KEY, |
|
321 |
- ) |
|
322 |
- ) |
|
323 |
- result = runner.invoke( |
|
324 |
- cli.derivepassphrase_export_vault, |
|
325 |
- [".vault"], |
|
326 |
- ) |
|
327 |
- assert result.error_exit( |
|
328 |
- error="Cannot parse '.vault' as a valid vault-native config", |
|
329 |
- record_tuples=caplog.record_tuples, |
|
330 |
- ), "expected error exit and known error message" |
|
331 |
- assert data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
|
301 |
+ with self._test(caplog, [".vault"], vault_config=""): |
|
302 |
+ pass |
|
332 | 303 |
|
333 |
- def test_vault_config_invalid_just_a_directory( |
|
304 |
+ def test_just_a_directory( |
|
334 | 305 |
self, |
335 | 306 |
caplog: pytest.LogCaptureFixture, |
336 | 307 |
) -> None: |
337 | 308 |
"""Fail to parse invalid vault configurations (directories).""" |
338 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
339 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
340 |
- # with-statements. |
|
341 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
342 |
- with contextlib.ExitStack() as stack: |
|
343 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
344 |
- stack.enter_context( |
|
345 |
- pytest_machinery.isolated_vault_exporter_config( |
|
346 |
- monkeypatch=monkeypatch, |
|
347 |
- runner=runner, |
|
348 |
- vault_config="", |
|
349 |
- vault_key=data.VAULT_MASTER_KEY, |
|
350 |
- ) |
|
351 |
- ) |
|
309 |
+ with self._test(caplog, [".vault"], vault_config=""): |
|
352 | 310 |
p = pathlib.Path(".vault") |
353 | 311 |
p.unlink() |
354 | 312 |
p.mkdir() |
355 |
- result = runner.invoke( |
|
356 |
- cli.derivepassphrase_export_vault, |
|
357 |
- [str(p)], |
|
358 |
- ) |
|
359 |
- assert result.error_exit( |
|
360 |
- error="Cannot parse '.vault' as a valid vault-native config", |
|
361 |
- record_tuples=caplog.record_tuples, |
|
362 |
- ), "expected error exit and known error message" |
|
363 |
- assert data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
|
364 | 313 |
|
365 |
- def test_invalid_vault_config_bad_signature( |
|
314 |
+ def test_bad_signature( |
|
366 | 315 |
self, |
367 | 316 |
caplog: pytest.LogCaptureFixture, |
368 | 317 |
) -> None: |
369 | 318 |
"""Fail to parse vault configurations with invalid integrity checks.""" |
370 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
371 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
372 |
- # with-statements. |
|
373 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
374 |
- with contextlib.ExitStack() as stack: |
|
375 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
376 |
- stack.enter_context( |
|
377 |
- pytest_machinery.isolated_vault_exporter_config( |
|
378 |
- monkeypatch=monkeypatch, |
|
379 |
- runner=runner, |
|
380 |
- vault_config=data.VAULT_V02_CONFIG, |
|
381 |
- vault_key=data.VAULT_MASTER_KEY, |
|
382 |
- ) |
|
383 |
- ) |
|
384 |
- result = runner.invoke( |
|
385 |
- cli.derivepassphrase_export_vault, |
|
319 |
+ with self._test( |
|
320 |
+ caplog, |
|
386 | 321 |
["-f", "v0.3", ".vault"], |
387 |
- ) |
|
388 |
- assert result.error_exit( |
|
389 |
- error="Cannot parse '.vault' as a valid vault-native config", |
|
390 |
- record_tuples=caplog.record_tuples, |
|
391 |
- ), "expected error exit and known error message" |
|
392 |
- assert data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
|
322 |
+ vault_config=data.VAULT_V02_CONFIG, |
|
323 |
+ ): |
|
324 |
+ pass |
|
393 | 325 |
|
394 |
- def test_vault_config_invalid_internal( |
|
326 |
+ def test_invalid_decrypted_contents( |
|
395 | 327 |
self, |
396 | 328 |
caplog: pytest.LogCaptureFixture, |
397 | 329 |
) -> None: |
398 |
- """The decoded vault configuration data is valid.""" |
|
399 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
400 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
401 |
- # with-statements. |
|
402 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
403 |
- with contextlib.ExitStack() as stack: |
|
404 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
405 |
- stack.enter_context( |
|
406 |
- pytest_machinery.isolated_vault_exporter_config( |
|
407 |
- monkeypatch=monkeypatch, |
|
408 |
- runner=runner, |
|
409 |
- vault_config=data.VAULT_V03_CONFIG, |
|
410 |
- vault_key=data.VAULT_MASTER_KEY, |
|
411 |
- ) |
|
412 |
- ) |
|
330 |
+ """Fail to parse encrypted vault configurations with invalid plaintext.""" |
|
331 |
+ with self._test( |
|
332 |
+ caplog, [".vault"], error_message="Invalid vault config: " |
|
333 |
+ ) as monkeypatch: |
|
413 | 334 |
|
414 | 335 |
def export_vault_config_data(*_args: Any, **_kwargs: Any) -> None: |
415 | 336 |
return None |
... | ... |
@@ -419,20 +340,34 @@ class TestCLI: |
419 | 340 |
"export_vault_config_data", |
420 | 341 |
export_vault_config_data, |
421 | 342 |
) |
422 |
- result = runner.invoke( |
|
423 |
- cli.derivepassphrase_export_vault, |
|
424 |
- [".vault"], |
|
425 |
- ) |
|
426 |
- assert result.error_exit( |
|
427 |
- error="Invalid vault config: ", |
|
428 |
- record_tuples=caplog.record_tuples, |
|
429 |
- ), "expected error exit and known error message" |
|
430 |
- assert data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr |
|
431 | 343 |
|
432 | 344 |
|
433 | 345 |
class TestStoreroom: |
434 | 346 |
"""Test the "storeroom" handler and handler machinery.""" |
435 | 347 |
|
348 |
+ @contextlib.contextmanager |
|
349 |
+ def _setup_environment( |
|
350 |
+ self, |
|
351 |
+ *, |
|
352 |
+ vault_config: str | bytes = data.VAULT_STOREROOM_CONFIG_ZIPPED, |
|
353 |
+ vault_key: str = data.VAULT_MASTER_KEY, |
|
354 |
+ ) -> Iterator[tuple[pytest.MonkeyPatch, machinery.CliRunner]]: |
|
355 |
+ runner = machinery.CliRunner(mix_stderr=False) |
|
356 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
357 |
+ # with-statements. |
|
358 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
359 |
+ with contextlib.ExitStack() as stack: |
|
360 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
361 |
+ stack.enter_context( |
|
362 |
+ pytest_machinery.isolated_vault_exporter_config( |
|
363 |
+ monkeypatch=monkeypatch, |
|
364 |
+ runner=runner, |
|
365 |
+ vault_config=vault_config, |
|
366 |
+ vault_key=vault_key, |
|
367 |
+ ) |
|
368 |
+ ) |
|
369 |
+ yield monkeypatch, runner |
|
370 |
+ |
|
436 | 371 |
@Parametrize.PATH |
437 | 372 |
@Parametrize.KEY_FORMATS |
438 | 373 |
@Parametrize.STOREROOM_HANDLER |
... | ... |
@@ -448,20 +383,7 @@ class TestStoreroom: |
448 | 383 |
them as well. |
449 | 384 |
|
450 | 385 |
""" |
451 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
452 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
453 |
- # with-statements. |
|
454 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
455 |
- with contextlib.ExitStack() as stack: |
|
456 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
457 |
- stack.enter_context( |
|
458 |
- pytest_machinery.isolated_vault_exporter_config( |
|
459 |
- monkeypatch=monkeypatch, |
|
460 |
- runner=runner, |
|
461 |
- vault_config=data.VAULT_STOREROOM_CONFIG_ZIPPED, |
|
462 |
- vault_key=data.VAULT_MASTER_KEY, |
|
463 |
- ) |
|
464 |
- ) |
|
386 |
+ with self._setup_environment(): |
|
465 | 387 |
assert ( |
466 | 388 |
handler(path, key, format="storeroom") |
467 | 389 |
== data.VAULT_STOREROOM_CONFIG_DATA |
... | ... |
@@ -491,24 +413,12 @@ class TestStoreroom: |
491 | 413 |
wrong shape. |
492 | 414 |
|
493 | 415 |
""" |
494 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
495 | 416 |
master_keys = _types.StoreroomMasterKeys( |
496 | 417 |
encryption_key=bytes(storeroom.KEY_SIZE), |
497 | 418 |
signing_key=bytes(storeroom.KEY_SIZE), |
498 | 419 |
hashing_key=bytes(storeroom.KEY_SIZE), |
499 | 420 |
) |
500 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
501 |
- # with-statements. |
|
502 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
503 |
- with contextlib.ExitStack() as stack: |
|
504 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
505 |
- stack.enter_context( |
|
506 |
- pytest_machinery.isolated_vault_exporter_config( |
|
507 |
- monkeypatch=monkeypatch, |
|
508 |
- runner=runner, |
|
509 |
- vault_config=data.VAULT_STOREROOM_CONFIG_ZIPPED, |
|
510 |
- ) |
|
511 |
- ) |
|
421 |
+ with self._setup_environment(): |
|
512 | 422 |
p = pathlib.Path(".vault", "20") |
513 | 423 |
with p.open("w", encoding="UTF-8") as outfile: |
514 | 424 |
print(config, file=outfile) |
... | ... |
@@ -528,20 +438,7 @@ class TestStoreroom: |
528 | 438 |
These include unknown versions, and data of the wrong shape. |
529 | 439 |
|
530 | 440 |
""" |
531 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
532 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
533 |
- # with-statements. |
|
534 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
535 |
- with contextlib.ExitStack() as stack: |
|
536 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
537 |
- stack.enter_context( |
|
538 |
- pytest_machinery.isolated_vault_exporter_config( |
|
539 |
- monkeypatch=monkeypatch, |
|
540 |
- runner=runner, |
|
541 |
- vault_config=data.VAULT_STOREROOM_CONFIG_ZIPPED, |
|
542 |
- vault_key=data.VAULT_MASTER_KEY, |
|
543 |
- ) |
|
544 |
- ) |
|
441 |
+ with self._setup_environment(): |
|
545 | 442 |
p = pathlib.Path(".vault", ".keys") |
546 | 443 |
with p.open("w", encoding="UTF-8") as outfile: |
547 | 444 |
print(master_keys_data, file=outfile) |
... | ... |
@@ -570,21 +467,11 @@ class TestStoreroom: |
570 | 467 |
subdirectories. |
571 | 468 |
|
572 | 469 |
""" |
573 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
470 |
+ with self._setup_environment(vault_config=zipped_config): # noqa: SIM117 |
|
574 | 471 |
# TODO(the-13th-letter): Rewrite using parenthesized |
575 | 472 |
# with-statements. |
576 | 473 |
# https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
577 |
- with contextlib.ExitStack() as stack: |
|
578 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
579 |
- stack.enter_context( |
|
580 |
- pytest_machinery.isolated_vault_exporter_config( |
|
581 |
- monkeypatch=monkeypatch, |
|
582 |
- runner=runner, |
|
583 |
- vault_config=zipped_config, |
|
584 |
- vault_key=data.VAULT_MASTER_KEY, |
|
585 |
- ) |
|
586 |
- ) |
|
587 |
- stack.enter_context(pytest.raises(RuntimeError, match=error_text)) |
|
474 |
+ with pytest.raises(RuntimeError, match=error_text): |
|
588 | 475 |
handler(format="storeroom") |
589 | 476 |
|
590 | 477 |
def test_decrypt_keys_wrong_data_length(self) -> None: |
... | ... |
@@ -667,6 +554,29 @@ class TestStoreroom: |
667 | 554 |
class TestVaultNativeConfig: |
668 | 555 |
"""Test the vault-native handler and handler machinery.""" |
669 | 556 |
|
557 |
+ @contextlib.contextmanager |
|
558 |
+ def _setup_environment( |
|
559 |
+ self, |
|
560 |
+ *, |
|
561 |
+ vault_config: str | bytes = data.VAULT_V03_CONFIG, |
|
562 |
+ vault_key: str = data.VAULT_MASTER_KEY, |
|
563 |
+ ) -> Iterator[tuple[pytest.MonkeyPatch, machinery.CliRunner]]: |
|
564 |
+ runner = machinery.CliRunner(mix_stderr=False) |
|
565 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
566 |
+ # with-statements. |
|
567 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
568 |
+ with contextlib.ExitStack() as stack: |
|
569 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
570 |
+ stack.enter_context( |
|
571 |
+ pytest_machinery.isolated_vault_exporter_config( |
|
572 |
+ monkeypatch=monkeypatch, |
|
573 |
+ runner=runner, |
|
574 |
+ vault_config=vault_config, |
|
575 |
+ vault_key=vault_key, |
|
576 |
+ ) |
|
577 |
+ ) |
|
578 |
+ yield monkeypatch, runner |
|
579 |
+ |
|
670 | 580 |
@Parametrize.VAULT_NATIVE_PBKDF2_RESULT |
671 | 581 |
def test_pbkdf2_manually(self, iterations: int, result: bytes) -> None: |
672 | 582 |
"""The PBKDF2 helper function works.""" |
... | ... |
@@ -696,20 +606,7 @@ class TestVaultNativeConfig: |
696 | 606 |
no longer does. |
697 | 607 |
|
698 | 608 |
""" |
699 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
700 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
701 |
- # with-statements. |
|
702 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
703 |
- with contextlib.ExitStack() as stack: |
|
704 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
705 |
- stack.enter_context( |
|
706 |
- pytest_machinery.isolated_vault_exporter_config( |
|
707 |
- monkeypatch=monkeypatch, |
|
708 |
- runner=runner, |
|
709 |
- vault_config=config, |
|
710 |
- vault_key=data.VAULT_MASTER_KEY, |
|
711 |
- ) |
|
712 |
- ) |
|
609 |
+ with self._setup_environment(vault_config=config): |
|
713 | 610 |
if isinstance(config_data, type): |
714 | 611 |
with pytest.raises(config_data): |
715 | 612 |
handler(None, format=format) |
... | ... |
@@ -732,20 +629,7 @@ class TestVaultNativeConfig: |
732 | 629 |
them as well. |
733 | 630 |
|
734 | 631 |
""" |
735 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
736 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
737 |
- # with-statements. |
|
738 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
739 |
- with contextlib.ExitStack() as stack: |
|
740 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
741 |
- stack.enter_context( |
|
742 |
- pytest_machinery.isolated_vault_exporter_config( |
|
743 |
- monkeypatch=monkeypatch, |
|
744 |
- runner=runner, |
|
745 |
- vault_config=data.VAULT_V03_CONFIG, |
|
746 |
- vault_key=data.VAULT_MASTER_KEY, |
|
747 |
- ) |
|
748 |
- ) |
|
632 |
+ with self._setup_environment(): |
|
749 | 633 |
assert ( |
750 | 634 |
handler(path, key, format="v0.3") == data.VAULT_V03_CONFIG_DATA |
751 | 635 |
) |
... | ... |
@@ -766,19 +650,8 @@ class TestVaultNativeConfig: |
766 | 650 |
|
767 | 651 |
return func |
768 | 652 |
|
769 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
770 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
771 |
- # with-statements. |
|
772 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
773 |
- with contextlib.ExitStack() as stack: |
|
774 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
775 |
- stack.enter_context( |
|
776 |
- pytest_machinery.isolated_vault_exporter_config( |
|
777 |
- monkeypatch=monkeypatch, |
|
778 |
- runner=runner, |
|
779 |
- vault_config=config, |
|
780 |
- ) |
|
781 |
- ) |
|
653 |
+ with self._setup_environment(vault_config=config) as values: |
|
654 |
+ monkeypatch, _ = values |
|
782 | 655 |
parser = parser_class( |
783 | 656 |
base64.b64decode(config), data.VAULT_MASTER_KEY |
784 | 657 |
) |
... | ... |
@@ -10,7 +10,7 @@ import os |
10 | 10 |
import pathlib |
11 | 11 |
import string |
12 | 12 |
import types |
13 |
-from typing import TYPE_CHECKING, Any, NamedTuple |
|
13 |
+from typing import TYPE_CHECKING, NamedTuple, TypeVar |
|
14 | 14 |
|
15 | 15 |
import hypothesis |
16 | 16 |
import pytest |
... | ... |
@@ -21,7 +21,57 @@ from tests import data, machinery |
21 | 21 |
from tests.machinery import pytest as pytest_machinery |
22 | 22 |
|
23 | 23 |
if TYPE_CHECKING: |
24 |
- from typing_extensions import Buffer |
|
24 |
+ from collections.abc import Callable, Iterator |
|
25 |
+ |
|
26 |
+ from typing_extensions import Any, Buffer |
|
27 |
+ |
|
28 |
+ |
|
29 |
+class Strategies: |
|
30 |
+ @strategies.composite |
|
31 |
+ @staticmethod |
|
32 |
+ def names(draw: strategies.DrawFn) -> str: |
|
33 |
+ """Return a strategy for identifier names.""" |
|
34 |
+ first_letter = draw( |
|
35 |
+ strategies.text(string.ascii_letters, min_size=1, max_size=1), |
|
36 |
+ label="first_letter", |
|
37 |
+ ) |
|
38 |
+ rest = draw( |
|
39 |
+ strategies.text( |
|
40 |
+ string.ascii_letters + string.digits + "_-", max_size=23 |
|
41 |
+ ), |
|
42 |
+ label="rest", |
|
43 |
+ ) |
|
44 |
+ return first_letter + rest |
|
45 |
+ |
|
46 |
+ _T = TypeVar("_T") |
|
47 |
+ |
|
48 |
+ @strategies.composite |
|
49 |
+ @staticmethod |
|
50 |
+ def pairs_of_lists( |
|
51 |
+ draw: strategies.DrawFn, |
|
52 |
+ strat: strategies.SearchStrategy[_T], |
|
53 |
+ min_size: int = 1, |
|
54 |
+ max_size: int = 3, |
|
55 |
+ ) -> tuple[list[_T], list[_T]]: |
|
56 |
+ """Return a strategy for two short lists, with unique items.""" |
|
57 |
+ size1 = draw( |
|
58 |
+ strategies.integers(min_value=min_size, max_value=max_size), |
|
59 |
+ label="size1", |
|
60 |
+ ) |
|
61 |
+ size2 = draw( |
|
62 |
+ strategies.integers(min_value=min_size, max_value=max_size), |
|
63 |
+ label="size2", |
|
64 |
+ ) |
|
65 |
+ all_values = draw( |
|
66 |
+ strategies.lists( |
|
67 |
+ strat, |
|
68 |
+ min_size=size1 + size2, |
|
69 |
+ max_size=size1 + size2, |
|
70 |
+ unique=True, |
|
71 |
+ ), |
|
72 |
+ label="all_values", |
|
73 |
+ ) |
|
74 |
+ return all_values[:size1], all_values[size1:] |
|
25 | 75 |
|
26 | 76 |
|
27 | 77 |
class Parametrize(types.SimpleNamespace): |
... | ... |
@@ -51,8 +101,8 @@ class Parametrize(types.SimpleNamespace): |
51 | 101 |
) |
52 | 102 |
|
53 | 103 |
|
54 |
-class TestUtilities: |
|
55 |
- """Test the utility functions in the `exporter` subpackage.""" |
|
104 |
+class TestCLIUtilities: |
|
105 |
+ """Test the command-line utility functions in the `exporter` subpackage.""" |
|
56 | 106 |
|
57 | 107 |
class VaultKeyEnvironment(NamedTuple): |
58 | 108 |
"""An environment configuration for vault key determination. |
... | ... |
@@ -87,7 +137,7 @@ class TestUtilities: |
87 | 137 |
def strategy( |
88 | 138 |
draw: strategies.DrawFn, |
89 | 139 |
allow_missing: bool = False, |
90 |
- ) -> TestUtilities.VaultKeyEnvironment: |
|
140 |
+ ) -> TestCLIUtilities.VaultKeyEnvironment: |
|
91 | 141 |
"""Return a vault key environment configuration.""" |
92 | 142 |
text_strategy = strategies.text( |
93 | 143 |
strategies.characters(min_codepoint=32, max_codepoint=127), |
... | ... |
@@ -100,7 +150,7 @@ class TestUtilities: |
100 | 150 |
) |
101 | 151 |
num_fields = sum( |
102 | 152 |
1 |
103 |
- for f in TestUtilities.VaultKeyEnvironment._fields |
|
153 |
+ for f in TestCLIUtilities.VaultKeyEnvironment._fields |
|
104 | 154 |
if f != "expected" |
105 | 155 |
) |
106 | 156 |
env_vars: list[str | None] = draw( |
... | ... |
@@ -124,9 +174,22 @@ class TestUtilities: |
124 | 174 |
for value in reversed(env_vars): |
125 | 175 |
if value is not None: |
126 | 176 |
expected = value |
127 |
- return TestUtilities.VaultKeyEnvironment( |
|
128 |
- expected, *env_vars |
|
177 |
+ return TestCLIUtilities.VaultKeyEnvironment(expected, *env_vars) |
|
178 |
+ |
|
179 |
+ @contextlib.contextmanager |
|
180 |
+ def _setup_environment(self) -> Iterator[pytest.MonkeyPatch]: |
|
181 |
+ runner = machinery.CliRunner(mix_stderr=False) |
|
182 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
183 |
+ # with-statements. |
|
184 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
185 |
+ with contextlib.ExitStack() as stack: |
|
186 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
187 |
+ stack.enter_context( |
|
188 |
+ pytest_machinery.isolated_vault_exporter_config( |
|
189 |
+ monkeypatch=monkeypatch, runner=runner |
|
190 |
+ ) |
|
129 | 191 |
) |
192 |
+ yield monkeypatch |
|
130 | 193 |
|
131 | 194 |
@hypothesis.example( |
132 | 195 |
VaultKeyEnvironment("4username", None, None, None, "4username") |
... | ... |
@@ -206,22 +269,22 @@ class TestUtilities: |
206 | 269 |
("USER", user), |
207 | 270 |
("USERNAME", username), |
208 | 271 |
] |
209 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
210 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
211 |
- # with-statements. |
|
212 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
213 |
- with contextlib.ExitStack() as stack: |
|
214 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
215 |
- stack.enter_context( |
|
216 |
- pytest_machinery.isolated_vault_exporter_config( |
|
217 |
- monkeypatch=monkeypatch, runner=runner |
|
218 |
- ) |
|
219 |
- ) |
|
272 |
+ with self._setup_environment() as monkeypatch: |
|
220 | 273 |
for key, value in priority_list: |
221 | 274 |
if value is not None: |
222 | 275 |
monkeypatch.setenv(key, value) |
223 | 276 |
assert os.fsdecode(exporter.get_vault_key()) == expected |
224 | 277 |
|
278 |
+ def test_get_vault_key_without_envs(self) -> None: |
|
279 |
+ """Fail to look up the vault key in the empty environment.""" |
|
280 |
+ with pytest.MonkeyPatch.context() as monkeypatch: |
|
281 |
+ monkeypatch.delenv("VAULT_KEY", raising=False) |
|
282 |
+ monkeypatch.delenv("LOGNAME", raising=False) |
|
283 |
+ monkeypatch.delenv("USER", raising=False) |
|
284 |
+ monkeypatch.delenv("USERNAME", raising=False) |
|
285 |
+ with pytest.raises(KeyError, match="VAULT_KEY"): |
|
286 |
+ exporter.get_vault_key() |
|
287 |
+ |
|
225 | 288 |
@Parametrize.EXPECTED_VAULT_PATH |
226 | 289 |
def test_get_vault_path( |
227 | 290 |
self, |
... | ... |
@@ -233,17 +296,7 @@ class TestUtilities: |
233 | 296 |
Handle relative paths, absolute paths, and missing paths. |
234 | 297 |
|
235 | 298 |
""" |
236 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
237 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
238 |
- # with-statements. |
|
239 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
240 |
- with contextlib.ExitStack() as stack: |
|
241 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
242 |
- stack.enter_context( |
|
243 |
- pytest_machinery.isolated_vault_exporter_config( |
|
244 |
- monkeypatch=monkeypatch, runner=runner |
|
245 |
- ) |
|
246 |
- ) |
|
299 |
+ with self._setup_environment() as monkeypatch: |
|
247 | 300 |
if path: |
248 | 301 |
monkeypatch.setenv( |
249 | 302 |
"VAULT_PATH", os.fspath(path) if path is not None else None |
... | ... |
@@ -253,64 +306,6 @@ class TestUtilities: |
253 | 306 |
== expected.expanduser().resolve() |
254 | 307 |
) |
255 | 308 |
|
256 |
- @hypothesis.given( |
|
257 |
- name_data=strategies.lists( |
|
258 |
- strategies.integers(min_value=1, max_value=3), |
|
259 |
- min_size=2, |
|
260 |
- max_size=2, |
|
261 |
- ).flatmap( |
|
262 |
- lambda nm: strategies.lists( |
|
263 |
- strategies.builds( |
|
264 |
- operator.add, |
|
265 |
- strategies.sampled_from(string.ascii_letters), |
|
266 |
- strategies.text( |
|
267 |
- string.ascii_letters + string.digits + "_-", |
|
268 |
- max_size=23, |
|
269 |
- ), |
|
270 |
- ), |
|
271 |
- min_size=sum(nm), |
|
272 |
- max_size=sum(nm), |
|
273 |
- unique=True, |
|
274 |
- ).flatmap( |
|
275 |
- lambda list_: strategies.just((list_[nm[0] :], list_[: nm[0]])) |
|
276 |
- ) |
|
277 |
- ), |
|
278 |
- ) |
|
279 |
- def test_register_export_vault_config_data_handler( |
|
280 |
- self, name_data: tuple[list[str], list[str]] |
|
281 |
- ) -> None: |
|
282 |
- """Register vault config data export handlers.""" |
|
283 |
- |
|
284 |
- def handler( # pragma: no cover |
|
285 |
- path: str | bytes | os.PathLike | None = None, |
|
286 |
- key: str | Buffer | None = None, |
|
287 |
- *, |
|
288 |
- format: str, |
|
289 |
- ) -> Any: |
|
290 |
- del path, key |
|
291 |
- raise ValueError(format) |
|
292 |
- |
|
293 |
- names1, names2 = name_data |
|
294 |
- |
|
295 |
- with pytest.MonkeyPatch.context() as monkeypatch: |
|
296 |
- registry = dict.fromkeys(names1, handler) |
|
297 |
- monkeypatch.setattr( |
|
298 |
- exporter, "_export_vault_config_data_registry", registry |
|
299 |
- ) |
|
300 |
- dec = exporter.register_export_vault_config_data_handler(*names2) |
|
301 |
- assert dec(handler) == handler |
|
302 |
- assert registry == dict.fromkeys(names1 + names2, handler) |
|
303 |
- |
|
304 |
- def test_get_vault_key_without_envs(self) -> None: |
|
305 |
- """Fail to look up the vault key in the empty environment.""" |
|
306 |
- with pytest.MonkeyPatch.context() as monkeypatch: |
|
307 |
- monkeypatch.delenv("VAULT_KEY", raising=False) |
|
308 |
- monkeypatch.delenv("LOGNAME", raising=False) |
|
309 |
- monkeypatch.delenv("USER", raising=False) |
|
310 |
- monkeypatch.delenv("USERNAME", raising=False) |
|
311 |
- with pytest.raises(KeyError, match="VAULT_KEY"): |
|
312 |
- exporter.get_vault_key() |
|
313 |
- |
|
314 | 309 |
def test_get_vault_path_without_home(self) -> None: |
315 | 310 |
"""Fail to look up the vault path without `HOME`.""" |
316 | 311 |
|
... | ... |
@@ -325,6 +320,52 @@ class TestUtilities: |
325 | 320 |
): |
326 | 321 |
exporter.get_vault_path() |
327 | 322 |
|
323 |
+ |
|
324 |
+class TestExportVaultConfigDataHandlerRegistry: |
|
325 |
+ """Test the registry of `vault` config data exporters.""" |
|
326 |
+ |
|
327 |
+ @contextlib.contextmanager |
|
328 |
+ def _setup_environment( |
|
329 |
+ self, |
|
330 |
+ *, |
|
331 |
+ registry: dict[str, Any] | None = None, |
|
332 |
+ find_handlers: Callable | None = None, |
|
333 |
+ ) -> Iterator[pytest.MonkeyPatch]: |
|
334 |
+ with pytest.MonkeyPatch.context() as monkeypatch: |
|
335 |
+ if registry is not None: # pragma: no branch |
|
336 |
+ monkeypatch.setattr( |
|
337 |
+ exporter, "_export_vault_config_data_registry", registry |
|
338 |
+ ) |
|
339 |
+ if find_handlers is not None: # pragma: no branch |
|
340 |
+ monkeypatch.setattr( |
|
341 |
+ exporter, "find_vault_config_data_handlers", find_handlers |
|
342 |
+ ) |
|
343 |
+ yield monkeypatch |
|
344 |
+ |
|
345 |
+ @staticmethod |
|
346 |
+ def dummy_handler( # pragma: no cover |
|
347 |
+ path: str | bytes | os.PathLike | None = None, |
|
348 |
+ key: str | Buffer | None = None, |
|
349 |
+ *, |
|
350 |
+ format: str, |
|
351 |
+ ) -> Any: |
|
352 |
+ del path, key |
|
353 |
+ raise ValueError(format) |
|
354 |
+ |
|
355 |
+ @hypothesis.given(name_data=Strategies.pairs_of_lists(Strategies.names())) |
|
356 |
+ def test_register_export_vault_config_data_handler( |
|
357 |
+ self, name_data: tuple[list[str], list[str]] |
|
358 |
+ ) -> None: |
|
359 |
+ """Register vault config data export handlers.""" |
|
360 |
+ names1, names2 = name_data |
|
361 |
+ registry = dict.fromkeys(names1, self.dummy_handler) |
|
362 |
+ with self._setup_environment(registry=registry): |
|
363 |
+ dec = exporter.register_export_vault_config_data_handler(*names2) |
|
364 |
+ assert dec(self.dummy_handler) == self.dummy_handler |
|
365 |
+ assert registry == dict.fromkeys( |
|
366 |
+ names1 + names2, self.dummy_handler |
|
367 |
+ ) |
|
368 |
+ |
|
328 | 369 |
@Parametrize.EXPORT_VAULT_CONFIG_DATA_HANDLER_NAMELISTS |
329 | 370 |
def test_register_export_vault_config_data_handler_errors( |
330 | 371 |
self, |
... | ... |
@@ -338,34 +379,21 @@ class TestUtilities: |
338 | 379 |
|
339 | 380 |
""" |
340 | 381 |
|
341 |
- def handler( # pragma: no cover |
|
342 |
- path: str | bytes | os.PathLike | None = None, |
|
343 |
- key: str | Buffer | None = None, |
|
344 |
- *, |
|
345 |
- format: str, |
|
346 |
- ) -> Any: |
|
347 |
- del path, key |
|
348 |
- raise ValueError(format) |
|
349 |
- |
|
350 |
- with pytest.MonkeyPatch.context() as monkeypatch: |
|
351 |
- registry = {"dummy": handler} |
|
352 |
- monkeypatch.setattr( |
|
353 |
- exporter, "_export_vault_config_data_registry", registry |
|
354 |
- ) |
|
382 |
+ with self._setup_environment(registry={"dummy": self.dummy_handler}): # noqa: SIM117 |
|
383 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
384 |
+ # with-statements. |
|
385 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
355 | 386 |
with pytest.raises(ValueError, match=err_pat): |
356 | 387 |
exporter.register_export_vault_config_data_handler(*namelist)( |
357 |
- handler |
|
388 |
+ self.dummy_handler |
|
358 | 389 |
) |
359 | 390 |
|
360 | 391 |
def test_export_vault_config_data_bad_handler(self) -> None: |
361 | 392 |
"""Fail to export vault config data without known handlers.""" |
362 |
- with pytest.MonkeyPatch.context() as monkeypatch: |
|
363 |
- monkeypatch.setattr( |
|
364 |
- exporter, "_export_vault_config_data_registry", {} |
|
365 |
- ) |
|
366 |
- monkeypatch.setattr( |
|
367 |
- exporter, "find_vault_config_data_handlers", lambda: None |
|
368 |
- ) |
|
393 |
+ with self._setup_environment(registry={}, find_handlers=lambda: None): # noqa: SIM117 |
|
394 |
+ # TODO(the-13th-letter): Rewrite using parenthesized |
|
395 |
+ # with-statements. |
|
396 |
+ # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
369 | 397 |
with pytest.raises( |
370 | 398 |
ValueError, |
371 | 399 |
match=r"Invalid vault native configuration format", |
... | ... |
@@ -373,11 +401,18 @@ class TestUtilities: |
373 | 401 |
exporter.export_vault_config_data(format="v0.3") |
374 | 402 |
|
375 | 403 |
|
376 |
-class TestCLI: |
|
377 |
- """Test the command-line functionality of the `exporter` subpackage.""" |
|
404 |
+class TestGenericVaultCLIErrors: |
|
405 |
+ """Test errors in the `derivepassphrase export vault` subpackage. |
|
378 | 406 |
|
379 |
- def test_invalid_format(self) -> None: |
|
380 |
- """Reject invalid vault configuration format names.""" |
|
407 |
+ These errors are always possible, even if the `export` extra is not |
|
408 |
+ available. |
|
409 |
+ |
|
410 |
+ """ |
|
411 |
+ |
|
412 |
+ @contextlib.contextmanager |
|
413 |
+ def _setup_environment( |
|
414 |
+ self, *, vault_config: str | bytes = data.VAULT_V03_CONFIG |
|
415 |
+ ) -> Iterator[tuple[pytest.MonkeyPatch, machinery.CliRunner]]: |
|
381 | 416 |
runner = machinery.CliRunner(mix_stderr=False) |
382 | 417 |
# TODO(the-13th-letter): Rewrite using parenthesized |
383 | 418 |
# with-statements. |
... | ... |
@@ -388,15 +423,29 @@ class TestCLI: |
388 | 423 |
pytest_machinery.isolated_vault_exporter_config( |
389 | 424 |
monkeypatch=monkeypatch, |
390 | 425 |
runner=runner, |
391 |
- vault_config=data.VAULT_V03_CONFIG, |
|
426 |
+ vault_config=vault_config, |
|
392 | 427 |
vault_key=data.VAULT_MASTER_KEY, |
393 | 428 |
) |
394 | 429 |
) |
395 |
- result = runner.invoke( |
|
430 |
+ yield monkeypatch, runner |
|
431 |
+ |
|
432 |
+ def _call_cli( |
|
433 |
+ self, |
|
434 |
+ command_line: list[str], |
|
435 |
+ *, |
|
436 |
+ vault_config: str | bytes = data.VAULT_V03_CONFIG, |
|
437 |
+ ) -> machinery.ReadableResult: |
|
438 |
+ with self._setup_environment(vault_config=vault_config) as contexts: |
|
439 |
+ _, runner = contexts |
|
440 |
+ return runner.invoke( |
|
396 | 441 |
cli.derivepassphrase_export_vault, |
397 |
- ["-f", "INVALID", "VAULT_PATH"], |
|
442 |
+ command_line, |
|
398 | 443 |
catch_exceptions=False, |
399 | 444 |
) |
445 |
+ |
|
446 |
+ def test_invalid_format(self) -> None: |
|
447 |
+ """Reject invalid vault configuration format names.""" |
|
448 |
+ result = self._call_cli(["-f", "INVALID", "VAULT_PATH"]) |
|
400 | 449 |
for snippet in ("Invalid value for", "-f", "--format", "INVALID"): |
401 | 450 |
assert result.error_exit(error=snippet), ( |
402 | 451 |
"expected error exit and known error message" |
... | ... |
@@ -413,24 +462,8 @@ class TestCLI: |
413 | 462 |
) -> None: |
414 | 463 |
"""Abort export call if no cryptography is available.""" |
415 | 464 |
del config_data |
416 |
- runner = machinery.CliRunner(mix_stderr=False) |
|
417 |
- # TODO(the-13th-letter): Rewrite using parenthesized |
|
418 |
- # with-statements. |
|
419 |
- # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 |
|
420 |
- with contextlib.ExitStack() as stack: |
|
421 |
- monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
422 |
- stack.enter_context( |
|
423 |
- pytest_machinery.isolated_vault_exporter_config( |
|
424 |
- monkeypatch=monkeypatch, |
|
425 |
- runner=runner, |
|
426 |
- vault_config=config, |
|
427 |
- vault_key=data.VAULT_MASTER_KEY, |
|
428 |
- ) |
|
429 |
- ) |
|
430 |
- result = runner.invoke( |
|
431 |
- cli.derivepassphrase_export_vault, |
|
432 |
- ["-f", format, "VAULT_PATH"], |
|
433 |
- catch_exceptions=False, |
|
465 |
+ result = self._call_cli( |
|
466 |
+ ["-f", format, "VAULT_PATH"], vault_config=config |
|
434 | 467 |
) |
435 | 468 |
assert result.error_exit( |
436 | 469 |
error=data.CANNOT_LOAD_CRYPTOGRAPHY, |
437 | 470 |