""" 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 ( _oidc_bootstrap_payload, _resolve_init_params, cmd_bootstrap_oidc, 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 def _oidc_args( client_id="local-dev", redirect_uri="http://127.0.0.1:3000/callback", port=8443, scheme="https", scope="openid profile email", output="env", ): ns = argparse.Namespace() ns.client_id = client_id ns.redirect_uri = redirect_uri ns.port = port ns.scheme = scheme ns.scope = scope ns.output = output ns.func = cmd_bootstrap_oidc 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 # ------------------------------------------------------------------ # # cmd_bootstrap_oidc # # ------------------------------------------------------------------ # class TestCmdBootstrapOidc: def test_payload_uses_local_issuer_and_client_settings(self): payload = _oidc_bootstrap_payload( _oidc_args( client_id="example-app", redirect_uri="http://localhost:8080/oidc/callback", port=9443, ) ) assert payload == { "issuer": "https://127.0.0.1:9443", "discovery_url": "https://127.0.0.1:9443/.well-known/openid-configuration", "client_id": "example-app", "redirect_uri": "http://localhost:8080/oidc/callback", "scope": "openid profile email", "token_endpoint_auth_method": "none", } def test_rejects_non_loopback_redirect_uri(self): with pytest.raises(ValueError, match="loopback"): _oidc_bootstrap_payload( _oidc_args(redirect_uri="https://example.com/callback") ) def test_persists_client_bootstrap_config(self, tmp_store, capsys): with patch("local_identity.cli.current_username", return_value="worsch"): cmd_init(_args(username="alice", fullname="Alice Smith", email="alice@example.com")) cmd_bootstrap_oidc( _oidc_args( client_id="demo", redirect_uri="http://127.0.0.1:5173/auth/callback", port=9443, ) ) cfg = read_config() assert cfg["last_oidc_bootstrap"] == "demo" assert cfg["oidc_clients"]["demo"]["issuer"] == "https://127.0.0.1:9443" assert cfg["oidc_clients"]["demo"]["redirect_uri"] == "http://127.0.0.1:5173/auth/callback" out = capsys.readouterr().out assert "OIDC_ISSUER=https://127.0.0.1:9443" in out assert "OIDC_TOKEN_ENDPOINT_AUTH_METHOD=none" in out def test_json_output(self, tmp_store, capsys): with patch("local_identity.cli.current_username", return_value="worsch"): cmd_init(_args(username="alice", fullname="Alice Smith", email="alice@example.com")) cmd_bootstrap_oidc(_oidc_args(client_id="json-app", output="json")) data = capsys.readouterr().out assert '"client_id": "json-app"' in data assert '"token_endpoint_auth_method": "none"' in data