gitacc-switcher 1.2.1__tar.gz → 1.3.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.2.1/gitacc_switcher.egg-info → gitacc_switcher-1.3.0}/PKG-INFO +1 -1
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/__init__.py +1 -1
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/account_manager.py +61 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/cli.py +41 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/ssh_manager.py +49 -1
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0/gitacc_switcher.egg-info}/PKG-INFO +1 -1
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/pyproject.toml +1 -1
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/setup.py +1 -1
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/test_account_manager.py +105 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/test_cli.py +59 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/test_ssh_manager.py +48 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/LICENSE +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/MANIFEST.in +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/README.md +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/completion.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/config_manager.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/hook_manager.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher/utils.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/SOURCES.txt +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/dependency_links.txt +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/entry_points.txt +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/requires.txt +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/top_level.txt +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/requirements.txt +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/setup.cfg +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/__init__.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/test_completion.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/test_config_manager.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/test_hook_manager.py +0 -0
- {gitacc_switcher-1.2.1 → gitacc_switcher-1.3.0}/tests/test_utils.py +0 -0
|
@@ -382,6 +382,67 @@ class AccountManager:
|
|
|
382
382
|
|
|
383
383
|
return True
|
|
384
384
|
|
|
385
|
+
def get_status(self, repo_path: Optional[Path] = None) -> Dict[str, object]:
|
|
386
|
+
"""Report the currently active Git identity and SSH agent key, and
|
|
387
|
+
which registered account (if any) they match.
|
|
388
|
+
|
|
389
|
+
Read-only: makes no changes to Git config, the SSH agent, or any
|
|
390
|
+
managed files.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
repo_path: Path to repository (default: current directory). Used
|
|
394
|
+
to resolve local Git config when run inside a repo.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Dictionary with the current name/email, the matched account name
|
|
398
|
+
(or None), whether the SSH agent is running, and which account
|
|
399
|
+
(if any) the currently loaded agent key belongs to.
|
|
400
|
+
"""
|
|
401
|
+
if self.config_manager.is_git_repo(repo_path):
|
|
402
|
+
current_config = self.config_manager.get_current_git_config_local(repo_path)
|
|
403
|
+
else:
|
|
404
|
+
current_config = self.config_manager.get_current_git_config()
|
|
405
|
+
|
|
406
|
+
current_name = current_config.get("name")
|
|
407
|
+
current_email = current_config.get("email")
|
|
408
|
+
|
|
409
|
+
accounts = self.config_manager.read_accounts()
|
|
410
|
+
|
|
411
|
+
matched_account = None
|
|
412
|
+
for account_name, account_info in accounts.items():
|
|
413
|
+
if (
|
|
414
|
+
current_name
|
|
415
|
+
and current_email
|
|
416
|
+
and account_info.get("name") == current_name
|
|
417
|
+
and account_info.get("email") == current_email
|
|
418
|
+
):
|
|
419
|
+
matched_account = account_name
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
agent_running = self.ssh_manager.is_ssh_agent_running()
|
|
423
|
+
agent_fingerprints = (
|
|
424
|
+
self.ssh_manager.get_agent_fingerprints() if agent_running else []
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
matched_key_account = None
|
|
428
|
+
if agent_fingerprints:
|
|
429
|
+
for account_name, account_info in accounts.items():
|
|
430
|
+
public_key = account_info.get("public_key")
|
|
431
|
+
if not public_key:
|
|
432
|
+
continue
|
|
433
|
+
fingerprint = self.ssh_manager.get_key_fingerprint(public_key)
|
|
434
|
+
if fingerprint and fingerprint in agent_fingerprints:
|
|
435
|
+
matched_key_account = account_name
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
"name": current_name,
|
|
440
|
+
"email": current_email,
|
|
441
|
+
"matched_account": matched_account,
|
|
442
|
+
"agent_running": agent_running,
|
|
443
|
+
"matched_key_account": matched_key_account,
|
|
444
|
+
}
|
|
445
|
+
|
|
385
446
|
def init_repo(self, account_name: str, repo_path: Optional[Path] = None) -> bool:
|
|
386
447
|
"""Initialize repository with expected account and install pre-commit hook.
|
|
387
448
|
|
|
@@ -45,6 +45,7 @@ class CLI:
|
|
|
45
45
|
self._add_switch_command(subparsers)
|
|
46
46
|
self._add_list_command(subparsers)
|
|
47
47
|
self._add_logout_command(subparsers)
|
|
48
|
+
self._add_status_command(subparsers)
|
|
48
49
|
self._add_init_command(subparsers)
|
|
49
50
|
self._add_verify_command(subparsers)
|
|
50
51
|
self._add_update_command(subparsers)
|
|
@@ -74,6 +75,7 @@ Examples:
|
|
|
74
75
|
gitacc remove myaccount Remove an account
|
|
75
76
|
gitacc list List all registered accounts
|
|
76
77
|
gitacc logout Logout current account
|
|
78
|
+
gitacc status Show the currently active account
|
|
77
79
|
gitacc init myaccount Initialize repo with account validation
|
|
78
80
|
gitacc verify Verify current account matches repo
|
|
79
81
|
gitacc update myaccount Update Git name for an account
|
|
@@ -158,6 +160,16 @@ Examples:
|
|
|
158
160
|
)
|
|
159
161
|
parser.set_defaults(func=self._handle_logout)
|
|
160
162
|
|
|
163
|
+
def _add_status_command(self, subparsers: argparse._SubParsersAction) -> None:
|
|
164
|
+
"""Add the 'status' command parser."""
|
|
165
|
+
parser = subparsers.add_parser(
|
|
166
|
+
"status",
|
|
167
|
+
help="Show the currently active account",
|
|
168
|
+
description="Show the current Git identity and SSH agent key, "
|
|
169
|
+
"and which managed account (if any) matches",
|
|
170
|
+
)
|
|
171
|
+
parser.set_defaults(func=self._handle_status)
|
|
172
|
+
|
|
161
173
|
def _add_init_command(self, subparsers: argparse._SubParsersAction) -> None:
|
|
162
174
|
"""Add the 'init' command parser."""
|
|
163
175
|
parser = subparsers.add_parser(
|
|
@@ -396,6 +408,35 @@ Examples:
|
|
|
396
408
|
success = self.account_manager.logout()
|
|
397
409
|
return 0 if success else 1
|
|
398
410
|
|
|
411
|
+
def _handle_status(self, args: argparse.Namespace) -> int:
|
|
412
|
+
"""Handle the 'status' command."""
|
|
413
|
+
status = self.account_manager.get_status()
|
|
414
|
+
name = status.get("name")
|
|
415
|
+
email = status.get("email")
|
|
416
|
+
|
|
417
|
+
echo_color("g", "Current Git identity:")
|
|
418
|
+
echo_color("g", f" Name: {name or '(not set)'}")
|
|
419
|
+
echo_color("g", f" Email: {email or '(not set)'}")
|
|
420
|
+
|
|
421
|
+
matched_account = status.get("matched_account")
|
|
422
|
+
if matched_account:
|
|
423
|
+
echo_color("g", f'Matched account: "{matched_account}"')
|
|
424
|
+
else:
|
|
425
|
+
echo_color("y", "No managed account matches the current Git identity.")
|
|
426
|
+
|
|
427
|
+
if status.get("agent_running"):
|
|
428
|
+
matched_key_account = status.get("matched_key_account")
|
|
429
|
+
if matched_key_account:
|
|
430
|
+
echo_color(
|
|
431
|
+
"g", f'SSH agent key matches account: "{matched_key_account}"'
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
echo_color("y", "SSH agent key does not match any managed account.")
|
|
435
|
+
else:
|
|
436
|
+
echo_color("y", "SSH agent is not running.")
|
|
437
|
+
|
|
438
|
+
return 0
|
|
439
|
+
|
|
399
440
|
def _handle_init(self, args: argparse.Namespace) -> int:
|
|
400
441
|
"""Handle the 'init' command."""
|
|
401
442
|
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 Optional, Tuple
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SSHManager:
|
|
@@ -221,6 +221,54 @@ class SSHManager:
|
|
|
221
221
|
"ssh-add command not found. Please ensure SSH tools are installed.",
|
|
222
222
|
)
|
|
223
223
|
|
|
224
|
+
def get_agent_fingerprints(self) -> List[str]:
|
|
225
|
+
"""Get fingerprints of keys currently loaded in the SSH agent.
|
|
226
|
+
|
|
227
|
+
Read-only; does not modify the agent.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of fingerprint strings (e.g. "SHA256:abc..."). Empty if the
|
|
231
|
+
agent is not running, has no keys loaded, or can't be reached.
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
result = subprocess.run(["ssh-add", "-l"], capture_output=True, text=True)
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
if result.returncode != 0:
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
fingerprints = []
|
|
242
|
+
for line in result.stdout.strip().split("\n"):
|
|
243
|
+
parts = line.strip().split()
|
|
244
|
+
if len(parts) >= 2:
|
|
245
|
+
fingerprints.append(parts[1])
|
|
246
|
+
return fingerprints
|
|
247
|
+
|
|
248
|
+
def get_key_fingerprint(self, public_key_path: str) -> Optional[str]:
|
|
249
|
+
"""Get the fingerprint of a public key file.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
public_key_path: Path to the public key file
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Fingerprint string (e.g. "SHA256:abc...") or None on error
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
result = subprocess.run(
|
|
259
|
+
["ssh-keygen", "-lf", str(public_key_path)],
|
|
260
|
+
capture_output=True,
|
|
261
|
+
text=True,
|
|
262
|
+
)
|
|
263
|
+
except FileNotFoundError:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
if result.returncode != 0:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
parts = result.stdout.strip().split()
|
|
270
|
+
return parts[1] if len(parts) >= 2 else None
|
|
271
|
+
|
|
224
272
|
def get_public_key_content(self, public_key_path: str) -> Optional[str]:
|
|
225
273
|
"""Read public key content.
|
|
226
274
|
|
|
@@ -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.3.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",
|
|
@@ -426,3 +426,108 @@ class TestAddAccount:
|
|
|
426
426
|
assert result is True
|
|
427
427
|
_, kwargs = manager.config_manager.add_account.call_args
|
|
428
428
|
assert kwargs["sign"] is False
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class TestGetStatus:
|
|
432
|
+
def test_matches_account_by_identity(self, manager):
|
|
433
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
434
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
435
|
+
"name": "Work User",
|
|
436
|
+
"email": "work@company.com",
|
|
437
|
+
}
|
|
438
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
439
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
440
|
+
|
|
441
|
+
status = manager.get_status()
|
|
442
|
+
|
|
443
|
+
assert status["name"] == "Work User"
|
|
444
|
+
assert status["email"] == "work@company.com"
|
|
445
|
+
assert status["matched_account"] == "work"
|
|
446
|
+
assert status["agent_running"] is False
|
|
447
|
+
assert status["matched_key_account"] is None
|
|
448
|
+
|
|
449
|
+
def test_no_match_when_identity_unknown(self, manager):
|
|
450
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
451
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
452
|
+
"name": "Someone Else",
|
|
453
|
+
"email": "someone@else.com",
|
|
454
|
+
}
|
|
455
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
456
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
457
|
+
|
|
458
|
+
status = manager.get_status()
|
|
459
|
+
|
|
460
|
+
assert status["matched_account"] is None
|
|
461
|
+
|
|
462
|
+
def test_no_match_when_identity_unset(self, manager):
|
|
463
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
464
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
465
|
+
"name": None,
|
|
466
|
+
"email": None,
|
|
467
|
+
}
|
|
468
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
469
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
470
|
+
|
|
471
|
+
status = manager.get_status()
|
|
472
|
+
|
|
473
|
+
assert status["matched_account"] is None
|
|
474
|
+
|
|
475
|
+
def test_uses_local_git_config_inside_repo(self, manager):
|
|
476
|
+
manager.config_manager.is_git_repo.return_value = True
|
|
477
|
+
manager.config_manager.get_current_git_config_local.return_value = {
|
|
478
|
+
"name": "Work User",
|
|
479
|
+
"email": "work@company.com",
|
|
480
|
+
}
|
|
481
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
482
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
483
|
+
|
|
484
|
+
status = manager.get_status()
|
|
485
|
+
|
|
486
|
+
manager.config_manager.get_current_git_config_local.assert_called_once()
|
|
487
|
+
manager.config_manager.get_current_git_config.assert_not_called()
|
|
488
|
+
assert status["matched_account"] == "work"
|
|
489
|
+
|
|
490
|
+
def test_matches_account_by_agent_key_fingerprint(self, manager):
|
|
491
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
492
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
493
|
+
"name": None,
|
|
494
|
+
"email": None,
|
|
495
|
+
}
|
|
496
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
497
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = True
|
|
498
|
+
manager.ssh_manager.get_agent_fingerprints.return_value = ["SHA256:abc123"]
|
|
499
|
+
manager.ssh_manager.get_key_fingerprint.return_value = "SHA256:abc123"
|
|
500
|
+
|
|
501
|
+
status = manager.get_status()
|
|
502
|
+
|
|
503
|
+
assert status["agent_running"] is True
|
|
504
|
+
assert status["matched_key_account"] == "work"
|
|
505
|
+
|
|
506
|
+
def test_no_key_match_when_fingerprints_differ(self, manager):
|
|
507
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
508
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
509
|
+
"name": None,
|
|
510
|
+
"email": None,
|
|
511
|
+
}
|
|
512
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
513
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = True
|
|
514
|
+
manager.ssh_manager.get_agent_fingerprints.return_value = ["SHA256:other"]
|
|
515
|
+
manager.ssh_manager.get_key_fingerprint.return_value = "SHA256:abc123"
|
|
516
|
+
|
|
517
|
+
status = manager.get_status()
|
|
518
|
+
|
|
519
|
+
assert status["matched_key_account"] is None
|
|
520
|
+
|
|
521
|
+
def test_no_key_lookup_when_agent_not_running(self, manager):
|
|
522
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
523
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
524
|
+
"name": None,
|
|
525
|
+
"email": None,
|
|
526
|
+
}
|
|
527
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
528
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
529
|
+
|
|
530
|
+
status = manager.get_status()
|
|
531
|
+
|
|
532
|
+
manager.ssh_manager.get_agent_fingerprints.assert_not_called()
|
|
533
|
+
assert status["matched_key_account"] is None
|
|
@@ -69,6 +69,9 @@ class TestParserCommands:
|
|
|
69
69
|
def test_logout_command(self, cli):
|
|
70
70
|
assert cli.parser.parse_args(["logout"]).command == "logout"
|
|
71
71
|
|
|
72
|
+
def test_status_command(self, cli):
|
|
73
|
+
assert cli.parser.parse_args(["status"]).command == "status"
|
|
74
|
+
|
|
72
75
|
def test_init_parses_account(self, cli):
|
|
73
76
|
args = cli.parser.parse_args(["init", "mywork"])
|
|
74
77
|
assert args.account_name == "mywork"
|
|
@@ -170,6 +173,50 @@ class TestHandlers:
|
|
|
170
173
|
cli.account_manager.verify_account.return_value = False
|
|
171
174
|
assert cli._handle_verify(argparse.Namespace()) == 1
|
|
172
175
|
|
|
176
|
+
def test_handle_status_matched(self, cli, capsys):
|
|
177
|
+
cli.account_manager.get_status.return_value = {
|
|
178
|
+
"name": "Work User",
|
|
179
|
+
"email": "work@co.com",
|
|
180
|
+
"matched_account": "work",
|
|
181
|
+
"agent_running": True,
|
|
182
|
+
"matched_key_account": "work",
|
|
183
|
+
}
|
|
184
|
+
result = cli._handle_status(argparse.Namespace())
|
|
185
|
+
out = capsys.readouterr().out
|
|
186
|
+
assert result == 0
|
|
187
|
+
assert "Work User" in out
|
|
188
|
+
assert "work@co.com" in out
|
|
189
|
+
assert 'Matched account: "work"' in out
|
|
190
|
+
assert 'SSH agent key matches account: "work"' in out
|
|
191
|
+
|
|
192
|
+
def test_handle_status_no_match(self, cli, capsys):
|
|
193
|
+
cli.account_manager.get_status.return_value = {
|
|
194
|
+
"name": "Someone",
|
|
195
|
+
"email": "someone@else.com",
|
|
196
|
+
"matched_account": None,
|
|
197
|
+
"agent_running": True,
|
|
198
|
+
"matched_key_account": None,
|
|
199
|
+
}
|
|
200
|
+
result = cli._handle_status(argparse.Namespace())
|
|
201
|
+
out = capsys.readouterr().out
|
|
202
|
+
assert result == 0
|
|
203
|
+
assert "No managed account matches" in out
|
|
204
|
+
assert "does not match any managed account" in out
|
|
205
|
+
|
|
206
|
+
def test_handle_status_no_identity_set(self, cli, capsys):
|
|
207
|
+
cli.account_manager.get_status.return_value = {
|
|
208
|
+
"name": None,
|
|
209
|
+
"email": None,
|
|
210
|
+
"matched_account": None,
|
|
211
|
+
"agent_running": False,
|
|
212
|
+
"matched_key_account": None,
|
|
213
|
+
}
|
|
214
|
+
result = cli._handle_status(argparse.Namespace())
|
|
215
|
+
out = capsys.readouterr().out
|
|
216
|
+
assert result == 0
|
|
217
|
+
assert "(not set)" in out
|
|
218
|
+
assert "SSH agent is not running." in out
|
|
219
|
+
|
|
173
220
|
def test_handle_init_success(self, cli):
|
|
174
221
|
cli.account_manager.init_repo.return_value = True
|
|
175
222
|
assert cli._handle_init(argparse.Namespace(account_name="work")) == 0
|
|
@@ -295,3 +342,15 @@ class TestRun:
|
|
|
295
342
|
with patch.object(sys, "argv", ["gitacc", "logout"]):
|
|
296
343
|
result = cli.run()
|
|
297
344
|
assert result == 0
|
|
345
|
+
|
|
346
|
+
def test_status_command_runs(self, cli):
|
|
347
|
+
cli.account_manager.get_status.return_value = {
|
|
348
|
+
"name": None,
|
|
349
|
+
"email": None,
|
|
350
|
+
"matched_account": None,
|
|
351
|
+
"agent_running": False,
|
|
352
|
+
"matched_key_account": None,
|
|
353
|
+
}
|
|
354
|
+
with patch.object(sys, "argv", ["gitacc", "status"]):
|
|
355
|
+
result = cli.run()
|
|
356
|
+
assert result == 0
|
|
@@ -183,3 +183,51 @@ class TestGetPublicKeyContent:
|
|
|
183
183
|
|
|
184
184
|
def test_returns_none_for_missing_file(self, ssh, tmp_path):
|
|
185
185
|
assert ssh.get_public_key_content(str(tmp_path / "nonexistent.pub")) is None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestGetAgentFingerprints:
|
|
189
|
+
def test_returns_fingerprints_for_loaded_keys(self, ssh):
|
|
190
|
+
output = (
|
|
191
|
+
"2048 SHA256:abc123 work@co.com (RSA)\n"
|
|
192
|
+
"256 SHA256:def456 personal@co.com (ED25519)\n"
|
|
193
|
+
)
|
|
194
|
+
with patch(
|
|
195
|
+
"subprocess.run",
|
|
196
|
+
return_value=MagicMock(returncode=0, stdout=output),
|
|
197
|
+
):
|
|
198
|
+
fingerprints = ssh.get_agent_fingerprints()
|
|
199
|
+
assert fingerprints == ["SHA256:abc123", "SHA256:def456"]
|
|
200
|
+
|
|
201
|
+
def test_returns_empty_when_agent_has_no_keys(self, ssh):
|
|
202
|
+
with patch(
|
|
203
|
+
"subprocess.run",
|
|
204
|
+
return_value=MagicMock(returncode=1, stdout=""),
|
|
205
|
+
):
|
|
206
|
+
assert ssh.get_agent_fingerprints() == []
|
|
207
|
+
|
|
208
|
+
def test_returns_empty_when_ssh_add_missing(self, ssh):
|
|
209
|
+
with patch("subprocess.run", side_effect=FileNotFoundError):
|
|
210
|
+
assert ssh.get_agent_fingerprints() == []
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TestGetKeyFingerprint:
|
|
214
|
+
def test_returns_fingerprint(self, ssh, tmp_path):
|
|
215
|
+
pub_key = tmp_path / "id_rsa_test.pub"
|
|
216
|
+
pub_key.write_text("ssh-rsa AAAA test@test.com")
|
|
217
|
+
with patch(
|
|
218
|
+
"subprocess.run",
|
|
219
|
+
return_value=MagicMock(
|
|
220
|
+
returncode=0, stdout="2048 SHA256:abc123 test@test.com (RSA)\n"
|
|
221
|
+
),
|
|
222
|
+
):
|
|
223
|
+
assert ssh.get_key_fingerprint(str(pub_key)) == "SHA256:abc123"
|
|
224
|
+
|
|
225
|
+
def test_returns_none_on_failure(self, ssh, tmp_path):
|
|
226
|
+
pub_key = tmp_path / "id_rsa_test.pub"
|
|
227
|
+
with patch("subprocess.run", return_value=MagicMock(returncode=1, stdout="")):
|
|
228
|
+
assert ssh.get_key_fingerprint(str(pub_key)) is None
|
|
229
|
+
|
|
230
|
+
def test_returns_none_when_ssh_keygen_missing(self, ssh, tmp_path):
|
|
231
|
+
pub_key = tmp_path / "id_rsa_test.pub"
|
|
232
|
+
with patch("subprocess.run", side_effect=FileNotFoundError):
|
|
233
|
+
assert ssh.get_key_fingerprint(str(pub_key)) is None
|
|
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.2.1 → gitacc_switcher-1.3.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
|