generated from coulomb/repo-seed
feat(local-identity): add --username and --fullname overrides to init
Resolves the system identity mismatch between the Linux username (worsch) and the bootstrap identity (tegwick / Bernd Worsch / custom email). Resolution order for all three fields: flag > config > system derivation. Config is updated on every init so --force reinits are idempotent without repeating the flags. - cli.py: extract _resolve_init_params(); add --username / --fullname args; persist all three fields to config.yaml on init - tests/test_cli.py: 13 new tests covering flag priority, config fallback, system derivation, config persistence, idempotent --force reinit 54 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,10 @@
|
|||||||
local-identity CLI — entry point.
|
local-identity CLI — entry point.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
init [--force] [--email EMAIL] Derive primary user from Linux identity,
|
init [--force] [--username U] [--fullname N] [--email E]
|
||||||
generate test users, write store.
|
Derive primary user, generate test users,
|
||||||
|
write store. All three identity fields are
|
||||||
|
resolved flag > config > system derivation.
|
||||||
list List all users in the store.
|
list List all users in the store.
|
||||||
show <username> Display a user's YAML record.
|
show <username> Display a user's YAML record.
|
||||||
|
|
||||||
@@ -19,6 +21,20 @@ from .user import UserRecord, make_test_user
|
|||||||
from . import store
|
from . import store
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_init_params(args: argparse.Namespace, config: dict) -> tuple[str, str, str]:
|
||||||
|
"""
|
||||||
|
Resolve (username, fullname, email) for init from three sources in order:
|
||||||
|
1. CLI flags (--username, --fullname, --email)
|
||||||
|
2. Persisted config (~/.local-identity/config.yaml)
|
||||||
|
3. System derivation ($USER / /etc/passwd GECOS) — username + fullname only
|
||||||
|
Email has no system default; missing email falls through to prompt in cmd_init.
|
||||||
|
"""
|
||||||
|
username: str = args.username or config.get("username") or current_username()
|
||||||
|
fullname: str = args.fullname or config.get("fullname") or get_gecos_fullname(username)
|
||||||
|
email: str = args.email or config.get("email") or ""
|
||||||
|
return username, fullname, email
|
||||||
|
|
||||||
|
|
||||||
def cmd_init(args: argparse.Namespace) -> None:
|
def cmd_init(args: argparse.Namespace) -> None:
|
||||||
if store.store_exists() and not args.force:
|
if store.store_exists() and not args.force:
|
||||||
print(
|
print(
|
||||||
@@ -28,12 +44,9 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
username = current_username()
|
|
||||||
fullname = get_gecos_fullname(username)
|
|
||||||
|
|
||||||
# Email resolution: flag > existing config > interactive prompt
|
|
||||||
config = store.read_config() if store.store_exists() else {}
|
config = store.read_config() if store.store_exists() else {}
|
||||||
email: str = args.email or config.get("email", "")
|
username, fullname, email = _resolve_init_params(args, config)
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
try:
|
try:
|
||||||
email = input(f"Email address for {username}: ").strip()
|
email = input(f"Email address for {username}: ").strip()
|
||||||
@@ -44,7 +57,7 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
store.init_dirs()
|
store.init_dirs()
|
||||||
config["email"] = email
|
config.update({"username": username, "fullname": fullname, "email": email})
|
||||||
store.write_config(config)
|
store.write_config(config)
|
||||||
|
|
||||||
primary = UserRecord(username=username, fullname=fullname, email=email)
|
primary = UserRecord(username=username, fullname=fullname, email=email)
|
||||||
@@ -102,9 +115,17 @@ def main() -> None:
|
|||||||
"--force", action="store_true",
|
"--force", action="store_true",
|
||||||
help="Reinitialise even if the store already exists",
|
help="Reinitialise even if the store already exists",
|
||||||
)
|
)
|
||||||
|
p_init.add_argument(
|
||||||
|
"--username",
|
||||||
|
help="Bootstrap username (default: $USER / $LOGNAME)",
|
||||||
|
)
|
||||||
|
p_init.add_argument(
|
||||||
|
"--fullname",
|
||||||
|
help="Full display name (default: /etc/passwd GECOS field)",
|
||||||
|
)
|
||||||
p_init.add_argument(
|
p_init.add_argument(
|
||||||
"--email",
|
"--email",
|
||||||
help="Email address (skips interactive prompt)",
|
help="Email address (default: interactive prompt)",
|
||||||
)
|
)
|
||||||
p_init.set_defaults(func=cmd_init)
|
p_init.set_defaults(func=cmd_init)
|
||||||
|
|
||||||
|
|||||||
151
local-identity/tests/test_cli.py
Normal file
151
local-identity/tests/test_cli.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
Tests for CLI init parameter resolution and command behaviour.
|
||||||
|
|
||||||
|
cmd_init resolution order: flag > config > system derivation.
|
||||||
|
All three identity fields (username, fullname, email) follow this pattern.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from local_identity.cli import _resolve_init_params, cmd_init
|
||||||
|
from local_identity.store import init_dirs, list_users, read_config, read_user
|
||||||
|
|
||||||
|
|
||||||
|
def _args(username=None, fullname=None, email=None, force=False):
|
||||||
|
"""Build a minimal Namespace for _resolve_init_params / cmd_init."""
|
||||||
|
ns = argparse.Namespace()
|
||||||
|
ns.username = username
|
||||||
|
ns.fullname = fullname
|
||||||
|
ns.email = email
|
||||||
|
ns.force = force
|
||||||
|
ns.func = cmd_init
|
||||||
|
return ns
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# _resolve_init_params #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
class TestResolveInitParams:
|
||||||
|
def test_flags_take_priority_over_config_and_system(self):
|
||||||
|
config = {"username": "from_config", "fullname": "Config Name", "email": "config@x.com"}
|
||||||
|
with (
|
||||||
|
patch("local_identity.cli.current_username", return_value="system_user"),
|
||||||
|
patch("local_identity.cli.get_gecos_fullname", return_value="System Name"),
|
||||||
|
):
|
||||||
|
u, f, e = _resolve_init_params(
|
||||||
|
_args(username="tegwick", fullname="Bernd Worsch", email="bernd@example.com"),
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
assert u == "tegwick"
|
||||||
|
assert f == "Bernd Worsch"
|
||||||
|
assert e == "bernd@example.com"
|
||||||
|
|
||||||
|
def test_config_takes_priority_over_system(self):
|
||||||
|
config = {"username": "tegwick", "fullname": "Bernd Worsch", "email": "bernd@example.com"}
|
||||||
|
with (
|
||||||
|
patch("local_identity.cli.current_username", return_value="worsch"),
|
||||||
|
patch("local_identity.cli.get_gecos_fullname", return_value="worsch"),
|
||||||
|
):
|
||||||
|
u, f, e = _resolve_init_params(_args(), config)
|
||||||
|
assert u == "tegwick"
|
||||||
|
assert f == "Bernd Worsch"
|
||||||
|
assert e == "bernd@example.com"
|
||||||
|
|
||||||
|
def test_system_derivation_when_no_flags_or_config(self):
|
||||||
|
config = {}
|
||||||
|
with (
|
||||||
|
patch("local_identity.cli.current_username", return_value="worsch"),
|
||||||
|
patch("local_identity.cli.get_gecos_fullname", return_value="Bernd Worsch"),
|
||||||
|
):
|
||||||
|
u, f, e = _resolve_init_params(_args(), config)
|
||||||
|
assert u == "worsch"
|
||||||
|
assert f == "Bernd Worsch"
|
||||||
|
assert e == "" # no default; caller must prompt
|
||||||
|
|
||||||
|
def test_flag_username_overrides_config_username(self):
|
||||||
|
config = {"username": "wrong", "fullname": "X", "email": "x@x.com"}
|
||||||
|
with patch("local_identity.cli.current_username", return_value="system"):
|
||||||
|
u, _, _ = _resolve_init_params(_args(username="tegwick"), config)
|
||||||
|
assert u == "tegwick"
|
||||||
|
|
||||||
|
def test_flag_fullname_overrides_config_fullname(self):
|
||||||
|
config = {"username": "u", "fullname": "Wrong Name", "email": "x@x.com"}
|
||||||
|
with patch("local_identity.cli.get_gecos_fullname", return_value="Gecos Name"):
|
||||||
|
_, f, _ = _resolve_init_params(_args(fullname="Bernd Worsch"), config)
|
||||||
|
assert f == "Bernd Worsch"
|
||||||
|
|
||||||
|
def test_gecos_used_when_no_fullname_flag_or_config(self):
|
||||||
|
config = {"username": "tegwick", "email": "x@x.com"}
|
||||||
|
with patch("local_identity.cli.get_gecos_fullname", return_value="Bernd Worsch"):
|
||||||
|
_, f, _ = _resolve_init_params(_args(), config)
|
||||||
|
assert f == "Bernd Worsch"
|
||||||
|
|
||||||
|
def test_empty_email_when_absent_everywhere(self):
|
||||||
|
config = {"username": "u", "fullname": "N"}
|
||||||
|
_, _, e = _resolve_init_params(_args(), config)
|
||||||
|
assert e == ""
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# cmd_init integration (uses tmp_store fixture) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
class TestCmdInit:
|
||||||
|
def test_init_with_all_flags(self, tmp_store):
|
||||||
|
with (
|
||||||
|
patch("local_identity.cli.current_username", return_value="worsch"),
|
||||||
|
patch("local_identity.cli.get_gecos_fullname", return_value="worsch"),
|
||||||
|
):
|
||||||
|
cmd_init(_args(username="tegwick", fullname="Bernd Worsch", email="bernd@example.com"))
|
||||||
|
|
||||||
|
primary = read_user("tegwick")
|
||||||
|
assert primary.username == "tegwick"
|
||||||
|
assert primary.fullname == "Bernd Worsch"
|
||||||
|
assert primary.email == "bernd@example.com"
|
||||||
|
|
||||||
|
def test_test_users_derived_from_override_username(self, tmp_store):
|
||||||
|
with patch("local_identity.cli.current_username", return_value="worsch"):
|
||||||
|
cmd_init(_args(username="tegwick", fullname="Bernd Worsch", email="bernd@example.com"))
|
||||||
|
|
||||||
|
users = {u.username for u in list_users()}
|
||||||
|
assert users == {"tegwick", "tegwick1", "tegwick2"}
|
||||||
|
|
||||||
|
def test_test_user_email_uses_override_email(self, tmp_store):
|
||||||
|
with patch("local_identity.cli.current_username", return_value="worsch"):
|
||||||
|
cmd_init(_args(username="tegwick", fullname="Bernd Worsch", email="bernd@example.com"))
|
||||||
|
|
||||||
|
assert read_user("tegwick1").email == "bernd+test1@example.com"
|
||||||
|
assert read_user("tegwick2").email == "bernd+test2@example.com"
|
||||||
|
|
||||||
|
def test_overrides_persisted_to_config(self, tmp_store):
|
||||||
|
with patch("local_identity.cli.current_username", return_value="worsch"):
|
||||||
|
cmd_init(_args(username="tegwick", fullname="Bernd Worsch", email="bernd@example.com"))
|
||||||
|
|
||||||
|
cfg = read_config()
|
||||||
|
assert cfg["username"] == "tegwick"
|
||||||
|
assert cfg["fullname"] == "Bernd Worsch"
|
||||||
|
assert cfg["email"] == "bernd@example.com"
|
||||||
|
|
||||||
|
def test_force_reinit_reads_config_without_flags(self, tmp_store):
|
||||||
|
with patch("local_identity.cli.current_username", return_value="worsch"):
|
||||||
|
cmd_init(_args(username="tegwick", fullname="Bernd Worsch", email="bernd@example.com"))
|
||||||
|
# Second init with --force and no flags — should reuse config
|
||||||
|
cmd_init(_args(force=True))
|
||||||
|
|
||||||
|
primary = read_user("tegwick")
|
||||||
|
assert primary.username == "tegwick"
|
||||||
|
assert primary.fullname == "Bernd Worsch"
|
||||||
|
|
||||||
|
def test_fails_if_store_exists_without_force(self, tmp_store):
|
||||||
|
with (
|
||||||
|
patch("local_identity.cli.current_username", return_value="worsch"),
|
||||||
|
pytest.raises(SystemExit) as exc_info,
|
||||||
|
):
|
||||||
|
cmd_init(_args(email="a@b.com"))
|
||||||
|
cmd_init(_args(email="a@b.com")) # second call should fail
|
||||||
|
|
||||||
|
assert exc_info.value.code == 1
|
||||||
Reference in New Issue
Block a user