gitacc-switcher 1.4.0__tar.gz → 1.5.0__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.
- {gitacc_switcher-1.4.0/gitacc_switcher.egg-info → gitacc_switcher-1.5.0}/PKG-INFO +1 -1
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/__init__.py +1 -1
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/account_manager.py +29 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/cli.py +47 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/ssh_manager.py +27 -1
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0/gitacc_switcher.egg-info}/PKG-INFO +1 -1
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/pyproject.toml +1 -1
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/setup.py +1 -1
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_account_manager.py +61 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_cli.py +81 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_ssh_manager.py +44 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/LICENSE +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/MANIFEST.in +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/README.md +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/completion.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/config_manager.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/hook_manager.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/rules_manager.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/shell_hook.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher/utils.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher.egg-info/SOURCES.txt +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher.egg-info/dependency_links.txt +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher.egg-info/entry_points.txt +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher.egg-info/requires.txt +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher.egg-info/top_level.txt +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/requirements.txt +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/setup.cfg +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/__init__.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_completion.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_config_manager.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_hook_manager.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_rules_manager.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_shell_hook.py +0 -0
- {gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/tests/test_utils.py +0 -0
|
@@ -384,6 +384,35 @@ class AccountManager:
|
|
|
384
384
|
|
|
385
385
|
return True
|
|
386
386
|
|
|
387
|
+
def run_doctor(self) -> Dict[str, object]:
|
|
388
|
+
"""Run read-only health checks on the SSH agent and managed key files.
|
|
389
|
+
|
|
390
|
+
Checks whether the SSH agent is running and reachable, and whether
|
|
391
|
+
each registered account's private key file exists, is readable, and
|
|
392
|
+
has safe permissions. Makes no changes.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Dictionary with 'agent_running' (bool) and 'accounts' (dict
|
|
396
|
+
mapping account name to its key-file check result — see
|
|
397
|
+
SSHManager.check_key_file).
|
|
398
|
+
"""
|
|
399
|
+
agent_running = self.ssh_manager.is_ssh_agent_running()
|
|
400
|
+
|
|
401
|
+
accounts = self.config_manager.read_accounts()
|
|
402
|
+
account_checks = {}
|
|
403
|
+
for account_name, account_info in accounts.items():
|
|
404
|
+
private_key = account_info.get("private_key")
|
|
405
|
+
if not private_key:
|
|
406
|
+
account_checks[account_name] = {
|
|
407
|
+
"exists": False,
|
|
408
|
+
"readable": False,
|
|
409
|
+
"permissions_ok": False,
|
|
410
|
+
}
|
|
411
|
+
continue
|
|
412
|
+
account_checks[account_name] = self.ssh_manager.check_key_file(private_key)
|
|
413
|
+
|
|
414
|
+
return {"agent_running": agent_running, "accounts": account_checks}
|
|
415
|
+
|
|
387
416
|
def get_status(self, repo_path: Optional[Path] = None) -> Dict[str, object]:
|
|
388
417
|
"""Report the currently active Git identity and SSH agent key, and
|
|
389
418
|
which registered account (if any) they match.
|
|
@@ -48,6 +48,7 @@ class CLI:
|
|
|
48
48
|
self._add_list_command(subparsers)
|
|
49
49
|
self._add_logout_command(subparsers)
|
|
50
50
|
self._add_status_command(subparsers)
|
|
51
|
+
self._add_doctor_command(subparsers)
|
|
51
52
|
self._add_init_command(subparsers)
|
|
52
53
|
self._add_verify_command(subparsers)
|
|
53
54
|
self._add_update_command(subparsers)
|
|
@@ -79,6 +80,7 @@ Examples:
|
|
|
79
80
|
gitacc list List all registered accounts
|
|
80
81
|
gitacc logout Logout current account
|
|
81
82
|
gitacc status Show the currently active account
|
|
83
|
+
gitacc doctor Run health checks on the SSH agent and keys
|
|
82
84
|
gitacc init myaccount Initialize repo with account validation
|
|
83
85
|
gitacc verify Verify current account matches repo
|
|
84
86
|
gitacc update myaccount Update Git name for an account
|
|
@@ -175,6 +177,17 @@ Examples:
|
|
|
175
177
|
)
|
|
176
178
|
parser.set_defaults(func=self._handle_status)
|
|
177
179
|
|
|
180
|
+
def _add_doctor_command(self, subparsers: argparse._SubParsersAction) -> None:
|
|
181
|
+
"""Add the 'doctor' command parser."""
|
|
182
|
+
parser = subparsers.add_parser(
|
|
183
|
+
"doctor",
|
|
184
|
+
help="Run health checks on the SSH agent and managed keys",
|
|
185
|
+
description="Check that the SSH agent is running and reachable, "
|
|
186
|
+
"and that each registered account's key file exists and has "
|
|
187
|
+
"safe permissions. Read-only.",
|
|
188
|
+
)
|
|
189
|
+
parser.set_defaults(func=self._handle_doctor)
|
|
190
|
+
|
|
178
191
|
def _add_init_command(self, subparsers: argparse._SubParsersAction) -> None:
|
|
179
192
|
"""Add the 'init' command parser."""
|
|
180
193
|
parser = subparsers.add_parser(
|
|
@@ -516,6 +529,40 @@ Examples:
|
|
|
516
529
|
|
|
517
530
|
return 0
|
|
518
531
|
|
|
532
|
+
def _handle_doctor(self, args: argparse.Namespace) -> int:
|
|
533
|
+
"""Handle the 'doctor' command."""
|
|
534
|
+
report = self.account_manager.run_doctor()
|
|
535
|
+
all_ok = True
|
|
536
|
+
|
|
537
|
+
if report.get("agent_running"):
|
|
538
|
+
echo_color("g", "✓ SSH agent is running and reachable")
|
|
539
|
+
else:
|
|
540
|
+
echo_color("r", "✗ SSH agent is not running or not reachable")
|
|
541
|
+
all_ok = False
|
|
542
|
+
|
|
543
|
+
accounts = report.get("accounts", {})
|
|
544
|
+
if not accounts:
|
|
545
|
+
echo_color("y", "No accounts registered yet.")
|
|
546
|
+
|
|
547
|
+
for account_name, check in accounts.items():
|
|
548
|
+
if not check.get("exists"):
|
|
549
|
+
echo_color("r", f'✗ Account "{account_name}": key file is missing')
|
|
550
|
+
all_ok = False
|
|
551
|
+
elif not check.get("readable"):
|
|
552
|
+
echo_color("r", f'✗ Account "{account_name}": key file is unreadable')
|
|
553
|
+
all_ok = False
|
|
554
|
+
elif not check.get("permissions_ok"):
|
|
555
|
+
echo_color(
|
|
556
|
+
"r",
|
|
557
|
+
f'✗ Account "{account_name}": key file permissions are too '
|
|
558
|
+
"permissive (should be readable by owner only)",
|
|
559
|
+
)
|
|
560
|
+
all_ok = False
|
|
561
|
+
else:
|
|
562
|
+
echo_color("g", f'✓ Account "{account_name}": key file OK')
|
|
563
|
+
|
|
564
|
+
return 0 if all_ok else 1
|
|
565
|
+
|
|
519
566
|
def _handle_init(self, args: argparse.Namespace) -> int:
|
|
520
567
|
"""Handle the 'init' command."""
|
|
521
568
|
success = self.account_manager.init_repo(args.account_name)
|
|
@@ -6,7 +6,7 @@ import subprocess
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import re
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import List, Optional, Tuple
|
|
9
|
+
from typing import Dict, List, Optional, Tuple
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SSHManager:
|
|
@@ -269,6 +269,32 @@ class SSHManager:
|
|
|
269
269
|
parts = result.stdout.strip().split()
|
|
270
270
|
return parts[1] if len(parts) >= 2 else None
|
|
271
271
|
|
|
272
|
+
def check_key_file(self, private_key_path: str) -> Dict[str, bool]:
|
|
273
|
+
"""Check a managed private key file's existence, readability, and permissions.
|
|
274
|
+
|
|
275
|
+
Read-only; does not modify anything it checks.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
private_key_path: Path to the private key file to check
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Dictionary with 'exists', 'readable', and 'permissions_ok' booleans.
|
|
282
|
+
'permissions_ok' is True when the file is not accessible to
|
|
283
|
+
group/other, matching the 0600 mode ssh-keygen gives private keys
|
|
284
|
+
on creation; it is only meaningful when 'exists' and 'readable'
|
|
285
|
+
are both True.
|
|
286
|
+
"""
|
|
287
|
+
path = Path(private_key_path)
|
|
288
|
+
if not path.is_file():
|
|
289
|
+
return {"exists": False, "readable": False, "permissions_ok": False}
|
|
290
|
+
|
|
291
|
+
if not os.access(path, os.R_OK):
|
|
292
|
+
return {"exists": True, "readable": False, "permissions_ok": False}
|
|
293
|
+
|
|
294
|
+
mode = path.stat().st_mode
|
|
295
|
+
permissions_ok = (mode & 0o077) == 0
|
|
296
|
+
return {"exists": True, "readable": True, "permissions_ok": permissions_ok}
|
|
297
|
+
|
|
272
298
|
def get_public_key_content(self, public_key_path: str) -> Optional[str]:
|
|
273
299
|
"""Read public key content.
|
|
274
300
|
|
|
@@ -9,7 +9,7 @@ long_description = readme_file.read_text() if readme_file.exists() else ""
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="gitacc-switcher",
|
|
12
|
-
version="1.
|
|
12
|
+
version="1.5.0",
|
|
13
13
|
description="Git Account Switcher - Manage multiple Git SSH accounts easily",
|
|
14
14
|
long_description=long_description,
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
@@ -531,3 +531,64 @@ class TestGetStatus:
|
|
|
531
531
|
|
|
532
532
|
manager.ssh_manager.get_agent_fingerprints.assert_not_called()
|
|
533
533
|
assert status["matched_key_account"] is None
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class TestRunDoctor:
|
|
537
|
+
def test_agent_running_and_all_keys_ok(self, manager, tmp_path):
|
|
538
|
+
key = tmp_path / "id_rsa_work"
|
|
539
|
+
key.write_text("private")
|
|
540
|
+
key.chmod(0o600)
|
|
541
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = True
|
|
542
|
+
manager.config_manager.read_accounts.return_value = {
|
|
543
|
+
"work": {**WORK_ACCOUNT, "private_key": str(key)}
|
|
544
|
+
}
|
|
545
|
+
manager.ssh_manager.check_key_file.return_value = {
|
|
546
|
+
"exists": True,
|
|
547
|
+
"readable": True,
|
|
548
|
+
"permissions_ok": True,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
report = manager.run_doctor()
|
|
552
|
+
|
|
553
|
+
assert report["agent_running"] is True
|
|
554
|
+
assert report["accounts"] == {
|
|
555
|
+
"work": {"exists": True, "readable": True, "permissions_ok": True}
|
|
556
|
+
}
|
|
557
|
+
manager.ssh_manager.check_key_file.assert_called_once_with(str(key))
|
|
558
|
+
|
|
559
|
+
def test_agent_not_running(self, manager):
|
|
560
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
561
|
+
manager.config_manager.read_accounts.return_value = {}
|
|
562
|
+
|
|
563
|
+
report = manager.run_doctor()
|
|
564
|
+
|
|
565
|
+
assert report["agent_running"] is False
|
|
566
|
+
assert report["accounts"] == {}
|
|
567
|
+
|
|
568
|
+
def test_flags_account_with_no_private_key_recorded(self, manager):
|
|
569
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = True
|
|
570
|
+
manager.config_manager.read_accounts.return_value = {
|
|
571
|
+
"broken": {"name": "Broken", "email": "b@co.com"}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
report = manager.run_doctor()
|
|
575
|
+
|
|
576
|
+
assert report["accounts"] == {
|
|
577
|
+
"broken": {"exists": False, "readable": False, "permissions_ok": False}
|
|
578
|
+
}
|
|
579
|
+
manager.ssh_manager.check_key_file.assert_not_called()
|
|
580
|
+
|
|
581
|
+
def test_flags_missing_key_file(self, manager):
|
|
582
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = True
|
|
583
|
+
manager.config_manager.read_accounts.return_value = {
|
|
584
|
+
"work": {**WORK_ACCOUNT, "private_key": "/nonexistent/id_rsa_work"}
|
|
585
|
+
}
|
|
586
|
+
manager.ssh_manager.check_key_file.return_value = {
|
|
587
|
+
"exists": False,
|
|
588
|
+
"readable": False,
|
|
589
|
+
"permissions_ok": False,
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
report = manager.run_doctor()
|
|
593
|
+
|
|
594
|
+
assert report["accounts"]["work"]["exists"] is False
|
|
@@ -79,6 +79,9 @@ class TestParserCommands:
|
|
|
79
79
|
def test_verify_command(self, cli):
|
|
80
80
|
assert cli.parser.parse_args(["verify"]).command == "verify"
|
|
81
81
|
|
|
82
|
+
def test_doctor_command(self, cli):
|
|
83
|
+
assert cli.parser.parse_args(["doctor"]).command == "doctor"
|
|
84
|
+
|
|
82
85
|
def test_update_no_flags(self, cli):
|
|
83
86
|
args = cli.parser.parse_args(["update", "mywork"])
|
|
84
87
|
assert args.account_name == "mywork"
|
|
@@ -217,6 +220,75 @@ class TestHandlers:
|
|
|
217
220
|
assert "(not set)" in out
|
|
218
221
|
assert "SSH agent is not running." in out
|
|
219
222
|
|
|
223
|
+
def test_handle_doctor_all_ok(self, cli, capsys):
|
|
224
|
+
cli.account_manager.run_doctor.return_value = {
|
|
225
|
+
"agent_running": True,
|
|
226
|
+
"accounts": {
|
|
227
|
+
"work": {"exists": True, "readable": True, "permissions_ok": True}
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
result = cli._handle_doctor(argparse.Namespace())
|
|
231
|
+
out = capsys.readouterr().out
|
|
232
|
+
assert result == 0
|
|
233
|
+
assert "SSH agent is running and reachable" in out
|
|
234
|
+
assert 'Account "work": key file OK' in out
|
|
235
|
+
|
|
236
|
+
def test_handle_doctor_agent_not_running(self, cli, capsys):
|
|
237
|
+
cli.account_manager.run_doctor.return_value = {
|
|
238
|
+
"agent_running": False,
|
|
239
|
+
"accounts": {},
|
|
240
|
+
}
|
|
241
|
+
result = cli._handle_doctor(argparse.Namespace())
|
|
242
|
+
out = capsys.readouterr().out
|
|
243
|
+
assert result == 1
|
|
244
|
+
assert "not running or not reachable" in out
|
|
245
|
+
|
|
246
|
+
def test_handle_doctor_missing_key(self, cli, capsys):
|
|
247
|
+
cli.account_manager.run_doctor.return_value = {
|
|
248
|
+
"agent_running": True,
|
|
249
|
+
"accounts": {
|
|
250
|
+
"work": {"exists": False, "readable": False, "permissions_ok": False}
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
result = cli._handle_doctor(argparse.Namespace())
|
|
254
|
+
out = capsys.readouterr().out
|
|
255
|
+
assert result == 1
|
|
256
|
+
assert 'Account "work": key file is missing' in out
|
|
257
|
+
|
|
258
|
+
def test_handle_doctor_unreadable_key(self, cli, capsys):
|
|
259
|
+
cli.account_manager.run_doctor.return_value = {
|
|
260
|
+
"agent_running": True,
|
|
261
|
+
"accounts": {
|
|
262
|
+
"work": {"exists": True, "readable": False, "permissions_ok": False}
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
result = cli._handle_doctor(argparse.Namespace())
|
|
266
|
+
out = capsys.readouterr().out
|
|
267
|
+
assert result == 1
|
|
268
|
+
assert 'Account "work": key file is unreadable' in out
|
|
269
|
+
|
|
270
|
+
def test_handle_doctor_bad_permissions(self, cli, capsys):
|
|
271
|
+
cli.account_manager.run_doctor.return_value = {
|
|
272
|
+
"agent_running": True,
|
|
273
|
+
"accounts": {
|
|
274
|
+
"work": {"exists": True, "readable": True, "permissions_ok": False}
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
result = cli._handle_doctor(argparse.Namespace())
|
|
278
|
+
out = capsys.readouterr().out
|
|
279
|
+
assert result == 1
|
|
280
|
+
assert "permissions are too permissive" in out
|
|
281
|
+
|
|
282
|
+
def test_handle_doctor_no_accounts(self, cli, capsys):
|
|
283
|
+
cli.account_manager.run_doctor.return_value = {
|
|
284
|
+
"agent_running": True,
|
|
285
|
+
"accounts": {},
|
|
286
|
+
}
|
|
287
|
+
result = cli._handle_doctor(argparse.Namespace())
|
|
288
|
+
out = capsys.readouterr().out
|
|
289
|
+
assert result == 0
|
|
290
|
+
assert "No accounts registered yet." in out
|
|
291
|
+
|
|
220
292
|
def test_handle_init_success(self, cli):
|
|
221
293
|
cli.account_manager.init_repo.return_value = True
|
|
222
294
|
assert cli._handle_init(argparse.Namespace(account_name="work")) == 0
|
|
@@ -355,6 +427,15 @@ class TestRun:
|
|
|
355
427
|
result = cli.run()
|
|
356
428
|
assert result == 0
|
|
357
429
|
|
|
430
|
+
def test_doctor_command_runs(self, cli):
|
|
431
|
+
cli.account_manager.run_doctor.return_value = {
|
|
432
|
+
"agent_running": True,
|
|
433
|
+
"accounts": {},
|
|
434
|
+
}
|
|
435
|
+
with patch.object(sys, "argv", ["gitacc", "doctor"]):
|
|
436
|
+
result = cli.run()
|
|
437
|
+
assert result == 0
|
|
438
|
+
|
|
358
439
|
|
|
359
440
|
# ---------------------------------------------------------------------------
|
|
360
441
|
# `rule` command parsing
|
|
@@ -210,6 +210,50 @@ class TestGetAgentFingerprints:
|
|
|
210
210
|
assert ssh.get_agent_fingerprints() == []
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
class TestCheckKeyFile:
|
|
214
|
+
def test_missing_file(self, ssh, tmp_path):
|
|
215
|
+
result = ssh.check_key_file(str(tmp_path / "nonexistent"))
|
|
216
|
+
assert result == {
|
|
217
|
+
"exists": False,
|
|
218
|
+
"readable": False,
|
|
219
|
+
"permissions_ok": False,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def test_correct_permissions(self, ssh, tmp_path):
|
|
223
|
+
key = tmp_path / "id_rsa_test"
|
|
224
|
+
key.write_text("private key data")
|
|
225
|
+
key.chmod(0o600)
|
|
226
|
+
result = ssh.check_key_file(str(key))
|
|
227
|
+
assert result == {
|
|
228
|
+
"exists": True,
|
|
229
|
+
"readable": True,
|
|
230
|
+
"permissions_ok": True,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def test_too_permissive(self, ssh, tmp_path):
|
|
234
|
+
key = tmp_path / "id_rsa_test"
|
|
235
|
+
key.write_text("private key data")
|
|
236
|
+
key.chmod(0o644)
|
|
237
|
+
result = ssh.check_key_file(str(key))
|
|
238
|
+
assert result == {
|
|
239
|
+
"exists": True,
|
|
240
|
+
"readable": True,
|
|
241
|
+
"permissions_ok": False,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def test_unreadable_file(self, ssh, tmp_path):
|
|
245
|
+
key = tmp_path / "id_rsa_test"
|
|
246
|
+
key.write_text("private key data")
|
|
247
|
+
key.chmod(0o600)
|
|
248
|
+
with patch("os.access", return_value=False):
|
|
249
|
+
result = ssh.check_key_file(str(key))
|
|
250
|
+
assert result == {
|
|
251
|
+
"exists": True,
|
|
252
|
+
"readable": False,
|
|
253
|
+
"permissions_ok": False,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
213
257
|
class TestGetKeyFingerprint:
|
|
214
258
|
def test_returns_fingerprint(self, ssh, tmp_path):
|
|
215
259
|
pub_key = tmp_path / "id_rsa_test.pub"
|
|
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
|
{gitacc_switcher-1.4.0 → gitacc_switcher-1.5.0}/gitacc_switcher.egg-info/dependency_links.txt
RENAMED
|
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
|