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
169 lines
5.7 KiB
Python
169 lines
5.7 KiB
Python
# SPDX-License-Identifier: MIT
|
|
"""Tests for AgentTransport — wraps AgentClient through Transport ABC."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from winpodx.core.agent import (
|
|
AgentAuthError,
|
|
AgentError,
|
|
AgentTimeoutError,
|
|
AgentUnavailableError,
|
|
)
|
|
from winpodx.core.agent import (
|
|
ExecResult as AgentExecResult,
|
|
)
|
|
from winpodx.core.config import Config
|
|
from winpodx.core.transport.agent import AgentTransport
|
|
from winpodx.core.transport.base import (
|
|
ExecResult,
|
|
HealthStatus,
|
|
TransportAuthError,
|
|
TransportError,
|
|
TransportTimeoutError,
|
|
TransportUnavailable,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def cfg() -> Config:
|
|
return Config()
|
|
|
|
|
|
@pytest.fixture
|
|
def transport(cfg: Config) -> AgentTransport:
|
|
return AgentTransport(cfg)
|
|
|
|
|
|
class TestName:
|
|
def test_name_is_agent(self):
|
|
assert AgentTransport.name == "agent"
|
|
|
|
|
|
class TestHealth:
|
|
"""health() never raises on transient state — see Transport ABC rule."""
|
|
|
|
def test_returns_available_when_client_responds(self, transport):
|
|
with (
|
|
patch.object(
|
|
type(transport._client),
|
|
"health",
|
|
return_value={"version": "0.2.2-rev1", "ok": True},
|
|
),
|
|
patch.object(type(transport._client), "auth_ready", return_value=(True, "")),
|
|
):
|
|
status = transport.health()
|
|
assert isinstance(status, HealthStatus)
|
|
assert status.available is True
|
|
assert status.version == "0.2.2-rev1"
|
|
|
|
def test_returns_unavailable_when_token_missing(self, transport):
|
|
with (
|
|
patch.object(
|
|
type(transport._client),
|
|
"health",
|
|
return_value={"version": "0.2.2-rev1", "ok": True},
|
|
),
|
|
patch.object(
|
|
type(transport._client),
|
|
"auth_ready",
|
|
return_value=(False, "agent token file missing"),
|
|
),
|
|
):
|
|
status = transport.health()
|
|
assert status.available is False
|
|
assert "token" in (status.detail or "")
|
|
|
|
def test_returns_unavailable_on_unavailable_error(self, transport):
|
|
with patch.object(
|
|
type(transport._client),
|
|
"health",
|
|
side_effect=AgentUnavailableError("connection refused"),
|
|
):
|
|
status = transport.health()
|
|
assert status.available is False
|
|
assert "connection refused" in (status.detail or "")
|
|
|
|
def test_returns_unavailable_on_unexpected_exception(self, transport):
|
|
# Defensive: even unexpected exceptions must not propagate.
|
|
with patch.object(type(transport._client), "health", side_effect=ValueError("boom")):
|
|
status = transport.health()
|
|
assert status.available is False
|
|
assert "boom" in (status.detail or "")
|
|
|
|
def test_handles_non_dict_payload_gracefully(self, transport):
|
|
# AgentClient.health() returns a dict on success; if it ever
|
|
# returns something else, AgentTransport must not crash.
|
|
with (
|
|
patch.object(type(transport._client), "health", return_value=[]),
|
|
patch.object(type(transport._client), "auth_ready", return_value=(True, "")),
|
|
):
|
|
status = transport.health()
|
|
assert status.available is True
|
|
assert status.version is None
|
|
|
|
|
|
class TestExec:
|
|
def test_happy_path_maps_agent_result_to_exec_result(self, transport):
|
|
agent_result = AgentExecResult(rc=0, stdout="hi\n", stderr="")
|
|
with patch.object(type(transport._client), "exec", return_value=agent_result):
|
|
result = transport.exec("Write-Output hi")
|
|
assert isinstance(result, ExecResult)
|
|
assert result.rc == 0
|
|
assert result.stdout == "hi\n"
|
|
assert result.stderr == ""
|
|
assert result.ok
|
|
|
|
def test_auth_error_maps_to_transport_auth_error(self, transport):
|
|
with patch.object(
|
|
type(transport._client),
|
|
"exec",
|
|
side_effect=AgentAuthError("/exec auth failed (401)"),
|
|
):
|
|
with pytest.raises(TransportAuthError):
|
|
transport.exec("anything")
|
|
|
|
def test_timeout_error_maps_to_transport_timeout(self, transport):
|
|
with patch.object(
|
|
type(transport._client),
|
|
"exec",
|
|
side_effect=AgentTimeoutError("60s timeout"),
|
|
):
|
|
with pytest.raises(TransportTimeoutError):
|
|
transport.exec("Start-Sleep 120")
|
|
|
|
def test_unavailable_error_maps_to_transport_unavailable(self, transport):
|
|
with patch.object(
|
|
type(transport._client),
|
|
"exec",
|
|
side_effect=AgentUnavailableError("connection refused"),
|
|
):
|
|
with pytest.raises(TransportUnavailable):
|
|
transport.exec("anything")
|
|
|
|
def test_generic_agent_error_maps_to_transport_error(self, transport):
|
|
with patch.object(type(transport._client), "exec", side_effect=AgentError("malformed")):
|
|
with pytest.raises(TransportError):
|
|
transport.exec("anything")
|
|
|
|
def test_passes_timeout_through_as_float(self, transport):
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_exec(self_inner, script, *, timeout):
|
|
captured["script"] = script
|
|
captured["timeout"] = timeout
|
|
return AgentExecResult(rc=0, stdout="", stderr="")
|
|
|
|
with patch.object(type(transport._client), "exec", fake_exec):
|
|
transport.exec("ok", timeout=42)
|
|
assert captured["timeout"] == 42.0
|
|
|
|
|
|
class TestStream:
|
|
def test_stream_raises_transport_unavailable(self, transport):
|
|
with pytest.raises(TransportUnavailable):
|
|
transport.stream("Get-Process", lambda _line: None)
|