import json import threading import unittest import urllib.request from urllib.error import HTTPError from detector.api import APIServer from detector.app import DetectorApp from detector.config import Config class TestAPI(unittest.TestCase): def setUp(self): config = Config.from_env() config.http_addr = "127.0.0.1:0" self.app = DetectorApp(config) self.app.seed_demo_rules() self.app.start() self.api = APIServer(self.app, "127.0.0.1:0") self.port = self.api.server.server_address[1] self.base = f"http://127.0.0.1:{self.port}" self.thread = threading.Thread(target=self.api.serve_forever, daemon=True) self.thread.start() def tearDown(self): self.api.shutdown() self.api.server.server_close() self.app.stop() def _request(self, method, path, body=None): data = json.dumps(body).encode("utf-8") if body is not None else None req = urllib.request.Request( self.base + path, data=data, method=method, headers={"Content-Type": "application/json"}, ) try: with urllib.request.urlopen(req, timeout=5) as resp: return resp.status, json.loads(resp.read().decode("utf-8")) except HTTPError as exc: return exc.code, json.loads(exc.read().decode("utf-8")) def test_healthz(self): status, body = self._request("GET", "/healthz") self.assertEqual(status, 200) self.assertEqual(body["data"]["status"], "ok") def test_list_rules(self): status, body = self._request("GET", "/api/v1/rules") self.assertEqual(status, 200) self.assertGreaterEqual(body["data"]["total"], 3) def test_create_and_get_rule(self): payload = { "rule_id": "r-test", "name": "load high", "metric": "load_1m", "aggregation": "avg", "operator": "gt", "threshold": 5.0, "for_duration": "60s", "severity": "warning", "scope": {"scope_type": "all"}, "notify_channels": ["email"], "enabled": True, } status, body = self._request("POST", "/api/v1/rules", payload) self.assertEqual(status, 200) self.assertEqual(body["data"]["rule_id"], "r-test") status, body = self._request("GET", "/api/v1/rules/r-test") self.assertEqual(status, 200) self.assertEqual(body["data"]["threshold"], 5.0) def test_ingest_triggers_firing_event(self): samples = [ {"name": "cpu_usage", "value": 95.0, "timestamp": float(i * 15), "labels": {"service": "web"}} for i in range(5) ] status, body = self._request( "POST", "/api/v1/ingest", {"host_id": "h-001", "host_group": "web", "samples": samples}, ) self.assertEqual(status, 200) events = body["data"]["events"] self.assertEqual(len(events), 1) self.assertEqual(events[0]["status"], "firing") # 事件已落库 status, body = self._request("GET", "/api/v1/events?host_id=h-001") self.assertEqual(status, 200) self.assertEqual(body["data"]["total"], 1) # 告警已生成 status, body = self._request("GET", "/api/v1/alerts") self.assertEqual(status, 200) self.assertEqual(body["data"]["total"], 1) alert_id = body["data"]["items"][0]["alert_id"] # 确认告警 status, body = self._request("POST", f"/api/v1/alerts/{alert_id}/ack", {"ack_by": "tester"}) self.assertEqual(status, 200) self.assertEqual(body["data"]["ack_status"], "acked") def test_batch_ack(self): self._request( "POST", "/api/v1/ingest", {"host_id": "h-002", "host_group": "web", "samples": [{"name": "cpu_usage", "value": 95.0, "timestamp": float(i * 15)} for i in range(5)]}, ) status, body = self._request("GET", "/api/v1/alerts") alert_ids = [a["alert_id"] for a in body["data"]["items"]] status, body = self._request("POST", "/api/v1/alerts/batch-ack", {"alert_ids": alert_ids}) self.assertEqual(status, 200) self.assertEqual(body["data"]["acked"], len(alert_ids)) if __name__ == "__main__": unittest.main()