238 lines
9.6 KiB
Python
238 lines
9.6 KiB
Python
"""Focused tests for authenticated Agent queue transport."""
|
|
|
|
import unittest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.agent_commands.dependencies import get_agent_command_service
|
|
from app.agent_commands.exceptions import (
|
|
AgentCommandConflict,
|
|
AgentResultValidationError,
|
|
)
|
|
from app.agent_commands.models import AgentCommandResultRequest
|
|
from app.agent_commands.repository import CLAIM_COMMANDS_SQL
|
|
from app.agent_commands.router import router
|
|
from app.agent_commands.service import AgentCommandService
|
|
from app.config import settings
|
|
|
|
|
|
NOW = datetime(2026, 8, 24, tzinfo=timezone.utc)
|
|
|
|
|
|
def agent_source_root() -> Path | None:
|
|
container_root = Path("/agents/windows")
|
|
if container_root.is_dir():
|
|
return container_root
|
|
repository_root = Path(__file__).parents[2] / "agents" / "windows"
|
|
return repository_root if repository_root.is_dir() else None
|
|
|
|
|
|
class FakeRepository:
|
|
def __init__(self, row=None):
|
|
self.row = row or {
|
|
"id": "command-1", "agent_id": "test-agent",
|
|
"command_type": "miner_control", "command_status": "claimed",
|
|
"request_payload": {"action": "suspend"},
|
|
}
|
|
self.finished = []
|
|
|
|
def claim_pending(self, agent_id, limit=5):
|
|
return [{
|
|
"command_id": "command-1", "command_type": "miner_control",
|
|
"request_payload": {"action": "suspend"}, "created_at": NOW,
|
|
}]
|
|
|
|
def get_command(self, command_id):
|
|
return self.row if command_id == self.row.get("id") else None
|
|
|
|
def finish_command(self, *args):
|
|
self.finished.append(args)
|
|
|
|
|
|
def typed_result(**overrides):
|
|
result = {
|
|
"schema_version": 1,
|
|
"command_accepted": True,
|
|
"verification_attempted": False,
|
|
"verification_status": "unconfirmed",
|
|
"observed_service_state": None,
|
|
"observed_performance_mode": None,
|
|
"failure_code": None,
|
|
}
|
|
result.update(overrides)
|
|
return result
|
|
|
|
|
|
class AgentCommandServiceTests(unittest.TestCase):
|
|
def test_claim_query_uses_skip_locked_and_deterministic_order(self):
|
|
self.assertIn("FOR UPDATE SKIP LOCKED", CLAIM_COMMANDS_SQL)
|
|
self.assertIn("ORDER BY created_at, id", CLAIM_COMMANDS_SQL)
|
|
self.assertIn("queue.command_status = 'pending'", CLAIM_COMMANDS_SQL)
|
|
|
|
def test_typed_result_is_strict_and_sanitized(self):
|
|
repository = FakeRepository()
|
|
request = AgentCommandResultRequest(
|
|
command_id="command-1", status="completed",
|
|
result_payload=typed_result(), error_message="raw 192.0.2.10 vendor exception",
|
|
)
|
|
AgentCommandService(repository).submit_result("test-agent", request)
|
|
finished = repository.finished[0]
|
|
self.assertIsNone(finished[4])
|
|
self.assertNotIn("192.0.2.10", str(finished))
|
|
|
|
def test_wrong_agent_pending_and_terminal_results_are_rejected(self):
|
|
request = AgentCommandResultRequest(
|
|
command_id="command-1", status="completed", result_payload=typed_result()
|
|
)
|
|
with self.assertRaises(AgentCommandConflict):
|
|
AgentCommandService(FakeRepository()).submit_result("wrong-agent", request)
|
|
for queue_status in ("pending", "completed", "failed", "cancelled"):
|
|
with self.subTest(status=queue_status), self.assertRaises(AgentCommandConflict):
|
|
row = FakeRepository().row.copy()
|
|
row["command_status"] = queue_status
|
|
AgentCommandService(FakeRepository(row)).submit_result("test-agent", request)
|
|
|
|
def test_invalid_status_and_secret_fields_are_rejected(self):
|
|
with self.assertRaises(Exception):
|
|
AgentCommandResultRequest(
|
|
command_id="command-1", status="unknown", result_payload={}
|
|
)
|
|
for forbidden in ("password", "account", "token", "raw_response"):
|
|
with self.subTest(field=forbidden):
|
|
repository = FakeRepository()
|
|
request = AgentCommandResultRequest(
|
|
command_id="command-1", status="completed",
|
|
result_payload={**typed_result(), forbidden: "sensitive"},
|
|
)
|
|
with self.assertRaises(AgentResultValidationError):
|
|
AgentCommandService(repository).submit_result("test-agent", request)
|
|
|
|
def test_suspend_resume_cannot_claim_confirmation(self):
|
|
for status in ("confirmed", "verification_failed"):
|
|
with self.subTest(status=status):
|
|
request = AgentCommandResultRequest(
|
|
command_id="command-1", status="completed",
|
|
result_payload=typed_result(
|
|
verification_attempted=True, verification_status=status
|
|
),
|
|
)
|
|
with self.assertRaises(AgentResultValidationError):
|
|
AgentCommandService(FakeRepository()).submit_result("test-agent", request)
|
|
|
|
|
|
class AgentCommandRouterTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.app = FastAPI()
|
|
self.app.include_router(router)
|
|
self.repository = FakeRepository()
|
|
self.app.dependency_overrides[get_agent_command_service] = lambda: AgentCommandService(
|
|
self.repository
|
|
)
|
|
self.client = TestClient(self.app)
|
|
self.old_id = settings.minerops_agent_id
|
|
self.old_secret = settings.minerops_agent_shared_secret
|
|
settings.minerops_agent_id = "test-agent"
|
|
settings.minerops_agent_shared_secret = "test-secret"
|
|
|
|
def tearDown(self):
|
|
settings.minerops_agent_id = self.old_id
|
|
settings.minerops_agent_shared_secret = self.old_secret
|
|
self.client.close()
|
|
|
|
def headers(self, agent="test-agent"):
|
|
return {
|
|
"X-MinerOps-Agent-ID": agent,
|
|
"X-MinerOps-Agent-Secret": "test-secret",
|
|
}
|
|
|
|
def test_poll_requires_header_identity_and_ignores_query_spoofing(self):
|
|
before_claim_calls = []
|
|
self.app.state.agent_command_before_claim = lambda: before_claim_calls.append(True)
|
|
self.assertEqual(self.client.get("/api/agent/commands/pending").status_code, 401)
|
|
self.assertEqual(
|
|
self.client.get(
|
|
"/api/agent/commands/pending?agent_id=other", headers=self.headers()
|
|
).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(before_claim_calls, [True])
|
|
self.assertEqual(
|
|
self.client.get(
|
|
"/api/agent/commands/pending", headers=self.headers("other")
|
|
).status_code,
|
|
403,
|
|
)
|
|
|
|
def test_result_requires_strict_claimed_contract(self):
|
|
response = self.client.post(
|
|
"/api/agent/commands/result",
|
|
headers=self.headers(),
|
|
json={
|
|
"command_id": "command-1", "status": "completed",
|
|
"result_payload": typed_result(),
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.json()["status"], "completed")
|
|
self.assertEqual(
|
|
self.client.post(
|
|
"/api/agent/commands/result", headers=self.headers(),
|
|
json={"command_id": "command-1", "status": "other",
|
|
"result_payload": {}},
|
|
).status_code,
|
|
422,
|
|
)
|
|
|
|
|
|
class AgentSourceContractTests(unittest.TestCase):
|
|
def test_agent_typed_path_has_locked_commands_and_no_default_credentials(self):
|
|
root = agent_source_root()
|
|
if root is None:
|
|
self.skipTest("Windows Agent source is not mounted")
|
|
service = (root / "MinerOpsAgentService.ps1").read_text()
|
|
control = (root / "MinerOpsControl.ps1").read_text()
|
|
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
|
|
powershell = "\n".join(path.read_text() for path in root.glob("*.ps1"))
|
|
self.assertNotIn('DefaultValue "super"', service)
|
|
self.assertNotIn('DefaultValue "super"', powershell)
|
|
self.assertIn('"X-MinerOps-Agent-ID"', service)
|
|
self.assertIn('.agent_zone_id', service)
|
|
self.assertIn('"agent_zone_id"', (root / "config.example.json").read_text())
|
|
self.assertNotIn("?agent_id=", service)
|
|
self.assertIn('"get.miner.setting"', control)
|
|
self.assertIn("$attempt -le 3", control)
|
|
self.assertIn("$Payload.performance_mode", control)
|
|
self.assertNotIn("api3_account =", control)
|
|
self.assertNotIn("api3_password =", control)
|
|
for forbidden in ("raw_response", "raw_command", "raw_exception"):
|
|
with self.subTest(field=forbidden):
|
|
self.assertNotIn(forbidden, control.lower())
|
|
self.assertNotIn('"agent_secret": secret', main)
|
|
self.assertNotIn('"secret_length":', main)
|
|
self.assertIn('"secret_configured": bool(secret)', main)
|
|
|
|
def test_deployment_preserves_local_state_and_registers_established_tasks(self):
|
|
root = agent_source_root()
|
|
if root is None:
|
|
self.skipTest("Windows Agent source is not mounted")
|
|
install = (root / "install.ps1").read_text()
|
|
uninstall = (root / "uninstall.ps1").read_text()
|
|
for task in (
|
|
"MinerOpsAgent", "MinerOpsAgentTray",
|
|
"MinerOpsApi3Enrichment", "MinerOpsPoolPoller",
|
|
):
|
|
self.assertIn(task, install)
|
|
self.assertIn(task, uninstall)
|
|
for local_path in ("agent-config.json", "agent-state.json", "logs"):
|
|
self.assertNotIn(f'"{local_path}"', install)
|
|
self.assertNotIn(f'"{local_path}"', uninstall)
|
|
self.assertNotIn("agent_secret", install.lower())
|
|
self.assertNotIn("api3_password", install.lower())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|