"""Tests for agent update settings, poll, request and ingest lifecycle.""" import time import uuid from datetime import datetime, timedelta, timezone from unittest.mock import patch from app.models import Host from app.services.agent_poll import build_agent_poll_payload from app.services.agent_update import ( AGENT_UPDATE_SUCCESS, AgentUpdateNotAllowedError, process_agent_update_ingest, request_agent_update, ) from app.services.agent_update_settings import upsert_agent_update_settings from app.services.ingest import ingest_event from tests.test_ingest import VALID_EVENT def wait_remote_job(client, host_id, headers, timeout=5.0): deadline = time.monotonic() + timeout while time.monotonic() < deadline: response = client.get( f"/api/v1/hosts/{host_id}/actions/remote-job", headers=headers, ) assert response.status_code == 200 body = response.json() if not body.get("active") and body.get("status") != "running": return body time.sleep(0.05) raise AssertionError("remote job did not finish in time") def test_agent_update_settings_sac_mode(db_session): cfg = upsert_agent_update_settings( db_session, mode="sac", recommended_ssh_version="2.1.0-SAC", fallback_after_minutes=10, ) assert cfg.mode == "sac" assert cfg.recommended_ssh_version == "2.1.0-SAC" assert cfg.fallback_after_minutes == 10 def test_request_agent_update_sets_pending(db_session): upsert_agent_update_settings(db_session, mode="sac", recommended_ssh_version="2.1.0-SAC") host = Host( hostname="linux-1", os_family="linux", product="ssh-monitor", product_version="2.0.0-SAC", agent_instance_id="agent-uuid-1", ) db_session.add(host) db_session.commit() request_agent_update(db_session, host) assert host.pending_agent_update is True assert host.pending_update_target_version == "2.1.0-SAC" assert host.agent_update_state == "requested" def test_request_agent_update_blocked_in_gpo_mode(db_session): upsert_agent_update_settings(db_session, mode="gpo") host = Host( hostname="linux-2", os_family="linux", product="ssh-monitor", agent_instance_id="agent-uuid-2", ) db_session.add(host) db_session.commit() try: request_agent_update(db_session, host) raise AssertionError("expected AgentUpdateNotAllowedError") except AgentUpdateNotAllowedError: pass def test_poll_payload_includes_update_and_config(db_session): upsert_agent_update_settings(db_session, mode="sac") host = Host( hostname="win-1", os_family="windows", product="rdp-login-monitor", agent_instance_id="agent-uuid-3", pending_agent_update=True, pending_update_target_version="2.1.0-SAC", agent_config={"ServerDisplayName": "Test PC"}, agent_config_revision=3, ) db_session.add(host) db_session.commit() payload = build_agent_poll_payload(db_session, host) assert payload["config_revision"] == 3 assert payload["config"]["settings"]["ServerDisplayName"] == "Test PC" assert payload["update"]["requested"] is True assert payload["update"]["target_version"] == "2.1.0-SAC" assert payload["update"]["source"] == "sac" def test_ingest_agent_update_success_clears_pending(db_session): host = Host( hostname="linux-3", os_family="linux", product="ssh-monitor", agent_instance_id="agent-uuid-4", pending_agent_update=True, pending_update_target_version="2.1.0-SAC", ) db_session.add(host) db_session.commit() process_agent_update_ingest( db_session, host, AGENT_UPDATE_SUCCESS, {"product_version": "2.1.0-SAC"}, ) assert host.pending_agent_update is False assert host.product_version == "2.1.0-SAC" assert host.agent_update_state == "success" def test_api_request_agent_update(jwt_headers, client, db_session): upsert_agent_update_settings(db_session, mode="sac", recommended_ssh_version="2.1.0-SAC") host = Host( hostname="api-linux", os_family="linux", product="ssh-monitor", agent_instance_id="agent-api-1", ) db_session.add(host) db_session.commit() response = client.post(f"/api/v1/hosts/{host.id}/actions/request-agent-update", headers=jwt_headers) assert response.status_code == 200 body = response.json() assert body["pending_agent_update"] is True assert body["target_version"] == "2.1.0-SAC" def test_api_agent_poll_extended(auth_headers, client, db_session): upsert_agent_update_settings(db_session, mode="sac") host = Host( hostname="poll-host", os_family="linux", product="ssh-monitor", agent_instance_id="poll-agent-id", pending_agent_update=True, pending_update_target_version="2.1.0-SAC", ) db_session.add(host) db_session.commit() response = client.get( "/api/v1/agent/commands", params={"agent_instance_id": "poll-agent-id"}, headers=auth_headers, ) assert response.status_code == 200 body = response.json() assert body["update"]["requested"] is True assert body["commands"] == [] def test_api_patch_agent_config(jwt_headers, client, db_session): host = Host( hostname="cfg-host", os_family="windows", product="rdp-login-monitor", ) db_session.add(host) db_session.commit() response = client.patch( f"/api/v1/hosts/{host.id}/agent-config", json={"settings": {"ServerDisplayName": "UNMS Kalina", "GetInventory": True}}, headers=jwt_headers, ) assert response.status_code == 200 body = response.json() assert body["config_revision"] == 1 assert body["settings"]["ServerDisplayName"] == "UNMS Kalina" def test_api_agent_update_fallback_ssh(jwt_headers, client, db_session, monkeypatch): monkeypatch.setenv("SAC_LINUX_ADMIN_USER", "root") monkeypatch.setenv("SAC_LINUX_ADMIN_PASSWORD", "pw") from app.config import get_settings get_settings.cache_clear() host = Host( hostname="fb-linux", os_family="linux", product="ssh-monitor", ipv4="10.10.36.9", ) db_session.add(host) db_session.commit() with patch("app.services.agent_update.run_ssh_monitor_update") as mock_update: from app.services.ssh_connect import SshCommandResult mock_update.return_value = SshCommandResult( ok=True, message="updated", target="fb-linux", agent_version="2.1.0-SAC", ) response = client.post( f"/api/v1/hosts/{host.id}/actions/agent-update-fallback", headers=jwt_headers, ) assert response.status_code == 202 assert response.json()["status"] == "running" job = wait_remote_job(client, host.id, jwt_headers) assert job["ok"] is True assert job["product_version"] == "2.1.0-SAC" def test_clear_stale_running_remote_actions(db_session): from app.services.host_remote_actions import clear_stale_running_remote_actions host = Host( hostname="stale-win", os_family="windows", product="rdp-login-monitor", agent_update_state="running", remote_action={"status": "running", "message": "Подключение…"}, ) db_session.add(host) db_session.commit() cleared = clear_stale_running_remote_actions(db_session, reason="test reset") assert cleared == ["stale-win"] db_session.refresh(host) assert host.agent_update_state == "failed" assert host.remote_action["status"] == "failed" assert host.remote_action["ok"] is False def test_get_remote_action_status_ignores_stale_running_payload(db_session): from app.services.host_remote_actions import get_remote_action_status host = Host( hostname="done-linux", os_family="linux", product="ssh-monitor", agent_update_state="success", remote_action={ "status": "running", "message": "Выполняется обновление… (лог с хоста)", "output": "=== Script update completed successfully ===", "ok": True, "finished_at": "2026-07-08T02:21:11+00:00", }, ) db_session.add(host) db_session.commit() status = get_remote_action_status(host) assert status["active"] is False assert status["agent_update_state"] == "success" def test_cancel_host_remote_job_api(jwt_headers, client, db_session): host = Host( hostname="cancel-me", os_family="windows", product="rdp-login-monitor", agent_update_state="running", remote_action={"status": "running", "title": "Обновление через WinRM"}, ) db_session.add(host) db_session.commit() response = client.post( f"/api/v1/hosts/{host.id}/actions/remote-job/cancel", headers=jwt_headers, ) assert response.status_code == 200 body = response.json() assert body["active"] is False assert body["status"] == "failed"