diff --git a/app/models/alerts.py b/app/models/alerts.py index 18152b1..c749a56 100644 --- a/app/models/alerts.py +++ b/app/models/alerts.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import math from dataclasses import asdict, dataclass from pathlib import Path from typing import Any @@ -25,6 +26,22 @@ class AlertEvent: updated_at: str 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]: return asdict(self) diff --git a/tests/test_alerts.py b/tests/test_alerts.py index 84e0a6d..ef64653 100644 --- a/tests/test_alerts.py +++ b/tests/test_alerts.py @@ -188,6 +188,15 @@ def test_alert_history_repository_raises_on_non_list_payload(tmp_path: Path) -> 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: history_path = tmp_path / "alert_history.json" 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() +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: alert_service.repository.history_path.write_text("{not valid json", encoding="utf-8") config = PortfolioConfig(