feat: add safe miner control foundation
This commit is contained in:
1 parent
6cd876b4b3
commit
f5965af914
39 files changed
+2952
-491
No files matched your search
@@ -0,0 +1,235 @@
|
||||
"""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.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()
|
||||
Reference in new issue
Block a user