Files
security-alert-center/backend/tests/test_agent_update.py
T

293 lines
9.1 KiB
Python

"""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"