Marco Ricci commited on 2024-07-28 15:02:34
Zeige 4 geänderte Dateien mit 102 Einfügungen und 1 Löschungen.
Do not error out during the first attempt at saving the configuration, where the configuration directory probably does not yet exist. On an unrelated note, fix one typo in `derivepassphrase.cli` and a missing dependency in the hatch environment for type checking.
... | ... |
@@ -97,7 +97,7 @@ def _load_config() -> dpp_types.VaultConfig: |
97 | 97 |
|
98 | 98 |
|
99 | 99 |
def _save_config(config: dpp_types.VaultConfig, /) -> None: |
100 |
- """Save a vault(1)-compatbile config to the application directory. |
|
100 |
+ """Save a vault(1)-compatible config to the application directory. |
|
101 | 101 |
|
102 | 102 |
The filename is obtained via |
103 | 103 |
[`derivepassphrase.cli._config_filename`][]. The config will be |
... | ... |
@@ -117,6 +117,12 @@ def _save_config(config: dpp_types.VaultConfig, /) -> None: |
117 | 117 |
if not dpp_types.is_vault_config(config): |
118 | 118 |
raise ValueError(_INVALID_VAULT_CONFIG) |
119 | 119 |
filename = _config_filename() |
120 |
+ filedir = os.path.dirname(os.path.abspath(filename)) |
|
121 |
+ try: |
|
122 |
+ os.makedirs(filedir, exist_ok=False) |
|
123 |
+ except FileExistsError: |
|
124 |
+ if not os.path.isdir(filedir): |
|
125 |
+ raise |
|
120 | 126 |
with open(filename, 'w', encoding='UTF-8') as fileobj: |
121 | 127 |
json.dump(config, fileobj) |
122 | 128 |
|
... | ... |
@@ -7,6 +7,7 @@ from __future__ import annotations |
7 | 7 |
import contextlib |
8 | 8 |
import json |
9 | 9 |
import os |
10 |
+import shutil |
|
10 | 11 |
import socket |
11 | 12 |
from typing import TYPE_CHECKING |
12 | 13 |
|
... | ... |
@@ -686,6 +687,32 @@ class TestCLI: |
686 | 687 |
b'cannot write config' in result.stderr_bytes |
687 | 688 |
), 'program did not print the expected error message' |
688 | 689 |
|
690 |
+ def test_214d_export_settings_settings_directory_not_a_directory( |
|
691 |
+ self, |
|
692 |
+ monkeypatch: Any, |
|
693 |
+ ) -> None: |
|
694 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
695 |
+ with tests.isolated_config( |
|
696 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}} |
|
697 |
+ ): |
|
698 |
+ with contextlib.suppress(FileNotFoundError): |
|
699 |
+ shutil.rmtree('.derivepassphrase') |
|
700 |
+ with open('.derivepassphrase', 'w', encoding='UTF-8') as outfile: |
|
701 |
+ print('Obstruction!!', file=outfile) |
|
702 |
+ result = runner.invoke( |
|
703 |
+ cli.derivepassphrase, |
|
704 |
+ ['--export', '-'], |
|
705 |
+ input=b'null', |
|
706 |
+ catch_exceptions=False, |
|
707 |
+ ) |
|
708 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
709 |
+ assert ( |
|
710 |
+ result.stderr_bytes |
|
711 |
+ ), 'program did not print any error message' |
|
712 |
+ assert ( |
|
713 |
+ b'cannot load config' in result.stderr_bytes |
|
714 |
+ ), 'program did not print the expected error message' |
|
715 |
+ |
|
689 | 716 |
def test_220_edit_notes_successfully(self, monkeypatch: Any) -> None: |
690 | 717 |
edit_result = """ |
691 | 718 |
|
... | ... |
@@ -946,6 +973,72 @@ contents go here |
946 | 973 |
b'no passphrase or key given' in result.stderr_bytes |
947 | 974 |
), 'expected error message missing' |
948 | 975 |
|
976 |
+ def test_230_config_directory_nonexistant(self, monkeypatch: Any) -> None: |
|
977 |
+ """the-13th-letter/derivepassphrase#6""" |
|
978 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
979 |
+ with tests.isolated_config( |
|
980 |
+ monkeypatch=monkeypatch, |
|
981 |
+ runner=runner, |
|
982 |
+ config={'services': {}}, |
|
983 |
+ ): |
|
984 |
+ os.remove('.derivepassphrase/settings.json') |
|
985 |
+ os.rmdir('.derivepassphrase') |
|
986 |
+ os_makedirs_called = False |
|
987 |
+ real_os_makedirs = os.makedirs |
|
988 |
+ |
|
989 |
+ def makedirs(*args: Any, **kwargs: Any) -> Any: |
|
990 |
+ nonlocal os_makedirs_called |
|
991 |
+ os_makedirs_called = True |
|
992 |
+ return real_os_makedirs(*args, **kwargs) |
|
993 |
+ |
|
994 |
+ monkeypatch.setattr(os, 'makedirs', makedirs) |
|
995 |
+ result = runner.invoke( |
|
996 |
+ cli.derivepassphrase, |
|
997 |
+ ['--config', '-p'], |
|
998 |
+ catch_exceptions=False, |
|
999 |
+ input='abc\n', |
|
1000 |
+ ) |
|
1001 |
+ assert ( |
|
1002 |
+ result.stderr_bytes == b'Passphrase:' |
|
1003 |
+ ), 'program unexpectedly failed?!' |
|
1004 |
+ assert result.exit_code == 0, 'program unexpectedly failed?!' |
|
1005 |
+ assert os_makedirs_called, 'os.makedirs has not been called?!' |
|
1006 |
+ with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
1007 |
+ config_readback = json.load(infile) |
|
1008 |
+ assert config_readback == { |
|
1009 |
+ 'global': {'phrase': 'abc'}, |
|
1010 |
+ 'services': {}, |
|
1011 |
+ }, 'config mismatch' |
|
1012 |
+ |
|
1013 |
+ def test_230a_config_directory_not_a_file(self, monkeypatch: Any) -> None: |
|
1014 |
+ """the-13th-letter/derivepassphrase#6""" |
|
1015 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
1016 |
+ with tests.isolated_config( |
|
1017 |
+ monkeypatch=monkeypatch, |
|
1018 |
+ runner=runner, |
|
1019 |
+ config={'services': {}}, |
|
1020 |
+ ): |
|
1021 |
+ _save_config = cli._save_config |
|
1022 |
+ |
|
1023 |
+ def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any: |
|
1024 |
+ with contextlib.suppress(FileNotFoundError): |
|
1025 |
+ shutil.rmtree('.derivepassphrase') |
|
1026 |
+ with open( |
|
1027 |
+ '.derivepassphrase', 'w', encoding='UTF-8' |
|
1028 |
+ ) as outfile: |
|
1029 |
+ print('Obstruction!!', file=outfile) |
|
1030 |
+ monkeypatch.setattr(cli, '_save_config', _save_config) |
|
1031 |
+ return _save_config(*args, **kwargs) |
|
1032 |
+ |
|
1033 |
+ monkeypatch.setattr(cli, '_save_config', obstruct_config_saving) |
|
1034 |
+ with pytest.raises(FileExistsError): |
|
1035 |
+ runner.invoke( |
|
1036 |
+ cli.derivepassphrase, |
|
1037 |
+ ['--config', '-p'], |
|
1038 |
+ catch_exceptions=False, |
|
1039 |
+ input='abc\n', |
|
1040 |
+ ) |
|
1041 |
+ |
|
949 | 1042 |
|
950 | 1043 |
class TestCLIUtils: |
951 | 1044 |
def test_100_save_bad_config(self, monkeypatch: Any) -> None: |