feat(template): add --store-credential to prepare-win + full docstring

- --store-credential: after snapshot, writes build-user credentials into
  the system keyring using the two-entry scheme (service:meta + service)
  that KeyringCredentialStore.get() reads. Works on Windows (Credential
  Manager) and Linux (SecretService / file backend).
- --credential-target: keyring target name, default BuildVMGuest (matches
  config.guest_cred_target).
- Rewrote module docstring: full prose documentation of both `template
  backup` and `template prepare-win` including all 12 provisioning steps.
- 2 new tests covering --store-credential with custom and default targets.
- mypy --strict clean; coverage 90.03%.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 01:30:57 +02:00
parent 9e84d89823
commit 84746d2232
2 changed files with 162 additions and 9 deletions
+86
View File
@@ -915,3 +915,89 @@ def test_prepare_win_vm_shutdown_timeout(
)
assert result.exit_code != 0
assert "power off" in result.output
def test_prepare_win_store_credential(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""--store-credential calls keyring.set_password with the two-entry scheme."""
vmx = _setup_prepare_env(tmp_path, with_static_ip=False)
monkeypatch.chdir(tmp_path)
backend = _make_backend([True, False])
transport = _make_transport()
_patch_prepare(monkeypatch, backend, transport)
stored: list[tuple[str, str, str]] = []
fake_keyring = types.SimpleNamespace(
set_password=lambda svc, user, pw: stored.append((svc, user, pw))
)
monkeypatch.setattr(tmpl_mod, "keyring", fake_keyring, raising=False)
# Patch the import inside the function so it returns our fake module.
import builtins
real_import = builtins.__import__
def _fake_import(name: str, *args: object, **kwargs: object) -> object:
if name == "keyring":
return fake_keyring
return real_import(name, *args, **kwargs) # type: ignore[arg-type]
monkeypatch.setattr(builtins, "__import__", _fake_import)
result = CliRunner().invoke(
cli,
[
"template", "prepare-win",
"--vmx", str(vmx),
"--ip", "192.168.1.100",
"--admin-password", "adminpass",
"--build-password", "buildpass",
"--store-credential",
"--credential-target", "TestTarget",
],
)
assert result.exit_code == 0, result.output
assert any(s[0] == "TestTarget:meta" for s in stored), "username meta-entry not stored"
assert any(s[0] == "TestTarget" for s in stored), "password entry not stored"
assert "Credentials stored" in result.output
def test_prepare_win_store_credential_default_target(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Default credential target is BuildVMGuest."""
vmx = _setup_prepare_env(tmp_path, with_static_ip=False)
monkeypatch.chdir(tmp_path)
backend = _make_backend([True, False])
transport = _make_transport()
_patch_prepare(monkeypatch, backend, transport)
stored: list[tuple[str, str, str]] = []
import builtins
real_import = builtins.__import__
def _fake_import(name: str, *args: object, **kwargs: object) -> object:
if name == "keyring":
return types.SimpleNamespace(
set_password=lambda svc, user, pw: stored.append((svc, user, pw))
)
return real_import(name, *args, **kwargs) # type: ignore[arg-type]
monkeypatch.setattr(builtins, "__import__", _fake_import)
result = CliRunner().invoke(
cli,
[
"template", "prepare-win",
"--vmx", str(vmx),
"--ip", "192.168.1.100",
"--admin-password", "adminpass",
"--build-password", "buildpass",
"--store-credential",
],
)
assert result.exit_code == 0, result.output
assert any(s[0] == "BuildVMGuest:meta" for s in stored)
assert any(s[0] == "BuildVMGuest" for s in stored)