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>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user