d63fca5967
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>
185 lines
6.7 KiB
Python
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"
|