Marco Ricci commited on 2025-02-06 14:27:21
Zeige 4 geänderte Dateien mit 278 Einfügungen und 56 Löschungen.
Reintroduce the previous git-like editor interface as the "modern" editor interface to the "vault" subcommand, and provide explicit options `--modern-editor-interface` and `--vault-legacy-editor-interface`, defaulting to the latter for compatibility. Furthermore, for the legacy interface, introduce a backup copy of the notes to guard against accidental data loss, because the legacy interface offers no way to abort mid-editing, making fatal mistakes all the more likely.
| ... | ... |
@@ -168,6 +168,7 @@ config_filename_table = {
|
| 168 | 168 |
# TODO(the-13th-letter): Remove the old settings.json file. |
| 169 | 169 |
# https://the13thletter.info/derivepassphrase/latest/upgrade-notes.html#v1.0-old-settings-file |
| 170 | 170 |
'old settings.json': 'settings.json', |
| 171 |
+ 'notes backup': 'old-notes.txt', |
|
| 171 | 172 |
} |
| 172 | 173 |
|
| 173 | 174 |
|
| ... | ... |
@@ -809,7 +809,8 @@ class Label(enum.Enum): |
| 809 | 809 |
"""""" |
| 810 | 810 |
DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT = commented( |
| 811 | 811 |
"This instruction text is shown above the user's old stored notes " |
| 812 |
- 'for this service, if any. ' |
|
| 812 |
+ 'for this service, if any, if the recommended ' |
|
| 813 |
+ '"modern" editor interface is used. ' |
|
| 813 | 814 |
'The next line is the cut marking defined in ' |
| 814 | 815 |
'Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER.' |
| 815 | 816 |
)( |
| ... | ... |
@@ -826,6 +827,21 @@ class Label(enum.Enum): |
| 826 | 827 |
""", |
| 827 | 828 |
) |
| 828 | 829 |
"""""" |
| 830 |
+ DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT = commented( |
|
| 831 |
+ "This instruction text is shown above the user's old stored notes " |
|
| 832 |
+ 'for this service, if any, if the vault(1)-compatible ' |
|
| 833 |
+ '"legacy" editor interface is used. ' |
|
| 834 |
+ 'The interface does not support commentary in the notes, ' |
|
| 835 |
+ 'so we fill this with obvious placeholder text instead. ' |
|
| 836 |
+ '(Please replace this with what *your* language/culture would ' |
|
| 837 |
+ 'obviously recognize as placeholder text.)' |
|
| 838 |
+ )( |
|
| 839 |
+ 'Label :: Help text :: Explanation', |
|
| 840 |
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' |
|
| 841 |
+ 'sed do eiusmod tempor incididunt ut labore ' |
|
| 842 |
+ 'et dolore magna aliqua.', |
|
| 843 |
+ ) |
|
| 844 |
+ """""" |
|
| 829 | 845 |
DEPRECATED_COMMAND_LABEL = commented( |
| 830 | 846 |
'We use this format string to indicate, at the beginning ' |
| 831 | 847 |
"of a command's help text, that this command is deprecated.", |
| ... | ... |
@@ -1082,6 +1098,21 @@ class Label(enum.Enum): |
| 1082 | 1098 |
'when exporting, export as JSON (default) or POSIX sh', |
| 1083 | 1099 |
) |
| 1084 | 1100 |
"""""" |
| 1101 |
+ DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT = commented( |
|
| 1102 |
+ 'The corresponding option is displayed as ' |
|
| 1103 |
+ '"--modern-editor-interface / --vault-legacy-editor-interface", ' |
|
| 1104 |
+ 'so you may want to hint that the default (legacy) ' |
|
| 1105 |
+ 'is the second of those options. ' |
|
| 1106 |
+ 'Though the vault(1) legacy editor interface clearly has deficiencies ' |
|
| 1107 |
+ 'and (in my opinion) should only be used for compatibility purposes, ' |
|
| 1108 |
+ 'the one-line help text should try not to sound too judgmental, ' |
|
| 1109 |
+ 'if possible.', |
|
| 1110 |
+ )( |
|
| 1111 |
+ 'Label :: Help text :: One-line description', |
|
| 1112 |
+ 'edit notes using the modern editor interface ' |
|
| 1113 |
+ 'or the vault-like legacy one (default)', |
|
| 1114 |
+ ) |
|
| 1115 |
+ """""" |
|
| 1085 | 1116 |
|
| 1086 | 1117 |
EXPORT_VAULT_FORMAT_METAVAR_FMT = commented( |
| 1087 | 1118 |
'', |
| ... | ... |
@@ -1785,6 +1816,16 @@ class WarnMsgTemplate(enum.Enum): |
| 1785 | 1816 |
'because a key is also set.', |
| 1786 | 1817 |
) |
| 1787 | 1818 |
"""""" |
| 1819 |
+ LEGACY_EDITOR_INTERFACE_NOTES_BACKUP = commented( |
|
| 1820 |
+ '', |
|
| 1821 |
+ )( |
|
| 1822 |
+ 'Warning message', |
|
| 1823 |
+ 'Using the vault(1)-compatible legacy editor interface, ' |
|
| 1824 |
+ 'which does not allow aborting mid-edit. ' |
|
| 1825 |
+ 'A backup copy of the old notes was saved to {filename!r} '
|
|
| 1826 |
+ 'to guard against editing mistakes.', |
|
| 1827 |
+ flags='python-brace-format', |
|
| 1828 |
+ ) |
|
| 1788 | 1829 |
PASSPHRASE_NOT_NORMALIZED = commented( |
| 1789 | 1830 |
'The key is a (vault) configuration key, in JSONPath syntax, ' |
| 1790 | 1831 |
'typically "$.global" for the global passphrase or ' |
| ... | ... |
@@ -593,6 +593,15 @@ def derivepassphrase_export_vault( |
| 593 | 593 |
), |
| 594 | 594 |
cls=cli_machinery.CompatibilityOption, |
| 595 | 595 |
) |
| 596 |
+@click.option( |
|
| 597 |
+ '--modern-editor-interface/--vault-legacy-editor-interface', |
|
| 598 |
+ 'modern_editor_interface', |
|
| 599 |
+ default=False, |
|
| 600 |
+ help=_msg.TranslatedString( |
|
| 601 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT |
|
| 602 |
+ ), |
|
| 603 |
+ cls=cli_machinery.CompatibilityOption, |
|
| 604 |
+) |
|
| 596 | 605 |
@cli_machinery.version_option |
| 597 | 606 |
@cli_machinery.color_forcing_pseudo_option |
| 598 | 607 |
@cli_machinery.standard_logging_options |
| ... | ... |
@@ -629,6 +638,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 629 | 638 |
overwrite_config: bool = False, |
| 630 | 639 |
unset_settings: Sequence[str] = (), |
| 631 | 640 |
export_as: Literal['json', 'sh'] = 'json', |
| 641 |
+ modern_editor_interface: bool = False, |
|
| 632 | 642 |
) -> None: |
| 633 | 643 |
"""Derive a passphrase using the vault(1) derivation scheme. |
| 634 | 644 |
|
| ... | ... |
@@ -722,6 +732,12 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 722 | 732 |
Command-line argument `--export-as`. If given together with |
| 723 | 733 |
`--export`, selects the format to export the current |
| 724 | 734 |
configuration as: JSON ("json", default) or POSIX sh ("sh").
|
| 735 |
+ modern_editor_interface: |
|
| 736 |
+ Command-line arguments `--modern-editor-interface` (True) |
|
| 737 |
+ and `--vault-legacy-editor-interface` (False). Controls |
|
| 738 |
+ whether editing notes uses a modern editor interface |
|
| 739 |
+ (supporting comments and aborting) or a vault(1)-compatible |
|
| 740 |
+ legacy editor interface (WYSIWYG notes contents). |
|
| 725 | 741 |
|
| 726 | 742 |
""" # noqa: DOC501 |
| 727 | 743 |
logger = logging.getLogger(PROG_NAME) |
| ... | ... |
@@ -996,7 +1012,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 996 | 1012 |
extra={'color': ctx.color},
|
| 997 | 1013 |
) |
| 998 | 1014 |
|
| 999 |
- if delete_service_settings: |
|
| 1015 |
+ if delete_service_settings: # noqa: PLR1702 |
|
| 1000 | 1016 |
assert service is not None |
| 1001 | 1017 |
configuration = get_config() |
| 1002 | 1018 |
if service in configuration['services']: |
| ... | ... |
@@ -1396,15 +1412,55 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
| 1396 | 1412 |
notes_marker = _msg.TranslatedString( |
| 1397 | 1413 |
_msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER |
| 1398 | 1414 |
) |
| 1415 |
+ notes_legacy_instructions = _msg.TranslatedString( |
|
| 1416 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT |
|
| 1417 |
+ ) |
|
| 1399 | 1418 |
old_notes_value = subtree.get('notes', '')
|
| 1419 |
+ if modern_editor_interface: |
|
| 1400 | 1420 |
text = '\n'.join([ |
| 1401 | 1421 |
str(notes_instructions), |
| 1402 | 1422 |
str(notes_marker), |
| 1403 | 1423 |
old_notes_value, |
| 1404 | 1424 |
]) |
| 1425 |
+ else: |
|
| 1426 |
+ text = str(notes_legacy_instructions) |
|
| 1405 | 1427 |
notes_value = click.edit(text=text, require_save=False) |
| 1406 | 1428 |
assert notes_value is not None |
| 1407 |
- if notes_value.strip() != old_notes_value.strip(): |
|
| 1429 |
+ if ( |
|
| 1430 |
+ not modern_editor_interface |
|
| 1431 |
+ and notes_value.strip() != old_notes_value.strip() |
|
| 1432 |
+ ): |
|
| 1433 |
+ backup_file = cli_helpers.config_filename( |
|
| 1434 |
+ subsystem='notes backup' |
|
| 1435 |
+ ) |
|
| 1436 |
+ backup_file.write_text(old_notes_value, encoding='UTF-8') |
|
| 1437 |
+ logger.warning( |
|
| 1438 |
+ _msg.TranslatedString( |
|
| 1439 |
+ _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP, |
|
| 1440 |
+ filename=str(backup_file), |
|
| 1441 |
+ ), |
|
| 1442 |
+ extra={'color': ctx.color},
|
|
| 1443 |
+ ) |
|
| 1444 |
+ subtree['notes'] = notes_value.strip() |
|
| 1445 |
+ elif ( |
|
| 1446 |
+ modern_editor_interface |
|
| 1447 |
+ and notes_value.strip() != text.strip() |
|
| 1448 |
+ ): |
|
| 1449 |
+ notes_lines = collections.deque( |
|
| 1450 |
+ notes_value.splitlines(True) # noqa: FBT003 |
|
| 1451 |
+ ) |
|
| 1452 |
+ while notes_lines: |
|
| 1453 |
+ line = notes_lines.popleft() |
|
| 1454 |
+ if line.startswith(str(notes_marker)): |
|
| 1455 |
+ notes_value = ''.join(notes_lines) |
|
| 1456 |
+ break |
|
| 1457 |
+ else: |
|
| 1458 |
+ if not notes_value.strip(): |
|
| 1459 |
+ err( |
|
| 1460 |
+ _msg.TranslatedString( |
|
| 1461 |
+ _msg.ErrMsgTemplate.USER_ABORTED_EDIT |
|
| 1462 |
+ ) |
|
| 1463 |
+ ) |
|
| 1408 | 1464 |
subtree['notes'] = notes_value.strip() |
| 1409 | 1465 |
put_config(configuration) |
| 1410 | 1466 |
else: |
| ... | ... |
@@ -639,6 +639,8 @@ class Parametrize(types.SimpleNamespace): |
| 639 | 639 |
'--merge-existing', |
| 640 | 640 |
'--unset', |
| 641 | 641 |
'--export-as', |
| 642 |
+ '--modern-editor-interface', |
|
| 643 |
+ '--vault-legacy-editor-interface', |
|
| 642 | 644 |
}), |
| 643 | 645 |
id='derivepassphrase-vault', |
| 644 | 646 |
), |
| ... | ... |
@@ -2468,6 +2470,15 @@ class TestCLI: |
| 2468 | 2470 |
'expected error exit and known error message' |
| 2469 | 2471 |
) |
| 2470 | 2472 |
|
| 2473 |
+ @pytest.mark.parametrize( |
|
| 2474 |
+ 'modern_editor_interface', [False, True], ids=['legacy', 'modern'] |
|
| 2475 |
+ ) |
|
| 2476 |
+ @hypothesis.settings( |
|
| 2477 |
+ suppress_health_check=[ |
|
| 2478 |
+ *hypothesis.settings().suppress_health_check, |
|
| 2479 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
| 2480 |
+ ], |
|
| 2481 |
+ ) |
|
| 2471 | 2482 |
@hypothesis.given( |
| 2472 | 2483 |
notes=strategies.text( |
| 2473 | 2484 |
strategies.characters( |
| ... | ... |
@@ -2479,6 +2490,8 @@ class TestCLI: |
| 2479 | 2490 |
) |
| 2480 | 2491 |
def test_220_edit_notes_successfully( |
| 2481 | 2492 |
self, |
| 2493 |
+ caplog: pytest.LogCaptureFixture, |
|
| 2494 |
+ modern_editor_interface: bool, |
|
| 2482 | 2495 |
notes: str, |
| 2483 | 2496 |
) -> None: |
| 2484 | 2497 |
"""Editing notes works.""" |
| ... | ... |
@@ -2487,6 +2500,8 @@ class TestCLI: |
| 2487 | 2500 |
# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - - |
| 2488 | 2501 |
{notes}
|
| 2489 | 2502 |
""" |
| 2503 |
+ # Reset caplog between hypothesis runs. |
|
| 2504 |
+ caplog.clear() |
|
| 2490 | 2505 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 2491 | 2506 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2492 | 2507 |
# with-statements. |
| ... | ... |
@@ -2503,46 +2518,62 @@ class TestCLI: |
| 2503 | 2518 |
}, |
| 2504 | 2519 |
) |
| 2505 | 2520 |
) |
| 2521 |
+ notes_backup_file = cli_helpers.config_filename( |
|
| 2522 |
+ subsystem='notes backup' |
|
| 2523 |
+ ) |
|
| 2524 |
+ notes_backup_file.write_text( |
|
| 2525 |
+ 'These backup notes are left over from the previous session.', |
|
| 2526 |
+ encoding='UTF-8', |
|
| 2527 |
+ ) |
|
| 2506 | 2528 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: edit_result) |
| 2507 | 2529 |
result_ = runner.invoke( |
| 2508 | 2530 |
cli.derivepassphrase_vault, |
| 2509 |
- ['--config', '--notes', '--', 'sv'], |
|
| 2531 |
+ [ |
|
| 2532 |
+ '--config', |
|
| 2533 |
+ '--notes', |
|
| 2534 |
+ '--modern-editor-interface' |
|
| 2535 |
+ if modern_editor_interface |
|
| 2536 |
+ else '--vault-legacy-editor-interface', |
|
| 2537 |
+ '--', |
|
| 2538 |
+ 'sv', |
|
| 2539 |
+ ], |
|
| 2510 | 2540 |
catch_exceptions=False, |
| 2511 | 2541 |
) |
| 2512 | 2542 |
result = tests.ReadableResult.parse(result_) |
| 2513 |
- assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
| 2543 |
+ assert result.clean_exit(), 'expected clean exit' |
|
| 2544 |
+ assert all(map(is_warning_line, result.stderr.splitlines(True))) |
|
| 2545 |
+ assert modern_editor_interface or tests.warning_emitted( |
|
| 2546 |
+ 'Using the vault(1)-compatible legacy editor interface, ' |
|
| 2547 |
+ 'which does not allow aborting mid-edit. ' |
|
| 2548 |
+ 'A backup copy of the old notes was saved', |
|
| 2549 |
+ caplog.record_tuples, |
|
| 2550 |
+ ), 'expected known warning message in stderr' |
|
| 2551 |
+ assert ( |
|
| 2552 |
+ modern_editor_interface |
|
| 2553 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
| 2554 |
+ == 'Contents go here' |
|
| 2555 |
+ ) |
|
| 2514 | 2556 |
with cli_helpers.config_filename(subsystem='vault').open( |
| 2515 | 2557 |
encoding='UTF-8' |
| 2516 | 2558 |
) as infile: |
| 2517 | 2559 |
config = json.load(infile) |
| 2518 | 2560 |
assert config == {
|
| 2519 | 2561 |
'global': {'phrase': 'abc'},
|
| 2520 |
- 'services': {'sv': {'notes': edit_result.strip()}},
|
|
| 2562 |
+ 'services': {
|
|
| 2563 |
+ 'sv': {
|
|
| 2564 |
+ 'notes': notes.strip() |
|
| 2565 |
+ if modern_editor_interface |
|
| 2566 |
+ else edit_result.strip() |
|
| 2567 |
+ } |
|
| 2568 |
+ }, |
|
| 2521 | 2569 |
} |
| 2522 | 2570 |
|
| 2523 | 2571 |
@pytest.mark.parametrize( |
| 2524 |
- 'edit_func_name', |
|
| 2572 |
+ ['edit_func_name', 'modern_editor_interface'], |
|
| 2525 | 2573 |
[ |
| 2526 |
- pytest.param( |
|
| 2527 |
- 'empty', |
|
| 2528 |
- marks=[ |
|
| 2529 |
- pytest.mark.xfail(reason='incompatibility with vault(1)') |
|
| 2530 |
- ], |
|
| 2531 |
- ), |
|
| 2532 |
- 'space', |
|
| 2533 |
- ], |
|
| 2534 |
- ) |
|
| 2535 |
- # Skip the "target", "shrink" and "explain" phases on this test because |
|
| 2536 |
- # one of its parametrizations is marked xfail, and we don't want to do |
|
| 2537 |
- # a lot of extra work that will be thrown away anyway. |
|
| 2538 |
- # |
|
| 2539 |
- # TODO(the-13th-letter): remove this settings decorator once the |
|
| 2540 |
- # xfail'ing parametrization is fixed. |
|
| 2541 |
- @hypothesis.settings( |
|
| 2542 |
- phases=[ |
|
| 2543 |
- hypothesis.Phase.explicit, |
|
| 2544 |
- hypothesis.Phase.reuse, |
|
| 2545 |
- hypothesis.Phase.generate, |
|
| 2574 |
+ pytest.param('empty', True, id='empty'),
|
|
| 2575 |
+ pytest.param('space', False, id='space-legacy'),
|
|
| 2576 |
+ pytest.param('space', True, id='space-modern'),
|
|
| 2546 | 2577 |
], |
| 2547 | 2578 |
) |
| 2548 | 2579 |
@hypothesis.given( |
| ... | ... |
@@ -2557,16 +2588,18 @@ class TestCLI: |
| 2557 | 2588 |
def test_221_edit_notes_noop( |
| 2558 | 2589 |
self, |
| 2559 | 2590 |
edit_func_name: Literal['empty', 'space'], |
| 2591 |
+ modern_editor_interface: bool, |
|
| 2560 | 2592 |
notes: str, |
| 2561 | 2593 |
) -> None: |
| 2562 | 2594 |
"""Abandoning edited notes works.""" |
| 2563 | 2595 |
|
| 2564 |
- def empty(text: str, *_args: Any, **_kwargs: Any) -> None: |
|
| 2596 |
+ def empty(text: str, *_args: Any, **_kwargs: Any) -> str: |
|
| 2565 | 2597 |
del text |
| 2598 |
+ return '' |
|
| 2566 | 2599 |
|
| 2567 | 2600 |
def space(text: str, *_args: Any, **_kwargs: Any) -> str: |
| 2568 | 2601 |
del text |
| 2569 |
- return ' ' + notes + '\n\n\n\n\n\n' |
|
| 2602 |
+ return ' ' + notes.strip() + '\n\n\n\n\n\n' |
|
| 2570 | 2603 |
|
| 2571 | 2604 |
edit_funcs = {'empty': empty, 'space': space}
|
| 2572 | 2605 |
runner = click.testing.CliRunner(mix_stderr=False) |
| ... | ... |
@@ -2581,30 +2614,60 @@ class TestCLI: |
| 2581 | 2614 |
runner=runner, |
| 2582 | 2615 |
vault_config={
|
| 2583 | 2616 |
'global': {'phrase': 'abc'},
|
| 2584 |
- 'services': {'sv': {'notes': notes}},
|
|
| 2617 |
+ 'services': {'sv': {'notes': notes.strip()}},
|
|
| 2585 | 2618 |
}, |
| 2586 | 2619 |
) |
| 2587 | 2620 |
) |
| 2588 |
- |
|
| 2621 |
+ notes_backup_file = cli_helpers.config_filename( |
|
| 2622 |
+ subsystem='notes backup' |
|
| 2623 |
+ ) |
|
| 2624 |
+ notes_backup_file.write_text( |
|
| 2625 |
+ 'These backup notes are left over from the previous session.', |
|
| 2626 |
+ encoding='UTF-8', |
|
| 2627 |
+ ) |
|
| 2589 | 2628 |
monkeypatch.setattr(click, 'edit', edit_funcs[edit_func_name]) |
| 2590 | 2629 |
result_ = runner.invoke( |
| 2591 | 2630 |
cli.derivepassphrase_vault, |
| 2592 |
- ['--config', '--notes', '--', 'sv'], |
|
| 2631 |
+ [ |
|
| 2632 |
+ '--config', |
|
| 2633 |
+ '--notes', |
|
| 2634 |
+ '--modern-editor-interface' |
|
| 2635 |
+ if modern_editor_interface |
|
| 2636 |
+ else '--vault-legacy-editor-interface', |
|
| 2637 |
+ '--', |
|
| 2638 |
+ 'sv', |
|
| 2639 |
+ ], |
|
| 2593 | 2640 |
catch_exceptions=False, |
| 2594 | 2641 |
) |
| 2595 | 2642 |
result = tests.ReadableResult.parse(result_) |
| 2596 |
- assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
| 2643 |
+ assert result.clean_exit(empty_stderr=True) or result.error_exit( |
|
| 2644 |
+ error='the user aborted the request' |
|
| 2645 |
+ ), 'expected clean exit' |
|
| 2646 |
+ assert ( |
|
| 2647 |
+ modern_editor_interface |
|
| 2648 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
| 2649 |
+ == 'These backup notes are left over from the previous session.' |
|
| 2650 |
+ ) |
|
| 2597 | 2651 |
with cli_helpers.config_filename(subsystem='vault').open( |
| 2598 | 2652 |
encoding='UTF-8' |
| 2599 | 2653 |
) as infile: |
| 2600 | 2654 |
config = json.load(infile) |
| 2601 | 2655 |
assert config == {
|
| 2602 | 2656 |
'global': {'phrase': 'abc'},
|
| 2603 |
- 'services': {'sv': {'notes': notes}},
|
|
| 2657 |
+ 'services': {'sv': {'notes': notes.strip()}},
|
|
| 2604 | 2658 |
} |
| 2605 | 2659 |
|
| 2606 | 2660 |
# TODO(the-13th-letter): Keep this behavior or not, with or without |
| 2607 | 2661 |
# warning? |
| 2662 |
+ @pytest.mark.parametrize( |
|
| 2663 |
+ 'modern_editor_interface', [False, True], ids=['legacy', 'modern'] |
|
| 2664 |
+ ) |
|
| 2665 |
+ @hypothesis.settings( |
|
| 2666 |
+ suppress_health_check=[ |
|
| 2667 |
+ *hypothesis.settings().suppress_health_check, |
|
| 2668 |
+ hypothesis.HealthCheck.function_scoped_fixture, |
|
| 2669 |
+ ], |
|
| 2670 |
+ ) |
|
| 2608 | 2671 |
@hypothesis.given( |
| 2609 | 2672 |
notes=strategies.text( |
| 2610 | 2673 |
strategies.characters( |
| ... | ... |
@@ -2616,6 +2679,8 @@ class TestCLI: |
| 2616 | 2679 |
) |
| 2617 | 2680 |
def test_222_edit_notes_marker_removed( |
| 2618 | 2681 |
self, |
| 2682 |
+ caplog: pytest.LogCaptureFixture, |
|
| 2683 |
+ modern_editor_interface: bool, |
|
| 2619 | 2684 |
notes: str, |
| 2620 | 2685 |
) -> None: |
| 2621 | 2686 |
"""Removing the notes marker still saves the notes. |
| ... | ... |
@@ -2627,6 +2692,8 @@ class TestCLI: |
| 2627 | 2692 |
cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER |
| 2628 | 2693 |
) |
| 2629 | 2694 |
hypothesis.assume(str(notes_marker) not in notes.strip()) |
| 2695 |
+ # Reset caplog between hypothesis runs. |
|
| 2696 |
+ caplog.clear() |
|
| 2630 | 2697 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 2631 | 2698 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2632 | 2699 |
# with-statements. |
| ... | ... |
@@ -2643,14 +2710,43 @@ class TestCLI: |
| 2643 | 2710 |
}, |
| 2644 | 2711 |
) |
| 2645 | 2712 |
) |
| 2713 |
+ notes_backup_file = cli_helpers.config_filename( |
|
| 2714 |
+ subsystem='notes backup' |
|
| 2715 |
+ ) |
|
| 2716 |
+ notes_backup_file.write_text( |
|
| 2717 |
+ 'These backup notes are left over from the previous session.', |
|
| 2718 |
+ encoding='UTF-8', |
|
| 2719 |
+ ) |
|
| 2646 | 2720 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: notes) |
| 2647 | 2721 |
result_ = runner.invoke( |
| 2648 | 2722 |
cli.derivepassphrase_vault, |
| 2649 |
- ['--config', '--notes', '--', 'sv'], |
|
| 2723 |
+ [ |
|
| 2724 |
+ '--config', |
|
| 2725 |
+ '--notes', |
|
| 2726 |
+ '--modern-editor-interface' |
|
| 2727 |
+ if modern_editor_interface |
|
| 2728 |
+ else '--vault-legacy-editor-interface', |
|
| 2729 |
+ '--', |
|
| 2730 |
+ 'sv', |
|
| 2731 |
+ ], |
|
| 2650 | 2732 |
catch_exceptions=False, |
| 2651 | 2733 |
) |
| 2652 | 2734 |
result = tests.ReadableResult.parse(result_) |
| 2653 |
- assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
| 2735 |
+ assert result.clean_exit(), 'expected clean exit' |
|
| 2736 |
+ assert not result.stderr or all( |
|
| 2737 |
+ map(is_warning_line, result.stderr.splitlines(True)) |
|
| 2738 |
+ ) |
|
| 2739 |
+ assert not caplog.record_tuples or tests.warning_emitted( |
|
| 2740 |
+ 'Using the vault(1)-compatible legacy editor interface, ' |
|
| 2741 |
+ 'which does not allow aborting mid-edit. ' |
|
| 2742 |
+ 'A backup copy of the old notes was saved', |
|
| 2743 |
+ caplog.record_tuples, |
|
| 2744 |
+ ), 'expected known warning message in stderr' |
|
| 2745 |
+ assert ( |
|
| 2746 |
+ modern_editor_interface |
|
| 2747 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
| 2748 |
+ == 'Contents go here' |
|
| 2749 |
+ ) |
|
| 2654 | 2750 |
with cli_helpers.config_filename(subsystem='vault').open( |
| 2655 | 2751 |
encoding='UTF-8' |
| 2656 | 2752 |
) as infile: |
| ... | ... |
@@ -2660,20 +2756,6 @@ class TestCLI: |
| 2660 | 2756 |
'services': {'sv': {'notes': notes.strip()}},
|
| 2661 | 2757 |
} |
| 2662 | 2758 |
|
| 2663 |
- @pytest.mark.xfail(reason='incompatibility with vault(1)') |
|
| 2664 |
- # Skip the "target", "shrink" and "explain" phases on this test because |
|
| 2665 |
- # this test is marked xfail, and we don't want to do a lot of extra work |
|
| 2666 |
- # that will be thrown away anyway. |
|
| 2667 |
- # |
|
| 2668 |
- # TODO(the-13th-letter): remove this settings decorator once the xfail |
|
| 2669 |
- # is fixed. |
|
| 2670 |
- @hypothesis.settings( |
|
| 2671 |
- phases=[ |
|
| 2672 |
- hypothesis.Phase.explicit, |
|
| 2673 |
- hypothesis.Phase.reuse, |
|
| 2674 |
- hypothesis.Phase.generate, |
|
| 2675 |
- ], |
|
| 2676 |
- ) |
|
| 2677 | 2759 |
@hypothesis.given( |
| 2678 | 2760 |
notes=strategies.text( |
| 2679 | 2761 |
strategies.characters( |
| ... | ... |
@@ -2687,7 +2769,11 @@ class TestCLI: |
| 2687 | 2769 |
self, |
| 2688 | 2770 |
notes: str, |
| 2689 | 2771 |
) -> None: |
| 2690 |
- """Aborting editing notes works.""" |
|
| 2772 |
+ """Aborting editing notes works. |
|
| 2773 |
+ |
|
| 2774 |
+ Aborting is only supported with the modern editor interface. |
|
| 2775 |
+ |
|
| 2776 |
+ """ |
|
| 2691 | 2777 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 2692 | 2778 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2693 | 2779 |
# with-statements. |
| ... | ... |
@@ -2707,7 +2793,13 @@ class TestCLI: |
| 2707 | 2793 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: '') |
| 2708 | 2794 |
result_ = runner.invoke( |
| 2709 | 2795 |
cli.derivepassphrase_vault, |
| 2710 |
- ['--config', '--notes', '--', 'sv'], |
|
| 2796 |
+ [ |
|
| 2797 |
+ '--config', |
|
| 2798 |
+ '--notes', |
|
| 2799 |
+ '--modern-editor-interface', |
|
| 2800 |
+ '--', |
|
| 2801 |
+ 'sv', |
|
| 2802 |
+ ], |
|
| 2711 | 2803 |
catch_exceptions=False, |
| 2712 | 2804 |
) |
| 2713 | 2805 |
result = tests.ReadableResult.parse(result_) |
| ... | ... |
@@ -2723,11 +2815,14 @@ class TestCLI: |
| 2723 | 2815 |
'services': {'sv': {'notes': notes.strip()}},
|
| 2724 | 2816 |
} |
| 2725 | 2817 |
|
| 2726 |
- @pytest.mark.xfail(reason='incompatibility with vault(1)') |
|
| 2727 | 2818 |
def test_223a_edit_empty_notes_abort( |
| 2728 | 2819 |
self, |
| 2729 | 2820 |
) -> None: |
| 2730 |
- """Aborting editing notes works even if no notes are stored yet.""" |
|
| 2821 |
+ """Aborting editing notes works even if no notes are stored yet. |
|
| 2822 |
+ |
|
| 2823 |
+ Aborting is only supported with the modern editor interface. |
|
| 2824 |
+ |
|
| 2825 |
+ """ |
|
| 2731 | 2826 |
runner = click.testing.CliRunner(mix_stderr=False) |
| 2732 | 2827 |
# TODO(the-13th-letter): Rewrite using parenthesized |
| 2733 | 2828 |
# with-statements. |
| ... | ... |
@@ -2747,7 +2842,13 @@ class TestCLI: |
| 2747 | 2842 |
monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: '') |
| 2748 | 2843 |
result_ = runner.invoke( |
| 2749 | 2844 |
cli.derivepassphrase_vault, |
| 2750 |
- ['--config', '--notes', '--', 'sv'], |
|
| 2845 |
+ [ |
|
| 2846 |
+ '--config', |
|
| 2847 |
+ '--notes', |
|
| 2848 |
+ '--modern-editor-interface', |
|
| 2849 |
+ '--', |
|
| 2850 |
+ 'sv', |
|
| 2851 |
+ ], |
|
| 2751 | 2852 |
catch_exceptions=False, |
| 2752 | 2853 |
) |
| 2753 | 2854 |
result = tests.ReadableResult.parse(result_) |
| ... | ... |
@@ -2763,6 +2864,9 @@ class TestCLI: |
| 2763 | 2864 |
'services': {},
|
| 2764 | 2865 |
} |
| 2765 | 2866 |
|
| 2867 |
+ @pytest.mark.parametrize( |
|
| 2868 |
+ 'modern_editor_interface', [False, True], ids=['legacy', 'modern'] |
|
| 2869 |
+ ) |
|
| 2766 | 2870 |
@hypothesis.settings( |
| 2767 | 2871 |
suppress_health_check=[ |
| 2768 | 2872 |
*hypothesis.settings().suppress_health_check, |
| ... | ... |
@@ -2780,6 +2884,7 @@ class TestCLI: |
| 2780 | 2884 |
def test_223b_edit_notes_fail_config_option_missing( |
| 2781 | 2885 |
self, |
| 2782 | 2886 |
caplog: pytest.LogCaptureFixture, |
| 2887 |
+ modern_editor_interface: bool, |
|
| 2783 | 2888 |
notes: str, |
| 2784 | 2889 |
) -> None: |
| 2785 | 2890 |
"""Editing notes fails (and warns) if `--config` is missing.""" |
| ... | ... |
@@ -2810,10 +2915,24 @@ class TestCLI: |
| 2810 | 2915 |
def raiser(*_args: Any, **_kwargs: Any) -> NoReturn: |
| 2811 | 2916 |
pytest.fail(EDIT_ATTEMPTED) |
| 2812 | 2917 |
|
| 2918 |
+ notes_backup_file = cli_helpers.config_filename( |
|
| 2919 |
+ subsystem='notes backup' |
|
| 2920 |
+ ) |
|
| 2921 |
+ notes_backup_file.write_text( |
|
| 2922 |
+ 'These backup notes are left over from the previous session.', |
|
| 2923 |
+ encoding='UTF-8', |
|
| 2924 |
+ ) |
|
| 2813 | 2925 |
monkeypatch.setattr(click, 'edit', raiser) |
| 2814 | 2926 |
result_ = runner.invoke( |
| 2815 | 2927 |
cli.derivepassphrase_vault, |
| 2816 |
- ['--notes', '--', DUMMY_SERVICE], |
|
| 2928 |
+ [ |
|
| 2929 |
+ '--notes', |
|
| 2930 |
+ '--modern-editor-interface' |
|
| 2931 |
+ if modern_editor_interface |
|
| 2932 |
+ else '--vault-legacy-editor-interface', |
|
| 2933 |
+ '--', |
|
| 2934 |
+ DUMMY_SERVICE, |
|
| 2935 |
+ ], |
|
| 2817 | 2936 |
catch_exceptions=False, |
| 2818 | 2937 |
) |
| 2819 | 2938 |
result = tests.ReadableResult.parse(result_) |
| ... | ... |
@@ -2832,6 +2951,11 @@ class TestCLI: |
| 2832 | 2951 |
'No notes will be edited.', |
| 2833 | 2952 |
caplog.record_tuples, |
| 2834 | 2953 |
), 'expected known warning message in stderr' |
| 2954 |
+ assert ( |
|
| 2955 |
+ modern_editor_interface |
|
| 2956 |
+ or notes_backup_file.read_text(encoding='UTF-8') |
|
| 2957 |
+ == 'These backup notes are left over from the previous session.' |
|
| 2958 |
+ ) |
|
| 2835 | 2959 |
with cli_helpers.config_filename(subsystem='vault').open( |
| 2836 | 2960 |
encoding='UTF-8' |
| 2837 | 2961 |
) as infile: |
| 2838 | 2962 |