"""Tests for ``creds set`` (C6). CLI registration smoke tests (``--help``) are kept; behavioural tests mock the ``keyring`` module and include a round-trip via ``KeyringCredentialStore``. """ from __future__ import annotations import sys import types from typing import Any import pytest from click.testing import CliRunner from ci_orchestrator.__main__ import cli from ci_orchestrator.credentials import KeyringCredentialStore # ── --help smoke tests (kept) ─────────────────────────────────────────────── def test_creds_help_lists_set() -> None: result = CliRunner().invoke(cli, ["creds", "--help"]) assert result.exit_code == 0, result.output assert "set" in result.output def test_creds_set_help() -> None: result = CliRunner().invoke(cli, ["creds", "set", "--help"]) assert result.exit_code == 0, result.output assert "--target" in result.output assert "--user" in result.output # ── fake keyring ──────────────────────────────────────────────────────────── def _install_fake_keyring(monkeypatch: pytest.MonkeyPatch) -> dict[tuple[str, str], str]: """Install a fake ``keyring`` module with a two-key password store. Returns the backing dict so tests can inspect what was written. The fake implements both ``set_password`` (writer) and ``get_password`` / ``get_credential`` (reader), so round-trips work end to end. """ store: dict[tuple[str, str], str] = {} fake = types.ModuleType("keyring") def set_password(service: str, key: str, password: str) -> None: store[(service, key)] = password def get_password(service: str, key: str) -> str | None: return store.get((service, key)) def get_credential(service: str, _username: str | None) -> Any: # File backends are a no-op here (mirrors PlaintextKeyring); the reader # falls through to the two-entry scheme. return None fake.set_password = set_password # type: ignore[attr-defined] fake.get_password = get_password # type: ignore[attr-defined] fake.get_credential = get_credential # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "keyring", fake) return store # ── write scheme ──────────────────────────────────────────────────────────── def test_creds_set_writes_two_entries(monkeypatch: pytest.MonkeyPatch) -> None: store = _install_fake_keyring(monkeypatch) result = CliRunner().invoke( cli, ["creds", "set", "--target", "BuildVMGuest", "--user", "ci_build", "--password-stdin"], input="s3cret\n", ) assert result.exit_code == 0, result.output # username under "{target}:meta" assert store[("BuildVMGuest:meta", "username")] == "ci_build" # password under target keyed by username assert store[("BuildVMGuest", "ci_build")] == "s3cret" def test_creds_set_round_trips_via_store(monkeypatch: pytest.MonkeyPatch) -> None: """Round-trip: write with `creds set`, read back with the reader.""" _install_fake_keyring(monkeypatch) result = CliRunner().invoke( cli, ["creds", "set", "--target", "BuildVMGuest", "--user", "WINBUILD\\ci", "--password-stdin"], input="p@ss word!\n", ) assert result.exit_code == 0, result.output cred = KeyringCredentialStore().get("BuildVMGuest") assert cred.username == "WINBUILD\\ci" assert cred.password == "p@ss word!" def test_creds_set_default_target(monkeypatch: pytest.MonkeyPatch) -> None: store = _install_fake_keyring(monkeypatch) result = CliRunner().invoke( cli, ["creds", "set", "--user", "ci_build", "--password-stdin"], input="abc\n", ) assert result.exit_code == 0, result.output assert store[("BuildVMGuest:meta", "username")] == "ci_build" # ── password sourcing ─────────────────────────────────────────────────────── def test_creds_set_prompts_when_no_stdin_flag( monkeypatch: pytest.MonkeyPatch, ) -> None: store = _install_fake_keyring(monkeypatch) # click.prompt with confirmation_prompt reads the value twice. result = CliRunner().invoke( cli, ["creds", "set", "--target", "T", "--user", "u"], input="hunter2\nhunter2\n", ) assert result.exit_code == 0, result.output assert store[("T", "u")] == "hunter2" def test_creds_set_empty_password_rejected( monkeypatch: pytest.MonkeyPatch, ) -> None: _install_fake_keyring(monkeypatch) result = CliRunner().invoke( cli, ["creds", "set", "--target", "T", "--user", "u", "--password-stdin"], input="\n", ) assert result.exit_code != 0 assert "empty" in result.output.lower() def test_creds_set_strips_trailing_newline( monkeypatch: pytest.MonkeyPatch, ) -> None: store = _install_fake_keyring(monkeypatch) result = CliRunner().invoke( cli, ["creds", "set", "--target", "T", "--user", "u", "--password-stdin"], input="secret\r\n", ) assert result.exit_code == 0, result.output assert store[("T", "u")] == "secret" def test_creds_set_never_accepts_password_option() -> None: """The CLI must NOT expose a --password option.""" result = CliRunner().invoke(cli, ["creds", "set", "--help"]) assert "--password " not in result.output assert "--password=" not in result.output def test_creds_set_keyring_missing(monkeypatch: pytest.MonkeyPatch) -> None: """When keyring import fails, surface a clean ClickException.""" import ci_orchestrator.commands.creds as creds_module def _raise(_t: str, _u: str, _p: str) -> None: raise RuntimeError("keyring is not installed; run `pip install keyring`.") monkeypatch.setattr(creds_module, "_store_credential", _raise) result = CliRunner().invoke( cli, ["creds", "set", "--target", "T", "--user", "u", "--password-stdin"], input="x\n", ) assert result.exit_code != 0 assert "keyring is not installed" in result.output def test_store_credential_invokes_keyring(monkeypatch: pytest.MonkeyPatch) -> None: """Unit-level: _store_credential writes both entries to the module.""" import ci_orchestrator.commands.creds as creds_module store = _install_fake_keyring(monkeypatch) creds_module._store_credential("Tgt", "user1", "pw1") assert store[("Tgt:meta", "username")] == "user1" assert store[("Tgt", "user1")] == "pw1"