Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

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