import _bootstrap # noqa: F401 import json import time import unittest import urllib.error import urllib.request from datetime import datetime, timezone from fault_log_analyzer.api import FaultLogApiServer from fault_log_analyzer.models import FaultLog, RootCause from fault_log_analyzer.storage import ( InMemoryFaultLogRepository, InMemoryFaultTypeRepository, InMemoryFilterRuleRepository, InMemoryRootCauseRepository, ) class TestFaultLogApiServer(unittest.TestCase): @classmethod def setUpClass(cls): cls.fl_repo = InMemoryFaultLogRepository() cls.rc_repo = InMemoryRootCauseRepository() cls.ft_repo = InMemoryFaultTypeRepository() cls.fr_repo = InMemoryFilterRuleRepository() cls.fl_repo.save( FaultLog( fault_log_id="fl-1", host_id="h-1", fingerprint="fp", level="ERROR", message="disk full", occurred_at=datetime(2025, 1, 1, tzinfo=timezone.utc), fault_type="disk_full", ) ) cls.rc_repo.save(RootCause(fault_log_id="fl-1", cause_type="disk_full", confidence=0.9)) cls.server = FaultLogApiServer( cls.fl_repo, cls.rc_repo, cls.ft_repo, cls.fr_repo, host="127.0.0.1", port=0 ) cls.server.start() # 等待监听线程就绪 time.sleep(0.05) @classmethod def tearDownClass(cls): cls.server.stop() def _get(self, path): with urllib.request.urlopen(f"http://127.0.0.1:{self.server.port}{path}", timeout=5) as resp: return json.loads(resp.read().decode("utf-8")) def _post(self, path, body): req = urllib.request.Request( f"http://127.0.0.1:{self.server.port}{path}", data=json.dumps(body).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=5) as resp: return json.loads(resp.read().decode("utf-8")) def test_healthz(self): data = self._get("/healthz") self.assertEqual(data["code"], 0) self.assertEqual(data["data"]["status"], "ok") def test_readyz(self): data = self._get("/readyz") self.assertEqual(data["code"], 0) self.assertIn("deps", data["data"]) def test_list_fault_logs(self): data = self._get("/api/v1/fault-logs") self.assertEqual(data["data"]["total"], 1) self.assertEqual(data["data"]["items"][0]["fault_log_id"], "fl-1") def test_list_fault_logs_filter(self): data = self._get("/api/v1/fault-logs?host_id=h-1&level=ERROR") self.assertEqual(data["data"]["total"], 1) def test_get_fault_log(self): data = self._get("/api/v1/fault-logs/fl-1") self.assertEqual(data["data"]["fault_log_id"], "fl-1") def test_get_fault_log_404(self): with self.assertRaises(urllib.error.HTTPError) as ctx: self._get("/api/v1/fault-logs/nope") self.assertEqual(ctx.exception.code, 404) def test_get_root_cause(self): data = self._get("/api/v1/fault-logs/fl-1/root-cause") self.assertEqual(data["data"]["cause_type"], "disk_full") def test_list_fault_types(self): data = self._get("/api/v1/fault-types") self.assertEqual(data["code"], 0) self.assertIn("items", data["data"]) def test_create_fault_type(self): data = self._post("/api/v1/fault-types", {"fault_type": "oom", "name": "OOM"}) self.assertEqual(data["code"], 0) self.assertEqual(data["data"]["fault_type"], "oom") def test_create_fault_type_missing_fields(self): with self.assertRaises(urllib.error.HTTPError) as ctx: self._post("/api/v1/fault-types", {"name": "OOM"}) self.assertEqual(ctx.exception.code, 400) def test_list_fault_filters(self): data = self._get("/api/v1/fault-filters") self.assertEqual(data["code"], 0) def test_create_fault_filter(self): data = self._post( "/api/v1/fault-filters", {"name": "disk", "level": "ERROR", "pattern": "disk"} ) self.assertEqual(data["code"], 0) def test_unknown_route_404(self): with self.assertRaises(urllib.error.HTTPError) as ctx: self._get("/nope") self.assertEqual(ctx.exception.code, 404) if __name__ == "__main__": unittest.main()