3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
175 lines
6.2 KiB
Python
175 lines
6.2 KiB
Python
# SPDX-License-Identifier: MIT
|
|
"""Tests for the guest agent token resync self-heal (#615)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from winpodx.core import agent_resync
|
|
from winpodx.core.agent import AgentAuthError, ExecResult
|
|
from winpodx.core.windows_exec import WindowsExecError, WindowsExecResult
|
|
|
|
_TOKEN = "ab" * 32 # 64 hex chars, like secrets.token_hex(32)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_token_side_effects(monkeypatch):
|
|
"""Stub token generation + OEM staging so tests don't touch disk."""
|
|
monkeypatch.setattr(agent_resync, "ensure_agent_token", lambda: _TOKEN)
|
|
monkeypatch.setattr(agent_resync, "stage_token_to_oem", lambda _p: None)
|
|
# Skip the real verify-loop sleeps.
|
|
monkeypatch.setattr(agent_resync.time, "sleep", lambda _s: None)
|
|
# The lazy `from winpodx.core.pod.compose import _find_oem_dir` is wrapped in
|
|
# try/except in resync_token; make it cheap and harmless.
|
|
import winpodx.core.pod.compose as compose
|
|
|
|
monkeypatch.setattr(compose, "_find_oem_dir", lambda: "/tmp/winpodx-oem", raising=False)
|
|
|
|
|
|
def _patch_freerdp(monkeypatch, *, rc=0, stdout="resynced+respawn", raises=None):
|
|
"""Patch the FreeRDP push (run_in_windows); return a payload-capture list."""
|
|
captured: list[str] = []
|
|
import winpodx.core.windows_exec as we
|
|
|
|
def fake_run_in_windows(cfg, payload, *, description="", timeout=60):
|
|
captured.append(payload)
|
|
if raises is not None:
|
|
raise raises
|
|
return WindowsExecResult(rc=rc, stdout=stdout, stderr="")
|
|
|
|
monkeypatch.setattr(we, "run_in_windows", fake_run_in_windows)
|
|
return captured
|
|
|
|
|
|
def _patch_agent_exec(monkeypatch, behaviors):
|
|
"""Patch AgentClient so each .exec() pops the next behavior (result or exc)."""
|
|
import winpodx.core.agent as agent
|
|
|
|
seq = list(behaviors)
|
|
|
|
class FakeClient:
|
|
def __init__(self, _cfg):
|
|
pass
|
|
|
|
def exec(self, _script, *, timeout=30.0):
|
|
item = seq.pop(0) if seq else ExecResult(rc=0, stdout="ok", stderr="")
|
|
if isinstance(item, Exception):
|
|
raise item
|
|
return item
|
|
|
|
monkeypatch.setattr(agent, "AgentClient", FakeClient)
|
|
|
|
|
|
def test_resync_success(monkeypatch):
|
|
_patch_freerdp(monkeypatch, stdout="resynced+respawn")
|
|
_patch_agent_exec(monkeypatch, [ExecResult(rc=0, stdout="ok", stderr="")])
|
|
|
|
ok, detail = agent_resync.resync_token(SimpleNamespace(), verify_timeout=6.0)
|
|
assert ok is True
|
|
assert "OK" in detail or "ok" in detail.lower()
|
|
|
|
|
|
def test_resync_payload_carries_token_and_respawn(monkeypatch):
|
|
captured = _patch_freerdp(monkeypatch)
|
|
_patch_agent_exec(monkeypatch, [ExecResult(rc=0, stdout="ok", stderr="")])
|
|
|
|
agent_resync.resync_token(SimpleNamespace(), verify_timeout=6.0)
|
|
|
|
assert len(captured) == 1
|
|
payload = captured[0]
|
|
assert _TOKEN in payload
|
|
assert "C:\\OEM\\agent_token.txt" in payload
|
|
assert "agent-respawn.ps1" in payload
|
|
assert "-NoNewline" in payload # byte-for-byte match with the host write
|
|
|
|
|
|
def test_resync_norespawn_reports_manual_step(monkeypatch):
|
|
_patch_freerdp(monkeypatch, stdout="resynced+norespawn")
|
|
# Even if the agent would answer, norespawn short-circuits before verify.
|
|
_patch_agent_exec(monkeypatch, [ExecResult(rc=0, stdout="ok", stderr="")])
|
|
|
|
ok, detail = agent_resync.resync_token(SimpleNamespace(), verify_timeout=6.0)
|
|
assert ok is False
|
|
assert "apply-fixes" in detail or "log" in detail.lower()
|
|
|
|
|
|
def test_resync_freerdp_push_failure(monkeypatch):
|
|
_patch_freerdp(monkeypatch, raises=WindowsExecError("no result file"))
|
|
ok, detail = agent_resync.resync_token(SimpleNamespace(), verify_timeout=6.0)
|
|
assert ok is False
|
|
assert "FreeRDP push failed" in detail
|
|
|
|
|
|
def test_resync_guest_rewrite_nonzero_rc(monkeypatch):
|
|
_patch_freerdp(monkeypatch, rc=1, stdout="")
|
|
ok, detail = agent_resync.resync_token(SimpleNamespace(), verify_timeout=6.0)
|
|
assert ok is False
|
|
assert "rewrite failed" in detail
|
|
|
|
|
|
def test_resync_verify_never_authenticates(monkeypatch):
|
|
_patch_freerdp(monkeypatch, stdout="resynced+respawn")
|
|
# Push OK, respawn launched, but the agent keeps rejecting → not healed.
|
|
_patch_agent_exec(monkeypatch, [AgentAuthError("/exec returned 401")] * 10)
|
|
|
|
ok, detail = agent_resync.resync_token(SimpleNamespace(), verify_timeout=4.0)
|
|
assert ok is False
|
|
assert "not authenticated" in detail
|
|
|
|
|
|
def test_doctor_guest_exec_auto_resyncs_on_401(monkeypatch):
|
|
"""probe_guest_exec heals a 401 once, then reports the round-trip OK."""
|
|
import winpodx.core.pod as pod
|
|
from winpodx.core import checks
|
|
from winpodx.core.pod import PodState
|
|
|
|
monkeypatch.setattr(pod, "pod_status", lambda _cfg: SimpleNamespace(state=PodState.RUNNING))
|
|
# First exec → 401; after resync, a fresh client → ok.
|
|
import winpodx.core.agent as agent
|
|
|
|
calls = {"n": 0}
|
|
|
|
class FakeClient:
|
|
def __init__(self, _cfg):
|
|
pass
|
|
|
|
def exec(self, _script, *, timeout=30.0):
|
|
calls["n"] += 1
|
|
if calls["n"] == 1:
|
|
raise AgentAuthError("/exec returned 401")
|
|
return ExecResult(rc=0, stdout="ok", stderr="")
|
|
|
|
monkeypatch.setattr(agent, "AgentClient", FakeClient)
|
|
monkeypatch.setattr(agent_resync, "resync_token", lambda _cfg: (True, "token resynced"))
|
|
|
|
probe = checks.probe_guest_exec(SimpleNamespace())
|
|
assert probe.status == "ok"
|
|
assert "drifted" in probe.detail or "resync" in probe.detail
|
|
|
|
|
|
def test_doctor_guest_exec_reports_failed_resync(monkeypatch):
|
|
import winpodx.core.pod as pod
|
|
from winpodx.core import checks
|
|
from winpodx.core.pod import PodState
|
|
|
|
monkeypatch.setattr(pod, "pod_status", lambda _cfg: SimpleNamespace(state=PodState.RUNNING))
|
|
import winpodx.core.agent as agent
|
|
|
|
class FakeClient:
|
|
def __init__(self, _cfg):
|
|
pass
|
|
|
|
def exec(self, _script, *, timeout=30.0):
|
|
raise AgentAuthError("/exec returned 401")
|
|
|
|
monkeypatch.setattr(agent, "AgentClient", FakeClient)
|
|
monkeypatch.setattr(
|
|
agent_resync, "resync_token", lambda _cfg: (False, "FreeRDP push failed: x")
|
|
)
|
|
|
|
probe = checks.probe_guest_exec(SimpleNamespace())
|
|
assert probe.status == "fail"
|
|
assert "auto-resync failed" in probe.detail
|