fix(CORE-001D3B): validate alert history entry types
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -25,6 +26,22 @@ class AlertEvent:
|
|||||||
updated_at: str
|
updated_at: str
|
||||||
email_alerts_enabled: bool
|
email_alerts_enabled: bool
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
for field_name in ("severity", "message", "updated_at"):
|
||||||
|
value = getattr(self, field_name)
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError(f"{field_name} must be a string")
|
||||||
|
for field_name in ("ltv_ratio", "warning_threshold", "critical_threshold", "spot_price"):
|
||||||
|
value = getattr(self, field_name)
|
||||||
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||||
|
raise TypeError(f"{field_name} must be numeric")
|
||||||
|
numeric_value = float(value)
|
||||||
|
if not math.isfinite(numeric_value):
|
||||||
|
raise ValueError(f"{field_name} must be finite")
|
||||||
|
setattr(self, field_name, numeric_value)
|
||||||
|
if not isinstance(self.email_alerts_enabled, bool):
|
||||||
|
raise TypeError("email_alerts_enabled must be a bool")
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,15 @@ def test_alert_history_repository_raises_on_non_list_payload(tmp_path: Path) ->
|
|||||||
repository.load()
|
repository.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_alert_history_repository_raises_on_non_object_entry(tmp_path: Path) -> None:
|
||||||
|
history_path = tmp_path / "alert_history.json"
|
||||||
|
history_path.write_text('["bad entry"]', encoding="utf-8")
|
||||||
|
repository = AlertHistoryRepository(history_path=history_path)
|
||||||
|
|
||||||
|
with pytest.raises(AlertHistoryLoadError, match="Alert history entry 0 must be an object"):
|
||||||
|
repository.load()
|
||||||
|
|
||||||
|
|
||||||
def test_alert_history_repository_raises_on_invalid_list_entry(tmp_path: Path) -> None:
|
def test_alert_history_repository_raises_on_invalid_list_entry(tmp_path: Path) -> None:
|
||||||
history_path = tmp_path / "alert_history.json"
|
history_path = tmp_path / "alert_history.json"
|
||||||
history_path.write_text('[{"severity": "warning"}]', encoding="utf-8")
|
history_path.write_text('[{"severity": "warning"}]', encoding="utf-8")
|
||||||
@@ -197,6 +206,20 @@ def test_alert_history_repository_raises_on_invalid_list_entry(tmp_path: Path) -
|
|||||||
repository.load()
|
repository.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_alert_history_repository_raises_on_wrong_typed_fields(tmp_path: Path) -> None:
|
||||||
|
history_path = tmp_path / "alert_history.json"
|
||||||
|
history_path.write_text(
|
||||||
|
'[{"severity": "warning", "message": "bad", "ltv_ratio": "oops", "warning_threshold": 0.7, '
|
||||||
|
'"critical_threshold": 0.75, "spot_price": 215.0, "updated_at": "2026-03-24T12:00:00Z", '
|
||||||
|
'"email_alerts_enabled": false}]',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
repository = AlertHistoryRepository(history_path=history_path)
|
||||||
|
|
||||||
|
with pytest.raises(AlertHistoryLoadError, match="Alert history entry 0 is invalid"):
|
||||||
|
repository.load()
|
||||||
|
|
||||||
|
|
||||||
def test_alert_service_marks_history_unavailable_on_corrupt_storage(alert_service: AlertService) -> None:
|
def test_alert_service_marks_history_unavailable_on_corrupt_storage(alert_service: AlertService) -> None:
|
||||||
alert_service.repository.history_path.write_text("{not valid json", encoding="utf-8")
|
alert_service.repository.history_path.write_text("{not valid json", encoding="utf-8")
|
||||||
config = PortfolioConfig(
|
config = PortfolioConfig(
|
||||||
|
|||||||
Reference in New Issue
Block a user