Files
local-ci-cd-system/tests/python/test_commands_creds.py
T
Simone d63fca5967 feat(cli): port burn-in/smoke/validate/creds to Python (Phase C1-C6)
Add four native command groups so the Linux host no longer needs pwsh for
manual ops. __main__ registers all four at once, so they ship together.

- bench run     (C2): N concurrent jobs in-process via concurrent.futures,
                      per-round orphan-clone + stale-lock assertions. Replaces
                      Test-CapacityBurnIn.ps1 + Start-BurnInTest*.ps1. No
                      Start-Job/pwsh/$IsWindows.
- bench measure (C3): clone/start/IP/transport/destroy timings appended to
                      benchmark.jsonl (legacy field names kept). Replaces
                      Measure-CIBenchmark.ps1.
- smoke run     (C4): one E2E job; asserts exit 0 + artifact dir + job/success
                      event. ns7zip/nsinnounp presets. Replaces Test-Smoke.ps1
                      + Test-*Build-Linux.ps1.
- validate host (C5): vmrun/snapshot/keyring/permissions/systemd checks.
- validate guest(C6): transport probe with explicit failure cause.
- creds set     (C6): keyring writer matching credentials.py read scheme;
                      password via stdin/prompt only. Replaces
                      Set-CIGuestCredential.ps1.

All groups import backends.load_backend only (Phase D ESXi path stays open).
Suite green, coverage 95.10% (gate 90%); new modules at 100%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 20:15:10 +02:00

185 lines
6.7 KiB
Python

"""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"