"""Tests for WinRmTransport (pypsrp client mocked).""" from __future__ import annotations import sys import types from typing import Any import pytest from ci_orchestrator.transport.errors import TransportCommandError, TransportConnectError from ci_orchestrator.transport.winrm import WinRmTransport class _FakeClient: def __init__(self, *args: Any, **kwargs: Any) -> None: self.args = args self.kwargs = kwargs self.copies: list[tuple[str, str]] = [] self.fetches: list[tuple[str, str]] = [] self.scripts: list[str] = [] self.next_result: tuple[str, str, int] = ("ok", "", 0) self.wsman = types.SimpleNamespace(close=lambda: None) def execute_ps(self, script: str) -> tuple[str, str, int]: self.scripts.append(script) return self.next_result def copy(self, local: str, remote: str) -> None: self.copies.append((local, remote)) def fetch(self, remote: str, local: str) -> None: self.fetches.append((remote, local)) def _install_fake_pypsrp(monkeypatch: pytest.MonkeyPatch, client: _FakeClient) -> None: module = types.ModuleType("pypsrp") client_module = types.ModuleType("pypsrp.client") client_module.Client = lambda *a, **kw: client # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "pypsrp", module) monkeypatch.setitem(sys.modules, "pypsrp.client", client_module) def test_run_returns_result(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() fake.next_result = ("hello\n", "", 0) _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") result = t.run("Write-Output 'hello'") assert result.ok assert result.stdout == "hello\n" assert fake.scripts == ["Write-Output 'hello'"] def test_run_raises_on_failure(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() fake.next_result = ("", "boom", 7) _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") with pytest.raises(TransportCommandError) as exc: t.run("bad") assert exc.value.returncode == 7 def test_run_check_false_does_not_raise(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() fake.next_result = ("", "boom", 7) _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") result = t.run("bad", check=False) assert not result.ok def test_copy_and_fetch(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") t.copy("local.txt", "C:/remote.txt") t.fetch("C:/remote.txt", "downloaded.txt") assert fake.copies == [("local.txt", "C:/remote.txt")] assert fake.fetches == [("C:/remote.txt", "downloaded.txt")] def test_is_ready_handles_connect_failure(monkeypatch: pytest.MonkeyPatch) -> None: # Make the local import inside _connect succeed but Client raise. module = types.ModuleType("pypsrp") client_module = types.ModuleType("pypsrp.client") def _boom(*_a: object, **_kw: object) -> None: raise OSError("connection refused") client_module.Client = _boom # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "pypsrp", module) monkeypatch.setitem(sys.modules, "pypsrp.client", client_module) t = WinRmTransport("10.0.0.1", "user", "pwd") assert t.is_ready() is False # Direct .run should raise. with pytest.raises(TransportConnectError): t.run("x") def test_close_and_context_manager(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() closed: list[bool] = [] fake.wsman = types.SimpleNamespace(close=lambda: closed.append(True)) _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") with t as ctx: assert ctx is t assert closed == [True] # close() on already-closed transport is a no-op. t.close() def test_close_swallows_wsman_exception(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() def _raise() -> None: raise RuntimeError("boom") fake.wsman = types.SimpleNamespace(close=_raise) _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") t.run("x") # connect once t.close() # must not raise def test_run_normalises_list_stderr(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() fake.next_result = ("out", ["a", "b"], 0) # type: ignore[assignment] _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") result = t.run("x") assert result.stderr == "a\nb" def test_run_normalises_non_iterable_stderr(monkeypatch: pytest.MonkeyPatch) -> None: class _Weird: def __str__(self) -> str: return "weird-stderr" fake = _FakeClient() fake.next_result = ("out", _Weird(), 0) # type: ignore[assignment] _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") result = t.run("x") assert "weird-stderr" in result.stderr def test_run_execute_ps_raises_wrapped(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() def _boom(_s: str) -> Any: raise RuntimeError("ps boom") fake.execute_ps = _boom # type: ignore[assignment] _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") with pytest.raises(TransportConnectError) as exc: t.run("x") assert "execute_ps failed" in str(exc.value) def test_copy_wraps_exception(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() def _boom(_l: str, _r: str) -> None: raise OSError("copy down") fake.copy = _boom # type: ignore[assignment] _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") with pytest.raises(TransportConnectError) as exc: t.copy("a", "b") assert "copy failed" in str(exc.value) def test_fetch_wraps_exception(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() def _boom(_r: str, _l: str) -> None: raise OSError("fetch down") fake.fetch = _boom # type: ignore[assignment] _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") with pytest.raises(TransportConnectError) as exc: t.fetch("a", "b") assert "fetch failed" in str(exc.value) def test_is_ready_true_when_run_succeeds(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() _install_fake_pypsrp(monkeypatch, fake) t = WinRmTransport("10.0.0.1", "user", "pwd") assert t.is_ready() is True def _install_fake_streaming(monkeypatch: pytest.MonkeyPatch, *, with_error: bool = False) -> None: """Inject fake pypsrp.powershell + complex_objects for run_streaming tests.""" class _State: NOT_STARTED, RUNNING, COMPLETED, FAILED, STOPPED, STOPPING, DISCONNECTED = range(7) class _Streams: def __init__(self) -> None: self.information: list[Any] = [] self.error: list[Any] = [] class _FakePS: def __init__(self, _pool: Any) -> None: self.state = _State.NOT_STARTED self.output: list[Any] = [] self.streams = _Streams() self._poll = 0 def add_script(self, script: str) -> None: self.script = script def begin_invoke(self) -> None: self.state = _State.RUNNING def poll_invoke(self, *_a: Any, **_k: Any) -> None: self._poll += 1 if self._poll == 1: self.output.append("line1") self.streams.information.append(types.SimpleNamespace(message_data="hostmsg")) elif self._poll == 2: self.output.append("line2") if with_error: self.streams.error.append("boom") self.state = _State.COMPLETED def end_invoke(self) -> None: pass class _FakePool: def __init__(self, wsman: Any) -> None: self.wsman = wsman def open(self) -> None: pass def close(self) -> None: pass co = types.ModuleType("pypsrp.complex_objects") co.PSInvocationState = _State # type: ignore[attr-defined] psmod = types.ModuleType("pypsrp.powershell") psmod.PowerShell = _FakePS # type: ignore[attr-defined] psmod.RunspacePool = _FakePool # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "pypsrp.complex_objects", co) monkeypatch.setitem(sys.modules, "pypsrp.powershell", psmod) monkeypatch.setattr("ci_orchestrator.transport.winrm.time.sleep", lambda _s: None) def test_run_streaming_emits_lines(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() _install_fake_pypsrp(monkeypatch, fake) _install_fake_streaming(monkeypatch) t = WinRmTransport("10.0.0.1", "u", "p") lines: list[str] = [] res = t.run_streaming("Write-Output 1", lines.append) assert lines == ["line1", "hostmsg", "line2"] assert res.ok and res.stdout == "line1\nline2" def test_run_streaming_raises_on_error(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() _install_fake_pypsrp(monkeypatch, fake) _install_fake_streaming(monkeypatch, with_error=True) t = WinRmTransport("10.0.0.1", "u", "p") with pytest.raises(TransportCommandError): t.run_streaming("x", lambda _s: None) def test_run_streaming_check_false(monkeypatch: pytest.MonkeyPatch) -> None: fake = _FakeClient() _install_fake_pypsrp(monkeypatch, fake) _install_fake_streaming(monkeypatch, with_error=True) t = WinRmTransport("10.0.0.1", "u", "p") res = t.run_streaming("x", lambda _s: None, check=False) assert not res.ok and "boom" in res.stderr