jwbmisc 0.0.4__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jwbmisc-0.0.4/src/jwbmisc.egg-info → jwbmisc-0.0.6}/PKG-INFO +1 -1
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/_version.py +3 -3
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/exec.py +5 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/keeper.py +49 -21
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/passwd.py +5 -7
- {jwbmisc-0.0.4 → jwbmisc-0.0.6/src/jwbmisc.egg-info}/PKG-INFO +1 -1
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_keeper.py +60 -68
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/LICENSE +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/MANIFEST.in +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/Makefile +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/README.md +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/pyproject.toml +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/release.sh +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/setup.cfg +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/__init__.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/collection.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/fs.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/interactive.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/json.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc/string.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc.egg-info/SOURCES.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc.egg-info/dependency_links.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc.egg-info/requires.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/src/jwbmisc.egg-info/top_level.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/conftest.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/fzf +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/pass +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_collection.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_exec.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_fs.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_interactive.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_json.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_passwd.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.6}/tests/test_string.py +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 6)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g6e7b21fff'
|
|
@@ -9,6 +9,7 @@ def run_cmd(
|
|
|
9
9
|
stdin=None,
|
|
10
10
|
contains_sensitive_data=False,
|
|
11
11
|
timeout=20,
|
|
12
|
+
cwd=None,
|
|
12
13
|
decode=True,
|
|
13
14
|
dry_run=False,
|
|
14
15
|
):
|
|
@@ -22,6 +23,9 @@ def run_cmd(
|
|
|
22
23
|
|
|
23
24
|
cmd = [str(v) for v in cmd]
|
|
24
25
|
|
|
26
|
+
if cwd is not None:
|
|
27
|
+
cwd = str(cwd)
|
|
28
|
+
|
|
25
29
|
if dry_run:
|
|
26
30
|
print(cmd)
|
|
27
31
|
if capture:
|
|
@@ -35,6 +39,7 @@ def run_cmd(
|
|
|
35
39
|
env=env,
|
|
36
40
|
check=True,
|
|
37
41
|
timeout=timeout,
|
|
42
|
+
cwd=cwd,
|
|
38
43
|
input=stdin,
|
|
39
44
|
)
|
|
40
45
|
except sp.CalledProcessError as ex:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from
|
|
2
|
+
from typing import Any
|
|
3
|
+
from keepercommander import vault
|
|
3
4
|
from keepercommander import api
|
|
4
5
|
from time import sleep
|
|
5
6
|
from keepercommander.params import KeeperParams
|
|
@@ -13,8 +14,10 @@ from logging import getLogger
|
|
|
13
14
|
|
|
14
15
|
logger = getLogger(__name__)
|
|
15
16
|
|
|
17
|
+
CONFIG_FILE = Path.home() / ".config" / "jwbmisc" / "keeper.json"
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
class MinimalKeeperUI:
|
|
18
21
|
def __init__(self):
|
|
19
22
|
self.waiting_for_sso_data_key = False
|
|
20
23
|
|
|
@@ -65,19 +68,45 @@ class _MinimalKeeperUI:
|
|
|
65
68
|
step.resume()
|
|
66
69
|
|
|
67
70
|
|
|
68
|
-
def
|
|
69
|
-
if isinstance(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
def as_strings(vs: list[Any] | Any) -> list[str]:
|
|
72
|
+
if not isinstance(vs, list):
|
|
73
|
+
vs = [vs]
|
|
74
|
+
return [str(v) for v in vs]
|
|
75
|
+
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
def extract_record_field(record, field: str | None) -> str | None | list[dict[str, str | list[str]]]:
|
|
78
|
+
if isinstance(record, vault.PasswordRecord):
|
|
79
|
+
values = [
|
|
80
|
+
{"type": "password", "label": "login", "value": as_strings(record.login)},
|
|
81
|
+
{"type": "password", "label": "password", "value": as_strings(record.password)},
|
|
82
|
+
{"type": "password", "label": "link", "value": as_strings(record.link)},
|
|
83
|
+
] + [{"type": f.type, "label": f.name, "value": as_strings(f.value)} for f in record.custom]
|
|
76
84
|
|
|
85
|
+
elif isinstance(record, vault.TypedRecord):
|
|
86
|
+
fields = record.fields + record.custom
|
|
87
|
+
values = [{"type": f.type, "label": f.label, "value": as_strings(f.value)} for f in fields]
|
|
88
|
+
else:
|
|
89
|
+
raise TypeError("only TypedRecord & PasswordRecord are supported")
|
|
90
|
+
|
|
91
|
+
if not field:
|
|
92
|
+
return values
|
|
93
|
+
|
|
94
|
+
value = next(
|
|
95
|
+
(
|
|
96
|
+
v
|
|
97
|
+
for v in values
|
|
98
|
+
if (v["type"] and v["type"].lower() == field.lower())
|
|
99
|
+
or (v["label"] and v["label"].lower() == field.lower())
|
|
100
|
+
),
|
|
101
|
+
None,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if value and value["value"]:
|
|
105
|
+
return value["value"][0]
|
|
77
106
|
return None
|
|
78
107
|
|
|
79
108
|
|
|
80
|
-
def
|
|
109
|
+
def perform_login(params):
|
|
81
110
|
if not params.user:
|
|
82
111
|
user = os.environ.get("KEEPER_USERNAME", None)
|
|
83
112
|
if user is None:
|
|
@@ -96,29 +125,28 @@ def _perform_keeper_login(params):
|
|
|
96
125
|
params.server = server
|
|
97
126
|
|
|
98
127
|
try:
|
|
99
|
-
api.login(params, login_ui=
|
|
128
|
+
api.login(params, login_ui=MinimalKeeperUI())
|
|
100
129
|
except KeyboardInterrupt:
|
|
101
130
|
raise KeyError("\nKeeper login cancelled by user.") from None
|
|
102
131
|
except Exception as e:
|
|
103
132
|
raise KeyError(f"Keeper login failed: {e}") from e
|
|
104
133
|
|
|
105
134
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
def get_password(record_uid: str, field_path: str | None) -> str:
|
|
136
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
109
137
|
|
|
110
|
-
params = KeeperParams(config_filename=str(
|
|
138
|
+
params = KeeperParams(config_filename=str(CONFIG_FILE))
|
|
111
139
|
|
|
112
|
-
if
|
|
140
|
+
if CONFIG_FILE.exists():
|
|
113
141
|
try:
|
|
114
|
-
params.config = json.loads(
|
|
142
|
+
params.config = json.loads(CONFIG_FILE.read_text())
|
|
115
143
|
loader.load_config_properties(params)
|
|
116
144
|
if not params.session_token:
|
|
117
145
|
raise ValueError("No session token")
|
|
118
146
|
except Exception:
|
|
119
|
-
|
|
147
|
+
perform_login(params)
|
|
120
148
|
else:
|
|
121
|
-
|
|
149
|
+
perform_login(params)
|
|
122
150
|
|
|
123
151
|
try:
|
|
124
152
|
api.sync_down(params)
|
|
@@ -126,11 +154,11 @@ def get_keeper_password(record_uid: str, field_path: str) -> str:
|
|
|
126
154
|
raise KeyError(f"Failed to sync Keeper vault: {e}") from e
|
|
127
155
|
|
|
128
156
|
try:
|
|
129
|
-
record =
|
|
157
|
+
record = vault.KeeperRecord.load(params, record_uid)
|
|
130
158
|
except Exception as e:
|
|
131
159
|
raise KeyError(f"Record {record_uid} not found: {e}") from e
|
|
132
160
|
|
|
133
|
-
value =
|
|
161
|
+
value = extract_record_field(record, field_path)
|
|
134
162
|
if value is None:
|
|
135
163
|
raise KeyError(f"Field '{field_path}' not found in record {record_uid}")
|
|
136
164
|
|
|
@@ -40,11 +40,9 @@ def get_pass(*pass_keys: str):
|
|
|
40
40
|
|
|
41
41
|
if pass_key.startswith("keeper://"):
|
|
42
42
|
path = pass_key.removeprefix("keeper://")
|
|
43
|
-
if "/" not in path:
|
|
44
|
-
raise KeyError("Invalid keeper:// format. Expected: keeper://RECORD_UID/field/fieldname")
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
return _keeper_password(
|
|
44
|
+
parts = path.split("/")
|
|
45
|
+
return _keeper_password(*parts)
|
|
48
46
|
|
|
49
47
|
raise KeyError(f"Could not acquire password from one of {pass_keys}")
|
|
50
48
|
|
|
@@ -71,7 +69,7 @@ def _call_unix_pass(key, lnum=1):
|
|
|
71
69
|
return pw
|
|
72
70
|
|
|
73
71
|
|
|
74
|
-
def _keeper_password(record_uid: str, field_path: str) -> str:
|
|
75
|
-
from .keeper import
|
|
72
|
+
def _keeper_password(record_uid: str, field_path: str | None = None) -> str:
|
|
73
|
+
from .keeper import get_password as keeper_get_password
|
|
76
74
|
|
|
77
|
-
return
|
|
75
|
+
return keeper_get_password(record_uid, field_path)
|
|
@@ -1,69 +1,61 @@
|
|
|
1
|
+
# pyright: basic
|
|
1
2
|
import pytest
|
|
2
3
|
|
|
4
|
+
from keepercommander import vault, utils
|
|
3
5
|
from jwbmisc.keeper import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
MinimalKeeperUI,
|
|
7
|
+
extract_record_field,
|
|
8
|
+
perform_login,
|
|
9
|
+
get_password,
|
|
8
10
|
)
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_typed_field(self, name):
|
|
23
|
-
return self._fields.get(name)
|
|
13
|
+
def gen_record(type_name="login"):
|
|
14
|
+
record = vault.TypedRecord()
|
|
15
|
+
record.type_name = type_name
|
|
16
|
+
record.record_uid = utils.generate_uid()
|
|
17
|
+
record.record_key = utils.generate_aes_key()
|
|
18
|
+
record.fields.append(
|
|
19
|
+
vault.TypedField.new_field(
|
|
20
|
+
field_type="password", field_label="AWS Secret Sauce", field_value=["password123"]
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
return record
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class TestExtractKeeperField:
|
|
27
|
-
def test_typed_record_with_list_value(self
|
|
28
|
-
|
|
29
|
-
field = MockTypedField(["password123"])
|
|
30
|
-
record = MockTypedRecord(fields={"password": field})
|
|
27
|
+
def test_typed_record_with_list_value(self):
|
|
28
|
+
record = gen_record()
|
|
31
29
|
|
|
32
|
-
result =
|
|
30
|
+
result = extract_record_field(record, "password")
|
|
33
31
|
assert result == "password123"
|
|
34
32
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
result = _extract_keeper_field(record, "password")
|
|
41
|
-
assert result == "single_value"
|
|
42
|
-
|
|
43
|
-
def test_typed_record_field_not_found(self, mocker):
|
|
44
|
-
mocker.patch("jwbmisc.keeper.keeper_vault.TypedRecord", MockTypedRecord)
|
|
45
|
-
record = MockTypedRecord(fields={})
|
|
46
|
-
|
|
47
|
-
result = _extract_keeper_field(record, "missing")
|
|
33
|
+
def test_typed_record_field_not_found(self):
|
|
34
|
+
record = gen_record()
|
|
35
|
+
record.fields = []
|
|
36
|
+
result = extract_record_field(record, "missing")
|
|
48
37
|
assert result is None
|
|
49
38
|
|
|
50
|
-
def test_typed_record_custom_field(self
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
def test_typed_record_custom_field(self):
|
|
40
|
+
record = gen_record()
|
|
41
|
+
record.custom.append(
|
|
42
|
+
vault.TypedField.new_field(
|
|
43
|
+
field_type="passwordcustom", field_label="AWS Custom Secret Sauce", field_value=["password3"]
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
result = extract_record_field(record, "passwordcustom")
|
|
47
|
+
assert result == "password3"
|
|
48
|
+
result = extract_record_field(record, "AWS Custom Secret Sauce")
|
|
49
|
+
assert result == "password3"
|
|
58
50
|
|
|
59
51
|
def test_non_typed_record_returns_none(self):
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
with pytest.raises(TypeError):
|
|
53
|
+
extract_record_field(object(), "any_field")
|
|
62
54
|
|
|
63
55
|
|
|
64
56
|
class TestMinimalKeeperUI:
|
|
65
57
|
def test_on_password_raises(self):
|
|
66
|
-
ui =
|
|
58
|
+
ui = MinimalKeeperUI()
|
|
67
59
|
with pytest.raises(KeyError, match="Password login not supported"):
|
|
68
60
|
ui.on_password(None)
|
|
69
61
|
|
|
@@ -74,7 +66,7 @@ class TestMinimalKeeperUI:
|
|
|
74
66
|
step = mocker.MagicMock()
|
|
75
67
|
step.sso_login_url = "https://sso.example.com"
|
|
76
68
|
|
|
77
|
-
ui =
|
|
69
|
+
ui = MinimalKeeperUI()
|
|
78
70
|
ui.on_sso_redirect(step)
|
|
79
71
|
|
|
80
72
|
mock_webbrowser.open_new_tab.assert_called_once_with("https://sso.example.com")
|
|
@@ -87,7 +79,7 @@ class TestMinimalKeeperUI:
|
|
|
87
79
|
step = mocker.MagicMock()
|
|
88
80
|
step.sso_login_url = "https://sso.example.com"
|
|
89
81
|
|
|
90
|
-
ui =
|
|
82
|
+
ui = MinimalKeeperUI()
|
|
91
83
|
with pytest.raises(ValueError, match="No SSO token"):
|
|
92
84
|
ui.on_sso_redirect(step)
|
|
93
85
|
|
|
@@ -100,7 +92,7 @@ class TestMinimalKeeperUI:
|
|
|
100
92
|
step = mocker.MagicMock()
|
|
101
93
|
step.get_channels.return_value = [mock_channel]
|
|
102
94
|
|
|
103
|
-
ui =
|
|
95
|
+
ui = MinimalKeeperUI()
|
|
104
96
|
ui.on_two_factor(step)
|
|
105
97
|
|
|
106
98
|
step.send_code.assert_called_once_with("channel_123", "123456")
|
|
@@ -113,12 +105,12 @@ class TestMinimalKeeperUI:
|
|
|
113
105
|
step = mocker.MagicMock()
|
|
114
106
|
step.get_channels.return_value = [mock_channel]
|
|
115
107
|
|
|
116
|
-
ui =
|
|
108
|
+
ui = MinimalKeeperUI()
|
|
117
109
|
with pytest.raises(ValueError, match="TOTP authenticator not available"):
|
|
118
110
|
ui.on_two_factor(step)
|
|
119
111
|
|
|
120
112
|
def test_on_device_approval_does_not_raise(self, mocker):
|
|
121
|
-
ui =
|
|
113
|
+
ui = MinimalKeeperUI()
|
|
122
114
|
ui.on_device_approval(None) # Should not raise
|
|
123
115
|
|
|
124
116
|
def test_on_sso_data_key_first_call(self, mocker):
|
|
@@ -127,7 +119,7 @@ class TestMinimalKeeperUI:
|
|
|
127
119
|
|
|
128
120
|
step = mocker.MagicMock()
|
|
129
121
|
|
|
130
|
-
ui =
|
|
122
|
+
ui = MinimalKeeperUI()
|
|
131
123
|
assert ui.waiting_for_sso_data_key is False
|
|
132
124
|
|
|
133
125
|
ui.on_sso_data_key(step)
|
|
@@ -141,7 +133,7 @@ class TestMinimalKeeperUI:
|
|
|
141
133
|
mock_sleep = mocker.patch("jwbmisc.keeper.sleep")
|
|
142
134
|
step = mocker.MagicMock()
|
|
143
135
|
|
|
144
|
-
ui =
|
|
136
|
+
ui = MinimalKeeperUI()
|
|
145
137
|
ui.waiting_for_sso_data_key = True
|
|
146
138
|
|
|
147
139
|
ui.on_sso_data_key(step)
|
|
@@ -159,7 +151,7 @@ class TestPerformKeeperLogin:
|
|
|
159
151
|
params = mocker.MagicMock()
|
|
160
152
|
params.user = None
|
|
161
153
|
|
|
162
|
-
|
|
154
|
+
perform_login(params)
|
|
163
155
|
|
|
164
156
|
assert params.user == "user@example.com"
|
|
165
157
|
assert params.server == "keepersecurity.com"
|
|
@@ -173,7 +165,7 @@ class TestPerformKeeperLogin:
|
|
|
173
165
|
params = mocker.MagicMock()
|
|
174
166
|
params.user = None
|
|
175
167
|
|
|
176
|
-
|
|
168
|
+
perform_login(params)
|
|
177
169
|
|
|
178
170
|
assert params.user == "user@example.com"
|
|
179
171
|
assert params.server == "keepersecurity.com"
|
|
@@ -188,7 +180,7 @@ class TestPerformKeeperLogin:
|
|
|
188
180
|
params.user = None
|
|
189
181
|
|
|
190
182
|
with pytest.raises(KeyError, match="cancelled by user"):
|
|
191
|
-
|
|
183
|
+
perform_login(params)
|
|
192
184
|
|
|
193
185
|
|
|
194
186
|
class TestGetKeeperPassword:
|
|
@@ -200,15 +192,15 @@ class TestGetKeeperPassword:
|
|
|
200
192
|
mocker.patch("jwbmisc.keeper.KeeperParams", return_value=mock_params)
|
|
201
193
|
|
|
202
194
|
mock_api = mocker.patch("jwbmisc.keeper.api")
|
|
203
|
-
mock_vault = mocker.patch("jwbmisc.keeper.
|
|
195
|
+
mock_vault = mocker.patch("jwbmisc.keeper.vault")
|
|
204
196
|
|
|
205
197
|
mock_record = mocker.MagicMock()
|
|
206
198
|
mock_vault.KeeperRecord.load.return_value = mock_record
|
|
207
199
|
|
|
208
|
-
mocker.patch("jwbmisc.keeper.
|
|
209
|
-
mocker.patch("jwbmisc.keeper.
|
|
200
|
+
mocker.patch("jwbmisc.keeper.extract_record_field", return_value="the_password")
|
|
201
|
+
mocker.patch("jwbmisc.keeper.perform_login")
|
|
210
202
|
|
|
211
|
-
result =
|
|
203
|
+
result = get_password("RECORD123", "password")
|
|
212
204
|
|
|
213
205
|
assert result == "the_password"
|
|
214
206
|
mock_api.sync_down.assert_called_once()
|
|
@@ -221,14 +213,14 @@ class TestGetKeeperPassword:
|
|
|
221
213
|
mocker.patch("jwbmisc.keeper.KeeperParams", return_value=mock_params)
|
|
222
214
|
|
|
223
215
|
mocker.patch("jwbmisc.keeper.api")
|
|
224
|
-
mock_vault = mocker.patch("jwbmisc.keeper.
|
|
216
|
+
mock_vault = mocker.patch("jwbmisc.keeper.vault")
|
|
225
217
|
mock_vault.KeeperRecord.load.return_value = mocker.MagicMock()
|
|
226
218
|
|
|
227
|
-
mocker.patch("jwbmisc.keeper.
|
|
228
|
-
mocker.patch("jwbmisc.keeper.
|
|
219
|
+
mocker.patch("jwbmisc.keeper.extract_record_field", return_value=None)
|
|
220
|
+
mocker.patch("jwbmisc.keeper.perform_login")
|
|
229
221
|
|
|
230
222
|
with pytest.raises(KeyError, match="Field.*not found"):
|
|
231
|
-
|
|
223
|
+
get_password("RECORD123", "missing_field")
|
|
232
224
|
|
|
233
225
|
def test_sync_failure_raises(self, mocker, tmp_path):
|
|
234
226
|
mocker.patch("jwbmisc.keeper.Path.home", return_value=tmp_path)
|
|
@@ -240,10 +232,10 @@ class TestGetKeeperPassword:
|
|
|
240
232
|
mock_api = mocker.patch("jwbmisc.keeper.api")
|
|
241
233
|
mock_api.sync_down.side_effect = Exception("Sync failed")
|
|
242
234
|
|
|
243
|
-
mocker.patch("jwbmisc.keeper.
|
|
235
|
+
mocker.patch("jwbmisc.keeper.perform_login")
|
|
244
236
|
|
|
245
237
|
with pytest.raises(KeyError, match="Failed to sync"):
|
|
246
|
-
|
|
238
|
+
get_password("RECORD123", "password")
|
|
247
239
|
|
|
248
240
|
# def test_keeper_url(self, mocker):
|
|
249
241
|
# mock_keeper = mocker.patch("jwbmisc.passwd._keeper_password")
|
|
@@ -259,8 +251,8 @@ class TestGetKeeperPassword:
|
|
|
259
251
|
|
|
260
252
|
|
|
261
253
|
# class TestKeeperPassword:
|
|
262
|
-
# def
|
|
263
|
-
# mock_get = mocker.patch("jwbmisc.keeper.
|
|
254
|
+
# def test_delegates_to_get_password(self, mocker):
|
|
255
|
+
# mock_get = mocker.patch("jwbmisc.keeper.get_password")
|
|
264
256
|
# mock_get.return_value = "keeper_secret"
|
|
265
257
|
|
|
266
258
|
# result = _keeper_password("RECORD_UID", "field/path")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|