Marco Ricci commited on 2024-09-11 22:13:56
Zeige 3 geänderte Dateien mit 365 Einfügungen und 29 Löschungen.
In preparation for multiple passphrase derivation schemes with separate settings, use multiple configuration files for each part of the application ("subsystem"). The only currently established subsystem is "vault", using `vault.json`, and the existing configuration effectively corresponds to a "settings" subsystem. Until v1.0 is released, fall back to `settings.json` if `vault.json` does not exist. (But include a deprecation warning, and attempt to migrate to the new filename automatically.)
... | ... |
@@ -340,20 +340,48 @@ def derivepassphrase_export_vault( |
340 | 340 |
# ===== |
341 | 341 |
|
342 | 342 |
|
343 |
-def _config_filename() -> str | bytes | pathlib.Path: |
|
344 |
- """Return the filename of the configuration file. |
|
343 |
+def _config_filename( |
|
344 |
+ subsystem: str | None = 'settings', |
|
345 |
+) -> str | bytes | pathlib.Path: |
|
346 |
+ """Return the filename of the configuration file for the subsystem. |
|
347 |
+ |
|
348 |
+ The (implicit default) file is currently named `settings.json`, |
|
349 |
+ located within the configuration directory as determined by the |
|
350 |
+ `DERIVEPASSPHRASE_PATH` environment variable, or by |
|
351 |
+ [`click.get_app_dir`][] in POSIX mode. Depending on the requested |
|
352 |
+ subsystem, this will usually be a different file within that |
|
353 |
+ directory. |
|
345 | 354 |
|
346 |
- The file is currently named `settings.json`, located within the |
|
347 |
- configuration directory as determined by the `DERIVEPASSPHRASE_PATH` |
|
348 |
- environment variable, or by [`click.get_app_dir`][] in POSIX |
|
349 |
- mode. |
|
355 |
+ Args: |
|
356 |
+ subsystem: |
|
357 |
+ Name of the configuration subsystem whose configuration |
|
358 |
+ filename to return. If not given, return the old filename |
|
359 |
+ from before the subcommand migration. If `None`, return the |
|
360 |
+ configuration directory instead. |
|
361 |
+ |
|
362 |
+ Raises: |
|
363 |
+ AssertionError: |
|
364 |
+ An unknown subsystem was passed. |
|
365 |
+ |
|
366 |
+ Deprecated: |
|
367 |
+ Since v0.2.0: The implicit default subsystem and the old |
|
368 |
+ configuration filename are deprecated, and will be removed in v1.0. |
|
369 |
+ The subsystem will be mandatory to specify. |
|
350 | 370 |
|
351 | 371 |
""" |
352 | 372 |
path: str | bytes | pathlib.Path |
353 | 373 |
path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir( |
354 | 374 |
PROG_NAME, force_posix=True |
355 | 375 |
) |
356 |
- return os.path.join(path, 'settings.json') |
|
376 |
+ match subsystem: |
|
377 |
+ case None: |
|
378 |
+ return path |
|
379 |
+ case 'vault' | 'settings': |
|
380 |
+ filename = f'{subsystem}.json' |
|
381 |
+ case _: # pragma: no cover |
|
382 |
+ msg = f'Unknown configuration subsystem: {subsystem!r}' |
|
383 |
+ raise AssertionError(msg) |
|
384 |
+ return os.path.join(path, filename) |
|
357 | 385 |
|
358 | 386 |
|
359 | 387 |
def _load_config() -> _types.VaultConfig: |
... | ... |
@@ -375,7 +403,7 @@ def _load_config() -> _types.VaultConfig: |
375 | 403 |
config. |
376 | 404 |
|
377 | 405 |
""" |
378 |
- filename = _config_filename() |
|
406 |
+ filename = _config_filename(subsystem='vault') |
|
379 | 407 |
with open(filename, 'rb') as fileobj: |
380 | 408 |
data = json.load(fileobj) |
381 | 409 |
if not _types.is_vault_config(data): |
... | ... |
@@ -383,6 +411,43 @@ def _load_config() -> _types.VaultConfig: |
383 | 411 |
return data |
384 | 412 |
|
385 | 413 |
|
414 |
+def _migrate_and_load_old_config() -> ( |
|
415 |
+ tuple[_types.VaultConfig, OSError | None] |
|
416 |
+): |
|
417 |
+ """Load and migrate a vault(1)-compatible config. |
|
418 |
+ |
|
419 |
+ The (old) filename is obtained via |
|
420 |
+ [`derivepassphrase.cli._config_filename`][]. This must be an |
|
421 |
+ unencrypted JSON file. After loading, the file is migrated to the new |
|
422 |
+ standard filename. |
|
423 |
+ |
|
424 |
+ Returns: |
|
425 |
+ The vault settings, and an optional exception encountered during |
|
426 |
+ migration. See [`derivepassphrase.types.VaultConfig`][] for |
|
427 |
+ details on the former. |
|
428 |
+ |
|
429 |
+ Raises: |
|
430 |
+ OSError: |
|
431 |
+ There was an OS error accessing the old file. |
|
432 |
+ ValueError: |
|
433 |
+ The data loaded from the file is not a vault(1)-compatible |
|
434 |
+ config. |
|
435 |
+ |
|
436 |
+ """ |
|
437 |
+ new_filename = _config_filename(subsystem='vault') |
|
438 |
+ old_filename = _config_filename() |
|
439 |
+ with open(old_filename, 'rb') as fileobj: |
|
440 |
+ data = json.load(fileobj) |
|
441 |
+ if not _types.is_vault_config(data): |
|
442 |
+ raise ValueError(_INVALID_VAULT_CONFIG) |
|
443 |
+ try: |
|
444 |
+ os.replace(old_filename, new_filename) |
|
445 |
+ except OSError as exc: |
|
446 |
+ return data, exc |
|
447 |
+ else: |
|
448 |
+ return data, None |
|
449 |
+ |
|
450 |
+ |
|
386 | 451 |
def _save_config(config: _types.VaultConfig, /) -> None: |
387 | 452 |
"""Save a vault(1)-compatible config to the application directory. |
388 | 453 |
|
... | ... |
@@ -403,7 +468,7 @@ def _save_config(config: _types.VaultConfig, /) -> None: |
403 | 468 |
""" |
404 | 469 |
if not _types.is_vault_config(config): |
405 | 470 |
raise ValueError(_INVALID_VAULT_CONFIG) |
406 |
- filename = _config_filename() |
|
471 |
+ filename = _config_filename(subsystem='vault') |
|
407 | 472 |
filedir = os.path.dirname(os.path.abspath(filename)) |
408 | 473 |
try: |
409 | 474 |
os.makedirs(filedir, exist_ok=False) |
... | ... |
@@ -1203,8 +1268,36 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1203 | 1268 |
def get_config() -> _types.VaultConfig: |
1204 | 1269 |
try: |
1205 | 1270 |
return _load_config() |
1271 |
+ except FileNotFoundError: |
|
1272 |
+ try: |
|
1273 |
+ backup_config, exc = _migrate_and_load_old_config() |
|
1206 | 1274 |
except FileNotFoundError: |
1207 | 1275 |
return {'services': {}} |
1276 |
+ old_name = os.path.basename(_config_filename()) |
|
1277 |
+ new_name = os.path.basename(_config_filename(subsystem='vault')) |
|
1278 |
+ click.echo( |
|
1279 |
+ ( |
|
1280 |
+ f'{PROG_NAME}: Using deprecated v0.1-style config file ' |
|
1281 |
+ f'{old_name!r}, instead of v0.2-style {new_name!r}. ' |
|
1282 |
+ f'Support for v0.1-style config filenames will be ' |
|
1283 |
+ f'removed in v1.0.' |
|
1284 |
+ ), |
|
1285 |
+ err=True, |
|
1286 |
+ ) |
|
1287 |
+ if isinstance(exc, OSError): |
|
1288 |
+ click.echo( |
|
1289 |
+ ( |
|
1290 |
+ f'{PROG_NAME}: Warning: Failed to migrate to ' |
|
1291 |
+ f'{new_name!r}: {exc.strerror}: {exc.filename!r}' |
|
1292 |
+ ), |
|
1293 |
+ err=True, |
|
1294 |
+ ) |
|
1295 |
+ else: |
|
1296 |
+ click.echo( |
|
1297 |
+ f'{PROG_NAME}: Successfully migrated to {new_name!r}.', |
|
1298 |
+ err=True, |
|
1299 |
+ ) |
|
1300 |
+ return backup_config |
|
1208 | 1301 |
except OSError as e: |
1209 | 1302 |
err(f'Cannot load config: {e.strerror}: {e.filename!r}') |
1210 | 1303 |
except Exception as e: # noqa: BLE001 |
... | ... |
@@ -552,7 +552,8 @@ def isolated_config( |
552 | 552 |
monkeypatch.setenv('HOME', os.getcwd()) |
553 | 553 |
monkeypatch.setenv('USERPROFILE', os.getcwd()) |
554 | 554 |
monkeypatch.delenv(env_name, raising=False) |
555 |
- os.makedirs(os.path.dirname(cli._config_filename()), exist_ok=True) |
|
555 |
+ config_dir = cli._config_filename(subsystem=None) |
|
556 |
+ os.makedirs(config_dir, exist_ok=True) |
|
556 | 557 |
yield |
557 | 558 |
|
558 | 559 |
|
... | ... |
@@ -563,7 +564,8 @@ def isolated_vault_config( |
563 | 564 |
config: Any, |
564 | 565 |
) -> Iterator[None]: |
565 | 566 |
with isolated_config(monkeypatch=monkeypatch, runner=runner): |
566 |
- with open(cli._config_filename(), 'w', encoding='UTF-8') as outfile: |
|
567 |
+ config_filename = cli._config_filename(subsystem='vault') |
|
568 |
+ with open(config_filename, 'w', encoding='UTF-8') as outfile: |
|
567 | 569 |
json.dump(config, outfile) |
568 | 570 |
yield |
569 | 571 |
|
... | ... |
@@ -620,10 +620,10 @@ class TestCLI: |
620 | 620 |
# We also might as well use `isolated_config` instead. |
621 | 621 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
622 | 622 |
with open( |
623 |
- cli._config_filename(), 'w', encoding='UTF-8' |
|
623 |
+ cli._config_filename(subsystem='vault'), 'w', encoding='UTF-8' |
|
624 | 624 |
) as outfile: |
625 | 625 |
print('This string is not valid JSON.', file=outfile) |
626 |
- dname = os.path.dirname(cli._config_filename()) |
|
626 |
+ dname = cli._config_filename(subsystem=None) |
|
627 | 627 |
_result = runner.invoke( |
628 | 628 |
cli.derivepassphrase_vault, |
629 | 629 |
['--import', os.fsdecode(dname)], |
... | ... |
@@ -641,7 +641,7 @@ class TestCLI: |
641 | 641 |
runner = click.testing.CliRunner(mix_stderr=False) |
642 | 642 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
643 | 643 |
with contextlib.suppress(FileNotFoundError): |
644 |
- os.remove(cli._config_filename()) |
|
644 |
+ os.remove(cli._config_filename(subsystem='vault')) |
|
645 | 645 |
_result = runner.invoke( |
646 | 646 |
cli.derivepassphrase_vault, |
647 | 647 |
['--export', '-'], |
... | ... |
@@ -676,8 +676,8 @@ class TestCLI: |
676 | 676 |
runner = click.testing.CliRunner(mix_stderr=False) |
677 | 677 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
678 | 678 |
with contextlib.suppress(FileNotFoundError): |
679 |
- os.remove(cli._config_filename()) |
|
680 |
- os.makedirs(cli._config_filename()) |
|
679 |
+ os.remove(cli._config_filename(subsystem='vault')) |
|
680 |
+ os.makedirs(cli._config_filename(subsystem='vault')) |
|
681 | 681 |
_result = runner.invoke( |
682 | 682 |
cli.derivepassphrase_vault, |
683 | 683 |
['--export', '-'], |
... | ... |
@@ -695,7 +695,7 @@ class TestCLI: |
695 | 695 |
) -> None: |
696 | 696 |
runner = click.testing.CliRunner(mix_stderr=False) |
697 | 697 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
698 |
- dname = os.path.dirname(cli._config_filename()) |
|
698 |
+ dname = cli._config_filename(subsystem=None) |
|
699 | 699 |
_result = runner.invoke( |
700 | 700 |
cli.derivepassphrase_vault, |
701 | 701 |
['--export', os.fsdecode(dname)], |
... | ... |
@@ -750,7 +750,9 @@ contents go here |
750 | 750 |
) |
751 | 751 |
result = tests.ReadableResult.parse(_result) |
752 | 752 |
assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
753 |
- with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
753 |
+ with open( |
|
754 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
755 |
+ ) as infile: |
|
754 | 756 |
config = json.load(infile) |
755 | 757 |
assert config == { |
756 | 758 |
'global': {'phrase': 'abc'}, |
... | ... |
@@ -774,7 +776,9 @@ contents go here |
774 | 776 |
) |
775 | 777 |
result = tests.ReadableResult.parse(_result) |
776 | 778 |
assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
777 |
- with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
779 |
+ with open( |
|
780 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
781 |
+ ) as infile: |
|
778 | 782 |
config = json.load(infile) |
779 | 783 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}} |
780 | 784 |
|
... | ... |
@@ -795,7 +799,9 @@ contents go here |
795 | 799 |
) |
796 | 800 |
result = tests.ReadableResult.parse(_result) |
797 | 801 |
assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
798 |
- with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
802 |
+ with open( |
|
803 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
804 |
+ ) as infile: |
|
799 | 805 |
config = json.load(infile) |
800 | 806 |
assert config == { |
801 | 807 |
'global': {'phrase': 'abc'}, |
... | ... |
@@ -821,7 +827,9 @@ contents go here |
821 | 827 |
assert result.error_exit( |
822 | 828 |
error='user aborted request' |
823 | 829 |
), 'expected known error message' |
824 |
- with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
830 |
+ with open( |
|
831 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
832 |
+ ) as infile: |
|
825 | 833 |
config = json.load(infile) |
826 | 834 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}} |
827 | 835 |
|
... | ... |
@@ -888,7 +896,9 @@ contents go here |
888 | 896 |
) |
889 | 897 |
result = tests.ReadableResult.parse(_result) |
890 | 898 |
assert result.clean_exit(), 'expected clean exit' |
891 |
- with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
899 |
+ with open( |
|
900 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
901 |
+ ) as infile: |
|
892 | 902 |
config = json.load(infile) |
893 | 903 |
assert ( |
894 | 904 |
config == result_config |
... | ... |
@@ -1015,7 +1025,7 @@ contents go here |
1015 | 1025 |
config={'global': {'phrase': 'abc'}, 'services': {}}, |
1016 | 1026 |
): |
1017 | 1027 |
tests.make_file_readonly( |
1018 |
- cli._config_filename(), |
|
1028 |
+ cli._config_filename(subsystem='vault'), |
|
1019 | 1029 |
try_race_free_implementation=try_race_free_implementation, |
1020 | 1030 |
) |
1021 | 1031 |
_result = runner.invoke( |
... | ... |
@@ -1118,7 +1128,9 @@ contents go here |
1118 | 1128 |
result.stderr == 'Passphrase:' |
1119 | 1129 |
), 'program unexpectedly failed?!' |
1120 | 1130 |
assert os_makedirs_called, 'os.makedirs has not been called?!' |
1121 |
- with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
1131 |
+ with open( |
|
1132 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
1133 |
+ ) as infile: |
|
1122 | 1134 |
config_readback = json.load(infile) |
1123 | 1135 |
assert config_readback == { |
1124 | 1136 |
'global': {'phrase': 'abc'}, |
... | ... |
@@ -1283,7 +1295,38 @@ contents go here |
1283 | 1295 |
|
1284 | 1296 |
|
1285 | 1297 |
class TestCLIUtils: |
1286 |
- def test_100_save_bad_config( |
|
1298 |
+ @pytest.mark.parametrize( |
|
1299 |
+ 'config', |
|
1300 |
+ [ |
|
1301 |
+ {'global': {'phrase': 'my passphrase'}, 'services': {}}, |
|
1302 |
+ {'global': {'key': DUMMY_KEY1_B64}, 'services': {}}, |
|
1303 |
+ { |
|
1304 |
+ 'global': {'phrase': 'abc'}, |
|
1305 |
+ 'services': {'sv': {'phrase': 'my passphrase'}}, |
|
1306 |
+ }, |
|
1307 |
+ { |
|
1308 |
+ 'global': {'phrase': 'abc'}, |
|
1309 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64}}, |
|
1310 |
+ }, |
|
1311 |
+ { |
|
1312 |
+ 'global': {'phrase': 'abc'}, |
|
1313 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}, |
|
1314 |
+ }, |
|
1315 |
+ ], |
|
1316 |
+ ) |
|
1317 |
+ def test_100_load_config( |
|
1318 |
+ self, monkeypatch: pytest.MonkeyPatch, config: Any |
|
1319 |
+ ) -> None: |
|
1320 |
+ runner = click.testing.CliRunner() |
|
1321 |
+ with tests.isolated_vault_config( |
|
1322 |
+ monkeypatch=monkeypatch, runner=runner, config=config |
|
1323 |
+ ): |
|
1324 |
+ config_filename = cli._config_filename(subsystem='vault') |
|
1325 |
+ with open(config_filename, encoding='UTF-8') as fileobj: |
|
1326 |
+ assert json.load(fileobj) == config |
|
1327 |
+ assert cli._load_config() == config |
|
1328 |
+ |
|
1329 |
+ def test_110_save_bad_config( |
|
1287 | 1330 |
self, monkeypatch: pytest.MonkeyPatch |
1288 | 1331 |
) -> None: |
1289 | 1332 |
runner = click.testing.CliRunner() |
... | ... |
@@ -1295,7 +1338,7 @@ class TestCLIUtils: |
1295 | 1338 |
): |
1296 | 1339 |
cli._save_config(None) # type: ignore[arg-type] |
1297 | 1340 |
|
1298 |
- def test_101_prompt_for_selection_multiple(self) -> None: |
|
1341 |
+ def test_111_prompt_for_selection_multiple(self) -> None: |
|
1299 | 1342 |
@click.command() |
1300 | 1343 |
@click.option('--heading', default='Our menu:') |
1301 | 1344 |
@click.argument('items', nargs=-1) |
... | ... |
@@ -1370,7 +1413,7 @@ Your selection? (1-10, leave empty to abort):\x20 |
1370 | 1413 |
""" # noqa: E501 |
1371 | 1414 |
), 'expected known output' |
1372 | 1415 |
|
1373 |
- def test_102_prompt_for_selection_single(self) -> None: |
|
1416 |
+ def test_112_prompt_for_selection_single(self) -> None: |
|
1374 | 1417 |
@click.command() |
1375 | 1418 |
@click.option('--item', default='baked beans') |
1376 | 1419 |
@click.argument('prompt') |
... | ... |
@@ -1415,7 +1458,7 @@ Boo. |
1415 | 1458 |
""" |
1416 | 1459 |
), 'expected known output' |
1417 | 1460 |
|
1418 |
- def test_103_prompt_for_passphrase( |
|
1461 |
+ def test_113_prompt_for_passphrase( |
|
1419 | 1462 |
self, monkeypatch: pytest.MonkeyPatch |
1420 | 1463 |
) -> None: |
1421 | 1464 |
monkeypatch.setattr( |
... | ... |
@@ -1480,7 +1523,9 @@ Boo. |
1480 | 1523 |
assert result.clean_exit( |
1481 | 1524 |
empty_stderr=True |
1482 | 1525 |
), 'expected clean exit' |
1483 |
- with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
1526 |
+ with open( |
|
1527 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
1528 |
+ ) as infile: |
|
1484 | 1529 |
config_readback = json.load(infile) |
1485 | 1530 |
assert config_readback == result_config |
1486 | 1531 |
|
... | ... |
@@ -1611,6 +1656,127 @@ class TestCLITransition: |
1611 | 1656 |
empty_stderr=True, output='Use NUMBER=0, e.g. "--symbol 0"' |
1612 | 1657 |
), 'expected clean exit, and option group epilog in help text' |
1613 | 1658 |
|
1659 |
+ @pytest.mark.parametrize( |
|
1660 |
+ 'config', |
|
1661 |
+ [ |
|
1662 |
+ {'global': {'phrase': 'my passphrase'}, 'services': {}}, |
|
1663 |
+ {'global': {'key': DUMMY_KEY1_B64}, 'services': {}}, |
|
1664 |
+ { |
|
1665 |
+ 'global': {'phrase': 'abc'}, |
|
1666 |
+ 'services': {'sv': {'phrase': 'my passphrase'}}, |
|
1667 |
+ }, |
|
1668 |
+ { |
|
1669 |
+ 'global': {'phrase': 'abc'}, |
|
1670 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64}}, |
|
1671 |
+ }, |
|
1672 |
+ { |
|
1673 |
+ 'global': {'phrase': 'abc'}, |
|
1674 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}, |
|
1675 |
+ }, |
|
1676 |
+ ], |
|
1677 |
+ ) |
|
1678 |
+ def test_110_load_config_backup( |
|
1679 |
+ self, monkeypatch: pytest.MonkeyPatch, config: Any |
|
1680 |
+ ) -> None: |
|
1681 |
+ runner = click.testing.CliRunner() |
|
1682 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
|
1683 |
+ config_filename = cli._config_filename() |
|
1684 |
+ with open(config_filename, 'w', encoding='UTF-8') as fileobj: |
|
1685 |
+ print(json.dumps(config, indent=2), file=fileobj) |
|
1686 |
+ assert cli._migrate_and_load_old_config()[0] == config |
|
1687 |
+ |
|
1688 |
+ @pytest.mark.parametrize( |
|
1689 |
+ 'config', |
|
1690 |
+ [ |
|
1691 |
+ {'global': {'phrase': 'my passphrase'}, 'services': {}}, |
|
1692 |
+ {'global': {'key': DUMMY_KEY1_B64}, 'services': {}}, |
|
1693 |
+ { |
|
1694 |
+ 'global': {'phrase': 'abc'}, |
|
1695 |
+ 'services': {'sv': {'phrase': 'my passphrase'}}, |
|
1696 |
+ }, |
|
1697 |
+ { |
|
1698 |
+ 'global': {'phrase': 'abc'}, |
|
1699 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64}}, |
|
1700 |
+ }, |
|
1701 |
+ { |
|
1702 |
+ 'global': {'phrase': 'abc'}, |
|
1703 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}, |
|
1704 |
+ }, |
|
1705 |
+ ], |
|
1706 |
+ ) |
|
1707 |
+ def test_111_migrate_config( |
|
1708 |
+ self, monkeypatch: pytest.MonkeyPatch, config: Any |
|
1709 |
+ ) -> None: |
|
1710 |
+ runner = click.testing.CliRunner() |
|
1711 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
|
1712 |
+ config_filename = cli._config_filename() |
|
1713 |
+ with open(config_filename, 'w', encoding='UTF-8') as fileobj: |
|
1714 |
+ print(json.dumps(config, indent=2), file=fileobj) |
|
1715 |
+ assert cli._migrate_and_load_old_config() == (config, None) |
|
1716 |
+ |
|
1717 |
+ @pytest.mark.parametrize( |
|
1718 |
+ 'config', |
|
1719 |
+ [ |
|
1720 |
+ {'global': {'phrase': 'my passphrase'}, 'services': {}}, |
|
1721 |
+ {'global': {'key': DUMMY_KEY1_B64}, 'services': {}}, |
|
1722 |
+ { |
|
1723 |
+ 'global': {'phrase': 'abc'}, |
|
1724 |
+ 'services': {'sv': {'phrase': 'my passphrase'}}, |
|
1725 |
+ }, |
|
1726 |
+ { |
|
1727 |
+ 'global': {'phrase': 'abc'}, |
|
1728 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64}}, |
|
1729 |
+ }, |
|
1730 |
+ { |
|
1731 |
+ 'global': {'phrase': 'abc'}, |
|
1732 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}, |
|
1733 |
+ }, |
|
1734 |
+ ], |
|
1735 |
+ ) |
|
1736 |
+ def test_112_migrate_config_error( |
|
1737 |
+ self, monkeypatch: pytest.MonkeyPatch, config: Any |
|
1738 |
+ ) -> None: |
|
1739 |
+ runner = click.testing.CliRunner() |
|
1740 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
|
1741 |
+ config_filename = cli._config_filename() |
|
1742 |
+ with open(config_filename, 'w', encoding='UTF-8') as fileobj: |
|
1743 |
+ print(json.dumps(config, indent=2), file=fileobj) |
|
1744 |
+ os.mkdir(cli._config_filename(subsystem='vault')) |
|
1745 |
+ config2, err = cli._migrate_and_load_old_config() |
|
1746 |
+ assert config2 == config |
|
1747 |
+ assert isinstance(err, OSError) |
|
1748 |
+ assert err.errno == errno.EISDIR |
|
1749 |
+ |
|
1750 |
+ @pytest.mark.parametrize( |
|
1751 |
+ 'config', |
|
1752 |
+ [ |
|
1753 |
+ {'global': '', 'services': {}}, |
|
1754 |
+ {'global': 0, 'services': {}}, |
|
1755 |
+ { |
|
1756 |
+ 'global': {'phrase': 'abc'}, |
|
1757 |
+ 'services': False, |
|
1758 |
+ }, |
|
1759 |
+ { |
|
1760 |
+ 'global': {'phrase': 'abc'}, |
|
1761 |
+ 'services': True, |
|
1762 |
+ }, |
|
1763 |
+ { |
|
1764 |
+ 'global': {'phrase': 'abc'}, |
|
1765 |
+ 'services': None, |
|
1766 |
+ }, |
|
1767 |
+ ], |
|
1768 |
+ ) |
|
1769 |
+ def test_113_migrate_config_error_bad_config_value( |
|
1770 |
+ self, monkeypatch: pytest.MonkeyPatch, config: Any |
|
1771 |
+ ) -> None: |
|
1772 |
+ runner = click.testing.CliRunner() |
|
1773 |
+ with tests.isolated_config(monkeypatch=monkeypatch, runner=runner): |
|
1774 |
+ config_filename = cli._config_filename() |
|
1775 |
+ with open(config_filename, 'w', encoding='UTF-8') as fileobj: |
|
1776 |
+ print(json.dumps(config, indent=2), file=fileobj) |
|
1777 |
+ with pytest.raises(ValueError, match=cli._INVALID_VAULT_CONFIG): |
|
1778 |
+ cli._migrate_and_load_old_config() |
|
1779 |
+ |
|
1614 | 1780 |
def test_200_forward_export_vault_path_parameter( |
1615 | 1781 |
self, monkeypatch: pytest.MonkeyPatch |
1616 | 1782 |
) -> None: |
... | ... |
@@ -1671,3 +1837,78 @@ class TestCLITransition: |
1671 | 1837 |
assert ( |
1672 | 1838 |
c not in result.output |
1673 | 1839 |
), f'derived password contains forbidden character {c!r}' |
1840 |
+ |
|
1841 |
+ def test_300_export_using_old_config_file( |
|
1842 |
+ self, |
|
1843 |
+ monkeypatch: pytest.MonkeyPatch, |
|
1844 |
+ ) -> None: |
|
1845 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
1846 |
+ with tests.isolated_config( |
|
1847 |
+ monkeypatch=monkeypatch, |
|
1848 |
+ runner=runner, |
|
1849 |
+ ): |
|
1850 |
+ with open( |
|
1851 |
+ cli._config_filename(), 'w', encoding='UTF-8' |
|
1852 |
+ ) as fileobj: |
|
1853 |
+ print( |
|
1854 |
+ json.dumps( |
|
1855 |
+ {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
|
1856 |
+ indent=2, |
|
1857 |
+ ), |
|
1858 |
+ file=fileobj, |
|
1859 |
+ ) |
|
1860 |
+ _result = runner.invoke( |
|
1861 |
+ cli.derivepassphrase_vault, |
|
1862 |
+ ['--export', '-'], |
|
1863 |
+ catch_exceptions=False, |
|
1864 |
+ ) |
|
1865 |
+ result = tests.ReadableResult.parse(_result) |
|
1866 |
+ assert result.clean_exit(), 'expected clean exit' |
|
1867 |
+ assert ( |
|
1868 |
+ 'v0.1-style config file' in result.stderr |
|
1869 |
+ ), 'expected known warning message in stderr' |
|
1870 |
+ assert ( |
|
1871 |
+ 'Successfully migrated to ' in result.stderr |
|
1872 |
+ ), 'expected known warning message in stderr' |
|
1873 |
+ |
|
1874 |
+ def test_300a_export_using_old_config_file_migration_error( |
|
1875 |
+ self, |
|
1876 |
+ monkeypatch: pytest.MonkeyPatch, |
|
1877 |
+ ) -> None: |
|
1878 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
1879 |
+ with tests.isolated_config( |
|
1880 |
+ monkeypatch=monkeypatch, |
|
1881 |
+ runner=runner, |
|
1882 |
+ ): |
|
1883 |
+ with open( |
|
1884 |
+ cli._config_filename(), 'w', encoding='UTF-8' |
|
1885 |
+ ) as fileobj: |
|
1886 |
+ print( |
|
1887 |
+ json.dumps( |
|
1888 |
+ {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
|
1889 |
+ indent=2, |
|
1890 |
+ ), |
|
1891 |
+ file=fileobj, |
|
1892 |
+ ) |
|
1893 |
+ |
|
1894 |
+ def raiser(*_args: Any, **_kwargs: Any) -> None: |
|
1895 |
+ raise OSError( |
|
1896 |
+ errno.EACCES, |
|
1897 |
+ os.strerror(errno.EACCES), |
|
1898 |
+ cli._config_filename(subsystem='vault'), |
|
1899 |
+ ) |
|
1900 |
+ |
|
1901 |
+ monkeypatch.setattr(os, 'replace', raiser) |
|
1902 |
+ _result = runner.invoke( |
|
1903 |
+ cli.derivepassphrase_vault, |
|
1904 |
+ ['--export', '-'], |
|
1905 |
+ catch_exceptions=False, |
|
1906 |
+ ) |
|
1907 |
+ result = tests.ReadableResult.parse(_result) |
|
1908 |
+ assert result.clean_exit(), 'expected clean exit' |
|
1909 |
+ assert ( |
|
1910 |
+ 'v0.1-style config file' in result.stderr |
|
1911 |
+ ), 'expected known warning message in stderr' |
|
1912 |
+ assert ( |
|
1913 |
+ 'Warning: Failed to migrate to ' in result.stderr |
|
1914 |
+ ), 'expected known warning message in stderr' |
|
1674 | 1915 |