fix(alerts): remediate P0 gateway auth, P1 Zyla schema, and budget safeguards (#31, #32)

This commit is contained in:
Developer 2026-10-10 18:23:32 -04:00
parent 625999245e
commit 9f73fd4cd4
12 changed files with 989 additions and 388 deletions

View File

@ -1,7 +1,9 @@
import math
import os
import time
from typing import Any, Optional
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
class GeographicCellCache:
@ -107,3 +109,98 @@ class CircuitBreaker:
self.failure_count = 0
self.state = "CLOSED"
self.last_error_message = None
class PersistentUsageTracker:
"""
Durable monthly usage and request-budget tracker.
Persists request counts and cost estimates to disk to ensure
process restarts or server reboots cannot reset billing safeguards.
"""
def __init__(
self,
provider_id: str,
monthly_budget_limit: int = 500,
cost_per_request_usd: float = 0.005,
state_file: Optional[Any] = None,
):
from pathlib import Path
import json
self.provider_id = provider_id
self.monthly_budget_limit = monthly_budget_limit
self.cost_per_request_usd = cost_per_request_usd
if state_file is not None:
self.state_file = Path(state_file)
else:
base_dir = Path(os.environ.get("USAGE_STATE_DIR", Path(__file__).resolve().parent.parent.parent / "storage"))
self.state_file = base_dir / f"usage_{provider_id}.json"
from threading import Lock
self.lock = Lock()
self.current_period = datetime.now(timezone.utc).strftime("%Y-%m")
self.requests_used: int = 0
self.total_lifetime_requests: int = 0
self._load()
def _load(self) -> None:
try:
if self.state_file.exists():
import json
with open(self.state_file, "r", encoding="utf-8") as f:
data = json.load(f)
saved_period = data.get("period")
if saved_period == self.current_period:
self.requests_used = int(data.get("requests_used", 0))
else:
self.requests_used = 0
self.total_lifetime_requests = int(data.get("lifetime_requests", self.requests_used))
except Exception:
pass
def _save(self) -> None:
try:
import json
self.state_file.parent.mkdir(parents=True, exist_ok=True)
tmp_path = self.state_file.with_suffix(".tmp")
data = {
"provider_id": self.provider_id,
"period": self.current_period,
"requests_used": self.requests_used,
"lifetime_requests": self.total_lifetime_requests,
"monthly_budget_limit": self.monthly_budget_limit,
"estimated_cost_usd": round(self.requests_used * self.cost_per_request_usd, 4),
"updated_at": datetime.now(timezone.utc).isoformat(),
}
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
tmp_path.replace(self.state_file)
except Exception:
pass
def check_period(self) -> None:
now_period = datetime.now(timezone.utc).strftime("%Y-%m")
if now_period != self.current_period:
self.current_period = now_period
self.requests_used = 0
self._save()
def can_request(self) -> bool:
with self.lock:
self.check_period()
return self.requests_used < self.monthly_budget_limit
def record_request(self) -> int:
with self.lock:
self.check_period()
self.requests_used += 1
self.total_lifetime_requests += 1
self._save()
return self.requests_used
@property
def estimated_cost_usd(self) -> float:
return round(self.requests_used * self.cost_per_request_usd, 4)

View File

@ -6,9 +6,12 @@ from pydantic import BaseModel, Field, field_validator
class NormalizedAlert(BaseModel):
id: str = Field(description="Normalized unique alert identifier")
source: str = Field(description="Attributed data source name")
source_type: Literal["native_rider", "licensed_external", "official_dot"] = Field(
description="Source classification"
)
source_type: Literal[
"native_rider",
"experimental_external",
"licensed_external",
"official_dot",
] = Field(description="Source classification")
category: Literal[
"police",
"accident",
@ -29,12 +32,15 @@ class NormalizedAlert(BaseModel):
reported_at_utc: str = Field(description="ISO 8601 UTC creation timestamp")
fetched_at_utc: str = Field(description="ISO 8601 UTC ingest timestamp")
expires_at_utc: str = Field(description="ISO 8601 UTC expiration timestamp")
confidence: float = Field(ge=0.0, le=1.0, description="Normalized confidence score (0.0 to 1.0)")
confidence: Optional[float] = Field(
default=None, ge=0.0, le=1.0, description="Normalized confidence score (0.0 to 1.0)"
)
reliability: Optional[float] = Field(default=None, description="Source reliability score")
is_stale: bool = Field(default=False, description="Flag indicating if record exceeds freshness TTL")
raw_provider_id: Optional[str] = Field(default=None, description="Original upstream provider/alert ID")
class AlertFeedResponse(BaseModel):
success: bool
count: int

View File

@ -63,25 +63,133 @@ def verify_rate_limit():
)
def verify_gateway_auth(
# Set of permitted static test fixture filenames
ALLOWED_FIXTURES = {"zyla_rome_sample.json", "zyla_atlanta_sample.json"}
def verify_gateway_or_user_auth(
authorization: Optional[str] = Header(None, alias="Authorization"),
x_gateway_key: Optional[str] = Header(None, alias="X-MotorSignal-Gateway-Key"),
):
"""
Authenticates requests via either:
1. A valid User Bearer Token (MotorSignal JWT access token or Firebase ID Token).
2. A matching server-side gateway secret (X-MotorSignal-Gateway-Key), if explicitly configured.
FAILS CLOSED: If neither is provided or valid, raises HTTP 401 Unauthorized.
"""
# 1. Server-to-server gateway key check (if configured in server environment)
required_secret = os.environ.get("MOTORSIGNAL_GATEWAY_SECRET")
if required_secret and x_gateway_key != required_secret:
if required_secret and x_gateway_key == required_secret:
return True
# 2. Bearer token check (MotorSignal access token or Firebase ID token)
if authorization:
scheme, _, token = authorization.partition(" ")
if scheme.lower() == "bearer" and token.strip():
token_val = token.strip()
# Try decoding MotorSignal access token
try:
from auth import decode_access_token
payload = decode_access_token(token_val)
if payload.get("type") in ("access", "test") or "sub" in payload:
return True
except Exception:
pass
# Try decoding Firebase ID token
try:
from auth import decode_firebase_id_token
claims = decode_firebase_id_token(token_val)
if claims.get("sub"):
return True
except Exception:
pass
# In test mode, accept simulated test/mock tokens
is_test_env = (
os.environ.get("TESTING", "").lower() in ("true", "1")
or os.environ.get("ENVIRONMENT", "").lower() == "test"
)
if is_test_env and (token_val.startswith("test_token_") or token_val.startswith("mock_token_")):
return True
# Strict Fail Closed
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or missing MotorSignal Gateway key",
detail="Authentication required. Provide a valid Bearer token or Gateway key.",
headers={"WWW-Authenticate": "Bearer"},
)
def verify_admin_auth(
authorization: Optional[str] = Header(None, alias="Authorization"),
x_admin_key: Optional[str] = Header(None, alias="X-MotorSignal-Admin-Key"),
):
"""
Requires admin privileges to modify operational provider switches.
FAILS CLOSED: If admin key is missing or invalid, raises HTTP 403 Forbidden.
"""
required_admin = os.environ.get("MOTORSIGNAL_ADMIN_KEY")
if required_admin and x_admin_key != required_admin:
if required_admin and x_admin_key == required_admin:
return True
if authorization:
scheme, _, token = authorization.partition(" ")
if scheme.lower() == "bearer" and token.strip():
token_val = token.strip()
try:
from auth import decode_access_token
payload = decode_access_token(token_val)
role = payload.get("role", "")
if role in ("owner", "operator", "admin"):
return True
except Exception:
pass
is_test_env = (
os.environ.get("TESTING", "").lower() in ("true", "1")
or os.environ.get("ENVIRONMENT", "").lower() == "test"
)
if is_test_env and token_val.startswith("test_admin_token_"):
return True
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Unauthorized access to Ops switch controls",
detail="Admin authorization required. Access forbidden.",
)
def validate_fixture_override(
fixture_override: Optional[str],
x_admin_key: Optional[str] = None,
) -> Optional[str]:
"""
Ensures fixture overrides cannot be exploited by normal clients in production.
"""
if not fixture_override:
return None
if fixture_override not in ALLOWED_FIXTURES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid fixture_override: '{fixture_override}'. Allowed fixtures: {list(ALLOWED_FIXTURES)}",
)
is_test_env = (
os.environ.get("TESTING", "").lower() in ("true", "1")
or os.environ.get("ENVIRONMENT", "").lower() == "test"
or os.environ.get("ENABLE_FIXTURE_OVERRIDE", "false").lower() in ("true", "1")
)
if is_test_env:
return fixture_override
required_admin = os.environ.get("MOTORSIGNAL_ADMIN_KEY")
if required_admin and x_admin_key == required_admin:
return fixture_override
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="fixture_override is restricted to test environments or authorized administrators.",
)
@ -109,10 +217,19 @@ def validate_bounding_box(min_lat: float, min_lng: float, max_lat: float, max_ln
)
@router.get("/health")
def get_public_health():
"""
Lightweight unauthenticated connectivity check. Does not expose operational metrics.
"""
return {"status": "ok", "service": "road-alerts"}
@router.get(
"/feed",
response_model=AlertFeedResponse,
dependencies=[Depends(verify_rate_limit), Depends(verify_gateway_auth)],
dependencies=[Depends(verify_rate_limit), Depends(verify_gateway_or_user_auth)],
)
def get_aggregated_feed(
min_lat: float = Query(..., ge=-90.0, le=90.0, description="Minimum latitude (South)"),
@ -122,12 +239,14 @@ def get_aggregated_feed(
include_zyla: bool = Query(default=True, description="Query Zyla Waze Data #4814 if enabled"),
include_ga511: bool = Query(default=True, description="Query Georgia 511 if enabled"),
fixture_override: Optional[str] = Query(default=None, description="Test fixture override name"),
x_admin_key: Optional[str] = Header(None, alias="X-MotorSignal-Admin-Key"),
):
"""
Retrieves multi-source road alerts within the bounded geographic box.
Enforces geographic bounds (<= 1.0 degree) and rate limits.
Enforces geographic bounds (<= 1.0 degree), rate limits, and authentication.
"""
validate_bounding_box(min_lat, min_lng, max_lat, max_lng)
safe_fixture = validate_fixture_override(fixture_override, x_admin_key)
all_alerts: list[NormalizedAlert] = []
providers_polled: list[str] = []
@ -138,7 +257,7 @@ def get_aggregated_feed(
min_lng=min_lng,
max_lat=max_lat,
max_lng=max_lng,
fixture_override=fixture_override,
fixture_override=safe_fixture,
)
all_alerts.extend(zyla_alerts)
providers_polled.append(zyla_adapter.PROVIDER_ID)
@ -165,7 +284,7 @@ def get_aggregated_feed(
@router.get(
"/zyla",
response_model=AlertFeedResponse,
dependencies=[Depends(verify_rate_limit), Depends(verify_gateway_auth)],
dependencies=[Depends(verify_rate_limit), Depends(verify_gateway_or_user_auth)],
)
def get_zyla_alerts(
min_lat: float = Query(..., ge=-90.0, le=90.0),
@ -173,19 +292,21 @@ def get_zyla_alerts(
max_lat: float = Query(..., ge=-90.0, le=90.0),
max_lng: float = Query(..., ge=-180.0, le=180.0),
fixture_override: Optional[str] = Query(default=None),
x_admin_key: Optional[str] = Header(None, alias="X-MotorSignal-Admin-Key"),
):
"""
Direct endpoint for Zyla Labs Waze Data API #4814.
Enforces geographic box bounds and server-side budget limits.
"""
validate_bounding_box(min_lat, min_lng, max_lat, max_lng)
safe_fixture = validate_fixture_override(fixture_override, x_admin_key)
alerts = zyla_adapter.fetch_alerts(
min_lat=min_lat,
min_lng=min_lng,
max_lat=max_lat,
max_lng=max_lng,
fixture_override=fixture_override,
fixture_override=safe_fixture,
)
return AlertFeedResponse(
success=True,
@ -199,7 +320,7 @@ def get_zyla_alerts(
@router.get(
"/ga511",
response_model=AlertFeedResponse,
dependencies=[Depends(verify_rate_limit), Depends(verify_gateway_auth)],
dependencies=[Depends(verify_rate_limit), Depends(verify_gateway_or_user_auth)],
)
def get_ga511_alerts(
min_lat: float = Query(..., ge=-90.0, le=90.0),
@ -228,11 +349,16 @@ def get_ga511_alerts(
)
@router.get("/ops/health", response_model=OpsHealthDashboardResponse)
@router.get(
"/ops/health",
response_model=OpsHealthDashboardResponse,
dependencies=[Depends(verify_gateway_or_user_auth)],
)
def get_ops_health():
"""
Operational health dashboard reporting uptime, failure rates, request counts,
cache hit ratio, and usage spend per provider.
Requires authentication to protect internal operational telemetry.
"""
zyla_health = zyla_adapter.get_health()
ga511_health = ga511_adapter.get_health()
@ -250,6 +376,7 @@ def get_ops_health():
)
@router.post("/ops/switches", dependencies=[Depends(verify_admin_auth)])
def update_provider_switch(payload: ProviderSwitchRequest):
"""

View File

@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional
from road_alerts.cache import CircuitBreaker, GeographicCellCache
from road_alerts.cache import CircuitBreaker, GeographicCellCache, PersistentUsageTracker
from road_alerts.models import NormalizedAlert, ProviderHealth
logger = logging.getLogger(__name__)
@ -40,7 +40,7 @@ class ZylaWazeAdapter:
"""
Backend-only adapter for Zyla Labs Waze Data API #4814.
Adheres strictly to zero-credential leakage, circuit breaker,
geographic caching, and schema normalization.
geographic caching, durable request-budget tracking, and schema normalization.
"""
PROVIDER_ID = "zyla_waze_4814"
@ -53,6 +53,7 @@ class ZylaWazeAdapter:
enabled: Optional[bool] = None,
monthly_budget_limit: int = 500,
cost_per_request_usd: float = 0.005,
state_file: Optional[Path] = None,
):
# Resolve config from environment or overrides
self.api_key = api_key if api_key is not None else os.environ.get("ZYLA_API_KEY", "")
@ -65,9 +66,15 @@ class ZylaWazeAdapter:
self.monthly_budget_limit = monthly_budget_limit
self.cost_per_request_usd = cost_per_request_usd
# Operational telemetry
# Operational telemetry and durable billing safeguard
self.cache = GeographicCellCache(cell_size_degrees=0.05, ttl_seconds=300.0)
self.circuit_breaker = CircuitBreaker(failure_threshold=5, recovery_timeout_sec=300.0)
self.usage_tracker = PersistentUsageTracker(
provider_id=self.PROVIDER_ID,
monthly_budget_limit=monthly_budget_limit,
cost_per_request_usd=cost_per_request_usd,
state_file=state_file,
)
self.request_count: int = 0
self.success_count: int = 0
self.error_count: int = 0
@ -87,6 +94,9 @@ class ZylaWazeAdapter:
else:
status = "operational"
monthly_used = self.usage_tracker.requests_used
estimated_cost = self.usage_tracker.estimated_cost_usd
return ProviderHealth(
provider_id=self.PROVIDER_ID,
name=self.PROVIDER_NAME,
@ -104,8 +114,8 @@ class ZylaWazeAdapter:
last_error_at=self.last_error_at,
last_error_message=self.last_error_message,
monthly_budget_limit=self.monthly_budget_limit,
monthly_requests_used=self.request_count,
estimated_cost_usd=round(self.request_count * self.cost_per_request_usd, 4),
monthly_requests_used=monthly_used,
estimated_cost_usd=estimated_cost,
)
def normalize_alert(self, raw: dict, now: datetime) -> Optional[NormalizedAlert]:
@ -221,45 +231,67 @@ class ZylaWazeAdapter:
return None
# 5. Identifier and Source Attribution
raw_id = str(raw.get("uuid") or raw.get("id") or f"{lat}_{lng}_{int(reported_at.timestamp())}")
raw_id = str(raw.get("alert_id") or raw.get("uuid") or raw.get("id") or f"{lat}_{lng}_{int(reported_at.timestamp())}")
normalized_id = f"zyla_4814_{raw_id}"
# 6. Confidence normalization (Waze rating 1-6, confidence 0-5)
raw_confidence = raw.get("confidence")
if isinstance(raw_confidence, (int, float)):
conf_score = max(0.1, min(1.0, float(raw_confidence) / 5.0))
# 6. Confidence normalization (Waze rating 1-6, confidence 0-5, or 0-10)
# Truthfully preserve 0 values without inventing synthetic default confidence
raw_confidence = raw.get("alert_confidence") if "alert_confidence" in raw else raw.get("confidence")
if raw_confidence is not None and isinstance(raw_confidence, (int, float)):
val = float(raw_confidence)
if 0.0 <= val <= 5.0:
conf_score: Optional[float] = round(max(0.0, min(1.0, val / 5.0)), 2)
elif 0.0 <= val <= 10.0:
conf_score = round(max(0.0, min(1.0, val / 10.0)), 2)
else:
conf_score = 0.7
conf_score = round(max(0.0, min(1.0, val)), 2)
else:
conf_score = None
raw_reliability = raw.get("reliability")
# 7. Reliability score preservation (including honest 0)
raw_reliability = raw.get("alert_reliability") if "alert_reliability" in raw else raw.get("reliability")
reliability_score = float(raw_reliability) if isinstance(raw_reliability, (int, float)) else None
raw_provider = raw.get("provider") or "Waze"
raw_provider_id = str(raw.get("provider_id") or raw.get("alert_id") or "4814")
description_text = raw.get("description") or raw.get("reportDescription")
return NormalizedAlert(
id=normalized_id,
source=self.PROVIDER_NAME,
source_type="licensed_external",
source_type="experimental_external",
category=category,
subtype=subtype,
latitude=round(lat, 6),
longitude=round(lng, 6),
road_name=raw.get("street"),
city=raw.get("city"),
description=raw.get("reportDescription"),
description=description_text,
reported_at_utc=reported_at.isoformat(),
fetched_at_utc=now.isoformat(),
expires_at_utc=expires_at.isoformat(),
confidence=round(conf_score, 2),
confidence=conf_score,
reliability=reliability_score,
is_stale=False,
raw_provider_id=str(raw.get("provider_id") or "4814"),
raw_provider_id=raw_provider_id,
)
def parse_payload(self, payload: dict, now: Optional[datetime] = None) -> list[NormalizedAlert]:
"""
Parses upstream Zyla payload, applying deduplication and normalization.
Parses upstream Zyla payload, supporting both top-level 'alerts' and wrapped
'{status: OK, data: {alerts: [...]}}' structures.
"""
if not isinstance(payload, dict):
return []
current_time = now or datetime.now(timezone.utc)
raw_alerts = payload.get("alerts", [])
if isinstance(payload.get("data"), dict):
raw_alerts = payload["data"].get("alerts", [])
elif isinstance(payload.get("alerts"), list):
raw_alerts = payload["alerts"]
else:
raw_alerts = []
if not isinstance(raw_alerts, list):
return []
@ -297,7 +329,7 @@ class ZylaWazeAdapter:
) -> list[NormalizedAlert]:
"""
Fetches alerts for a bounding box, enforcing cache, circuit breaker,
and zero live execution when disabled.
durable monthly budget tracker, and zero live execution when disabled.
"""
current_time = now or datetime.now(timezone.utc)
@ -320,13 +352,16 @@ class ZylaWazeAdapter:
logger.warning("Zyla Waze circuit breaker is OPEN. Requests prevented.")
return []
# 4. Request budget safeguard
if self.request_count >= self.monthly_budget_limit and not fixture_override:
# 4. Durable request budget safeguard
if not self.usage_tracker.can_request() and not fixture_override:
logger.warning("Zyla Waze monthly budget limit reached (%d).", self.monthly_budget_limit)
return []
# Increment tracked requests
self.request_count += 1
if not fixture_override:
self.usage_tracker.record_request()
try:
# 5. Fixture handling or mock fallback

View File

@ -1,6 +1,9 @@
{
"status": "OK",
"data": {
"alerts": [
{
"alert_id": "zyla_atl_pol_01",
"id": "zyla_atl_pol_01",
"uuid": "zyla_atl_pol_01",
"type": "POLICE",
@ -13,14 +16,18 @@
},
"pubMillis": 1791664300000,
"reportRating": 5,
"alert_confidence": 5,
"alert_reliability": 9,
"confidence": 5,
"reliability": 9,
"description": "Speed enforcement vehicle under Freedom Pkwy overpass",
"reportDescription": "Speed enforcement vehicle under Freedom Pkwy overpass",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:31:40Z"
},
{
"alert_id": "zyla_atl_acc_01",
"id": "zyla_atl_acc_01",
"uuid": "zyla_atl_acc_01",
"type": "ACCIDENT",
@ -33,14 +40,18 @@
},
"pubMillis": 1791664400000,
"reportRating": 4,
"alert_confidence": 5,
"alert_reliability": 8,
"confidence": 5,
"reliability": 8,
"description": "Multi-car crash blocking two left lanes near Cobb Cloverleaf",
"reportDescription": "Multi-car crash blocking two left lanes near Cobb Cloverleaf",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:33:20Z"
},
{
"alert_id": "zyla_atl_haz_01",
"id": "zyla_atl_haz_01",
"uuid": "zyla_atl_haz_01",
"type": "HAZARD",
@ -53,14 +64,18 @@
},
"pubMillis": 1791664250000,
"reportRating": 3,
"alert_confidence": 4,
"alert_reliability": 7,
"confidence": 4,
"reliability": 7,
"description": "Severe pothole right wheel track near Lenox Square",
"reportDescription": "Severe pothole right wheel track near Lenox Square",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:30:50Z"
},
{
"alert_id": "zyla_atl_clo_01",
"id": "zyla_atl_clo_01",
"uuid": "zyla_atl_clo_01",
"type": "ROAD_CLOSED",
@ -68,56 +83,43 @@
"street": "I-75 N at Howell Mill Rd",
"city": "Atlanta, GA",
"location": {
"x": -84.4250,
"y": 33.8150
"x": -84.4150,
"y": 33.8100
},
"pubMillis": 1791662000000,
"pubMillis": 1791663000000,
"reportRating": 5,
"alert_confidence": 5,
"alert_reliability": 10,
"confidence": 5,
"reliability": 10,
"reportDescription": "Overnight bridge joint replacement closure",
"description": "Overnight lane replacement construction",
"reportDescription": "Overnight lane replacement construction",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T19:53:20Z"
"publish_datetime_utc": "2026-10-10T20:10:00Z"
},
{
"alert_id": "zyla_atl_stale_01",
"id": "zyla_atl_stale_01",
"uuid": "zyla_atl_stale_01",
"type": "ACCIDENT",
"subtype": "ACCIDENT_MINOR",
"street": "Northside Dr NW",
"street": "GA-400 S",
"city": "Atlanta, GA",
"location": {
"x": -84.4100,
"y": 33.7800
"x": -84.3600,
"y": 33.8500
},
"pubMillis": 1791620000000,
"pubMillis": 1791650000000,
"reportRating": 2,
"confidence": 2,
"reliability": 3,
"reportDescription": "Cleared accident from 12 hours ago",
"alert_confidence": 2,
"alert_reliability": 3,
"description": "Cleared minor crash from 4 hours ago",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T08:13:20Z"
"publish_datetime_utc": "2026-10-10T16:33:20Z"
}
],
"jams": [
{
"id": "zyla_atl_jam_01",
"uuid": "zyla_atl_jam_01",
"street": "I-20 E",
"city": "Atlanta, GA",
"speed": 12.5,
"delay": 420,
"length": 1850,
"level": 4,
"line": [
{ "x": -84.4300, "y": 33.7480 },
{ "x": -84.4000, "y": 33.7500 }
],
"pubMillis": 1791664400000,
"provider": "Waze",
"provider_id": "4814"
"jams": []
}
]
}

View File

@ -1,6 +1,9 @@
{
"status": "OK",
"data": {
"alerts": [
{
"alert_id": "zyla_rome_pol_01",
"id": "zyla_rome_pol_01",
"uuid": "zyla_rome_pol_01",
"type": "POLICE",
@ -13,14 +16,18 @@
},
"pubMillis": 1791664000000,
"reportRating": 4,
"confidence": 5,
"reliability": 8,
"alert_confidence": 0,
"alert_reliability": 0,
"confidence": 0,
"reliability": 0,
"description": "Stationary radar trap Northbound on shoulder",
"reportDescription": "Stationary radar trap Northbound on shoulder",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:26:40Z"
},
{
"alert_id": "zyla_rome_haz_01",
"id": "zyla_rome_haz_01",
"uuid": "zyla_rome_haz_01",
"type": "HAZARD",
@ -33,14 +40,18 @@
},
"pubMillis": 1791664100000,
"reportRating": 3,
"alert_confidence": 4,
"alert_reliability": 7,
"confidence": 4,
"reliability": 7,
"description": "Tree branch in right lane around bend",
"reportDescription": "Tree branch in right lane around bend",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:28:20Z"
},
{
"alert_id": "zyla_rome_acc_01",
"id": "zyla_rome_acc_01",
"uuid": "zyla_rome_acc_01",
"type": "ACCIDENT",
@ -53,14 +64,18 @@
},
"pubMillis": 1791664200000,
"reportRating": 2,
"alert_confidence": 3,
"alert_reliability": 6,
"confidence": 3,
"reliability": 6,
"description": "Fender bender near Oostanaula bridge",
"reportDescription": "Fender bender near Oostanaula bridge",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:30:00Z"
},
{
"alert_id": "zyla_rome_clo_01",
"id": "zyla_rome_clo_01",
"uuid": "zyla_rome_clo_01",
"type": "ROAD_CLOSED",
@ -73,14 +88,18 @@
},
"pubMillis": 1791663000000,
"reportRating": 5,
"alert_confidence": 5,
"alert_reliability": 9,
"confidence": 5,
"reliability": 9,
"description": "Utility work road closure between 3rd and 4th Ave",
"reportDescription": "Utility work road closure between 3rd and 4th Ave",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:10:00Z"
},
{
"alert_id": "zyla_rome_stale_01",
"id": "zyla_rome_stale_01",
"uuid": "zyla_rome_stale_01",
"type": "POLICE",
@ -93,14 +112,15 @@
},
"pubMillis": 1791600000000,
"reportRating": 1,
"confidence": 1,
"reliability": 2,
"reportDescription": "Old radar report from 18 hours ago",
"alert_confidence": 1,
"alert_reliability": 2,
"description": "Old radar report from 18 hours ago",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T02:40:00Z"
},
{
"alert_id": "zyla_rome_future_01",
"id": "zyla_rome_future_01",
"uuid": "zyla_rome_future_01",
"type": "HAZARD",
@ -113,14 +133,15 @@
},
"pubMillis": 1854748800000,
"reportRating": 3,
"confidence": 3,
"reliability": 5,
"reportDescription": "Corrupted future timestamp record",
"alert_confidence": 3,
"alert_reliability": 5,
"description": "Corrupted future timestamp record",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2028-10-10T00:00:00Z"
},
{
"alert_id": "zyla_rome_malformed_01",
"id": "zyla_rome_malformed_01",
"uuid": "zyla_rome_malformed_01",
"type": "ACCIDENT",
@ -133,15 +154,16 @@
},
"pubMillis": 1791664000000,
"reportRating": 1,
"confidence": 1,
"reliability": 1,
"reportDescription": "Invalid out-of-bounds coordinates",
"alert_confidence": 1,
"alert_reliability": 1,
"description": "Invalid out-of-bounds coordinates",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:26:40Z"
},
{
"id": "zyla_rome_dup_01",
"alert_id": "zyla_rome_pol_01",
"id": "zyla_rome_pol_01",
"uuid": "zyla_rome_pol_01",
"type": "POLICE",
"subtype": "POLICE_VISIBLE",
@ -153,9 +175,9 @@
},
"pubMillis": 1791664000000,
"reportRating": 4,
"confidence": 5,
"reliability": 8,
"reportDescription": "Duplicate report with identical uuid",
"alert_confidence": 0,
"alert_reliability": 0,
"description": "Duplicate report with identical uuid",
"provider": "Waze",
"provider_id": "4814",
"publish_datetime_utc": "2026-10-10T20:26:40Z"
@ -163,3 +185,4 @@
],
"jams": []
}
}

View File

@ -1,6 +1,7 @@
import json
import os
import sys
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path
@ -13,9 +14,16 @@ PACKAGES_DIR = TEST_DIR.parent / ".python-packages"
sys.path.insert(0, str(APP_DIR))
sys.path.insert(0, str(PACKAGES_DIR))
from road_alerts.cache import CircuitBreaker, GeographicCellCache
from fastapi import HTTPException
from road_alerts.cache import CircuitBreaker, GeographicCellCache, PersistentUsageTracker
from road_alerts.ga511_adapter import Georgia511Adapter
from road_alerts.models import NormalizedAlert
from road_alerts.router import (
validate_bounding_box,
validate_fixture_override,
verify_admin_auth,
verify_gateway_or_user_auth,
)
from road_alerts.zyla_adapter import ZylaWazeAdapter
@ -29,15 +37,18 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
"""
Tests parsing the sanitized Rome, GA test fixture:
- Validates police, hazard, accident, and road closure normalization.
- Validates data.alerts actual response wrapper.
- Verifies honest 0.0 confidence and 0.0 reliability without synthetic inflation.
- Verifies rejection of stale, future-dated, and out-of-bounds coordinates.
- Verifies deduplication of duplicate alert IDs.
"""
payload = self.adapter.load_fixture("zyla_rome_sample.json")
self.assertGreater(len(payload.get("alerts", [])), 0)
raw_list = payload.get("data", {}).get("alerts", [])
self.assertGreater(len(raw_list), 0)
alerts = self.adapter.parse_payload(payload, now=self.ref_time)
# Expected valid alerts: police, hazard, accident, road closure = 4 distinct alerts
# Expected valid alerts: police, hazard/debris, accident, road closure = 4 distinct alerts
self.assertEqual(len(alerts), 4)
categories = [a.category for a in alerts]
@ -46,30 +57,37 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
self.assertIn("accident", categories)
self.assertIn("roadClosure", categories)
# 1. Police alert details
# 1. Police alert details & honest zero confidence/reliability
police_alert = next(a for a in alerts if a.category == "police")
self.assertEqual(police_alert.id, "zyla_4814_zyla_rome_pol_01")
self.assertEqual(police_alert.source, "Zyla Waze Data #4814")
self.assertEqual(police_alert.source_type, "licensed_external")
self.assertEqual(police_alert.source_type, "experimental_external")
self.assertEqual(police_alert.subtype, "police_visible")
self.assertAlmostEqual(police_alert.latitude, 34.2750)
self.assertAlmostEqual(police_alert.longitude, -85.1700)
self.assertEqual(police_alert.road_name, "Martha Berry Hwy")
self.assertEqual(police_alert.city, "Rome, GA")
self.assertGreaterEqual(police_alert.confidence, 0.8)
# Honest zero: alert_confidence: 0 -> 0.0, NOT invented 0.7 or clamped 0.1
self.assertEqual(police_alert.confidence, 0.0)
self.assertEqual(police_alert.reliability, 0.0)
# 2. Road closure details
# 2. Debris alert details
debris_alert = next(a for a in alerts if a.category == "debris")
self.assertEqual(debris_alert.confidence, 0.8) # 4 / 5.0 = 0.8
self.assertEqual(debris_alert.reliability, 7.0)
# 3. Road closure details
closure_alert = next(a for a in alerts if a.category == "roadClosure")
self.assertEqual(closure_alert.road_name, "Broad St")
self.assertEqual(closure_alert.subtype, "construction")
# 3. Verify stale, future, and malformed records were filtered
# 4. Verify stale, future, and malformed records were filtered
alert_ids = [a.id for a in alerts]
self.assertNotIn("zyla_4814_zyla_rome_stale_01", alert_ids)
self.assertNotIn("zyla_4814_zyla_rome_future_01", alert_ids)
self.assertNotIn("zyla_4814_zyla_rome_malformed_01", alert_ids)
# 4. Verify duplicate was filtered (only one alert with id ending in pol_01)
# 5. Verify duplicate was filtered (only one alert with id ending in pol_01)
pol_matches = [a for a in alerts if a.id == "zyla_4814_zyla_rome_pol_01"]
self.assertEqual(len(pol_matches), 1)
@ -110,6 +128,41 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
self.assertEqual(pothole.subtype, "pothole")
self.assertEqual(pothole.road_name, "Peachtree Rd NE")
def test_honest_confidence_and_reliability_no_invented_defaults(self):
"""
Tests that confidence and reliability fields are handled honestly:
- 0 values produce 0.0 (never clamped to 0.1 or defaulted to 0.7).
- Missing values produce None (never defaulted to 0.7).
"""
now = datetime.now(timezone.utc)
recent_millis = int(now.timestamp() * 1000)
# 1. Zero confidence and zero reliability
raw_zero = {
"alert_id": "test_zero_1",
"type": "POLICE",
"location": {"x": -85.17, "y": 34.25},
"pubMillis": recent_millis,
"alert_confidence": 0,
"alert_reliability": 0,
}
res_zero = self.adapter.normalize_alert(raw_zero, now)
self.assertIsNotNone(res_zero)
self.assertEqual(res_zero.confidence, 0.0)
self.assertEqual(res_zero.reliability, 0.0)
# 2. Missing confidence and missing reliability -> None
raw_missing = {
"alert_id": "test_missing_1",
"type": "HAZARD",
"location": {"x": -85.17, "y": 34.25},
"pubMillis": recent_millis,
}
res_missing = self.adapter.normalize_alert(raw_missing, now)
self.assertIsNotNone(res_missing)
self.assertIsNone(res_missing.confidence)
self.assertIsNone(res_missing.reliability)
def test_granular_freshness_policies(self):
"""
Verifies that freshness policies enforce category/subtype TTLs:
@ -122,7 +175,7 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
# 1. Police speed trap reported 35 mins ago -> expired
raw_police = {
"uuid": "trap_test_1",
"alert_id": "trap_test_1",
"type": "POLICE",
"subtype": "POLICE_HIDING",
"location": {"x": -85.17, "y": 34.25},
@ -133,7 +186,7 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
# 2. Road closure reported 4 hours ago -> still valid (24h TTL)
raw_closure = {
"uuid": "closure_test_1",
"alert_id": "closure_test_1",
"type": "ROAD_CLOSED",
"subtype": "ROAD_CLOSED_CONSTRUCTION",
"location": {"x": -85.17, "y": 34.25},
@ -143,36 +196,62 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
self.assertIsNotNone(res_closure)
self.assertEqual(res_closure.category, "roadClosure")
def test_zyla_disabled_by_policy_never_requests(self):
def test_persistent_usage_tracker_survives_process_restart(self):
"""
Ensures that when Zyla is disabled or API key is absent:
- No outbound requests are made.
- fetch_alerts returns an empty list cleanly.
- Status accurately reports disabled_by_policy or auth_missing.
Proves that PersistentUsageTracker persists usage to disk across restarts
and enforces monthly budget limits.
"""
# Disabled adapter
disabled_adapter = ZylaWazeAdapter(api_key="valid-key", enabled=False)
self.assertEqual(disabled_adapter.get_health().status, "disabled_by_policy")
res = disabled_adapter.fetch_alerts(34.0, -85.0, 34.5, -84.5)
self.assertEqual(res, [])
self.assertEqual(disabled_adapter.request_count, 0)
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
tmp_path = Path(tmp.name)
# Missing key adapter
missing_key_adapter = ZylaWazeAdapter(api_key="", enabled=True)
self.assertEqual(missing_key_adapter.get_health().status, "auth_missing")
res2 = missing_key_adapter.fetch_alerts(34.0, -85.0, 34.5, -84.5)
self.assertEqual(res2, [])
self.assertEqual(missing_key_adapter.request_count, 0)
try:
# 1. Process 1: Record 3 requests against a limit of 5
tracker1 = PersistentUsageTracker(
provider_id="test_provider",
monthly_budget_limit=5,
cost_per_request_usd=0.01,
state_file=tmp_path,
)
self.assertTrue(tracker1.can_request())
tracker1.record_request()
tracker1.record_request()
tracker1.record_request()
self.assertEqual(tracker1.requests_used, 3)
self.assertEqual(tracker1.estimated_cost_usd, 0.03)
# 2. Process 2 (simulated restart): Re-instantiate from the same state file
tracker2 = PersistentUsageTracker(
provider_id="test_provider",
monthly_budget_limit=5,
cost_per_request_usd=0.01,
state_file=tmp_path,
)
self.assertEqual(tracker2.requests_used, 3)
self.assertTrue(tracker2.can_request())
# Fill up to limit
tracker2.record_request()
tracker2.record_request()
self.assertEqual(tracker2.requests_used, 5)
self.assertFalse(tracker2.can_request())
# 3. Process 3: Re-instantiate after limit reached -> remains blocked!
tracker3 = PersistentUsageTracker(
provider_id="test_provider",
monthly_budget_limit=5,
cost_per_request_usd=0.01,
state_file=tmp_path,
)
self.assertEqual(tracker3.requests_used, 5)
self.assertFalse(tracker3.can_request())
finally:
if tmp_path.exists():
tmp_path.unlink()
def test_circuit_breaker_behavior_and_recovery(self):
"""
Tests that circuit breaker trips upon failures or 429 rate limiting,
protecting resources, and recovers after timeout.
"""
cb = CircuitBreaker(failure_threshold=3, recovery_timeout_sec=0.1)
self.assertTrue(cb.can_request())
# Simulate failures
cb.record_failure("500 Server Error")
self.assertEqual(cb.state, "CLOSED")
cb.record_failure("503 Service Unavailable")
@ -181,36 +260,26 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
self.assertEqual(cb.state, "OPEN")
self.assertFalse(cb.can_request())
# Immediate trip on 429 rate limit
cb_rate = CircuitBreaker()
cb_rate.record_failure("429 Too Many Requests", is_rate_limit=True)
self.assertEqual(cb_rate.state, "OPEN")
self.assertFalse(cb_rate_rate := cb_rate.can_request())
self.assertFalse(cb_rate.can_request())
def test_geographic_cell_cache(self):
"""
Tests that nearby / quantized bounding boxes hit cache to conserve budget.
"""
cache = GeographicCellCache(cell_size_degrees=0.05, ttl_seconds=60.0)
sample_data = ["alert_1", "alert_2"]
cache.set(34.2501, -85.1701, 34.2799, -85.1501, sample_data)
# Query slightly shifted box that falls in the same quantized grid
hit = cache.get(34.2520, -85.1710, 34.2780, -85.1520)
self.assertEqual(hit, sample_data)
self.assertEqual(cache.hits, 1)
# Query distinctly different cell
miss = cache.get(33.7500, -84.3800, 33.7900, -84.3500)
self.assertIsNone(miss)
self.assertEqual(cache.misses, 1)
self.assertEqual(cache.hit_ratio, 0.5)
def test_georgia_511_adapter(self):
"""
Tests Georgia 511 official event normalization and health.
"""
ga_adapter = Georgia511Adapter(api_key="ga-key", enabled=True)
ga_adapter.fixture_mode = True
@ -227,25 +296,102 @@ class TestZylaRoadAlertAdapter(unittest.TestCase):
self.assertAlmostEqual(first.confidence, 0.95)
def test_bounding_box_limit_enforcement(self):
"""
Verifies that geographic queries exceeding 1.0 degree span are rejected.
"""
from road_alerts.router import validate_bounding_box
from fastapi import HTTPException
# Valid box within 1.0 degree span
validate_bounding_box(34.2, -85.2, 34.8, -84.8)
# Inverted coordinates
with self.assertRaises(HTTPException) as ctx:
validate_bounding_box(35.0, -85.0, 34.0, -84.0)
self.assertEqual(ctx.exception.status_code, 400)
# Overly broad box exceeding 1.0 degree
with self.assertRaises(HTTPException) as ctx:
validate_bounding_box(30.0, -90.0, 35.0, -80.0)
self.assertEqual(ctx.exception.status_code, 400)
self.assertIn("maximum allowed geographic span", ctx.exception.detail)
class TestSecurityAndAuthGates(unittest.TestCase):
def setUp(self):
self.orig_env = dict(os.environ)
def tearDown(self):
os.environ.clear()
os.environ.update(self.orig_env)
def test_gateway_auth_fails_closed_when_unauthenticated(self):
"""
P0: Proves verify_gateway_or_user_auth fails closed (HTTP 401)
when no valid token or gateway key is provided.
"""
# Ensure secret is NOT set
os.environ.pop("MOTORSIGNAL_GATEWAY_SECRET", None)
os.environ["TESTING"] = "false"
# Missing both authorization and gateway key -> 401
with self.assertRaises(HTTPException) as ctx:
verify_gateway_or_user_auth(authorization=None, x_gateway_key=None)
self.assertEqual(ctx.exception.status_code, 401)
# Invalid bearer token -> 401
with self.assertRaises(HTTPException) as ctx:
verify_gateway_or_user_auth(authorization="Bearer invalid_garbage_token", x_gateway_key=None)
self.assertEqual(ctx.exception.status_code, 401)
# Valid gateway secret -> 200/True when configured
os.environ["MOTORSIGNAL_GATEWAY_SECRET"] = "correct_secret_999"
self.assertTrue(verify_gateway_or_user_auth(authorization=None, x_gateway_key="correct_secret_999"))
# Wrong gateway secret -> 401
with self.assertRaises(HTTPException) as ctx:
verify_gateway_or_user_auth(authorization=None, x_gateway_key="wrong_secret")
self.assertEqual(ctx.exception.status_code, 401)
def test_admin_auth_fails_closed_when_unauthorized(self):
"""
P0: Proves verify_admin_auth fails closed (HTTP 403)
when admin credentials or roles are absent.
"""
os.environ.pop("MOTORSIGNAL_ADMIN_KEY", None)
os.environ["TESTING"] = "false"
# Missing all auth -> 403
with self.assertRaises(HTTPException) as ctx:
verify_admin_auth(authorization=None, x_admin_key=None)
self.assertEqual(ctx.exception.status_code, 403)
# Configured admin key matches -> True
os.environ["MOTORSIGNAL_ADMIN_KEY"] = "super_admin_pass_123"
self.assertTrue(verify_admin_auth(authorization=None, x_admin_key="super_admin_pass_123"))
# Wrong admin key -> 403
with self.assertRaises(HTTPException) as ctx:
verify_admin_auth(authorization=None, x_admin_key="bad_admin_pass")
self.assertEqual(ctx.exception.status_code, 403)
def test_fixture_override_security_restrictions(self):
"""
P1: Proves fixture_override rejects path traversal and unknown fixtures,
and forbids ordinary callers in production.
"""
os.environ["TESTING"] = "false"
os.environ["ENABLE_FIXTURE_OVERRIDE"] = "false"
os.environ.pop("MOTORSIGNAL_ADMIN_KEY", None)
# Disallowed / path traversal fixture -> 400
with self.assertRaises(HTTPException) as ctx:
validate_fixture_override("../../etc/passwd")
self.assertEqual(ctx.exception.status_code, 400)
# Disallowed arbitrary fixture -> 400
with self.assertRaises(HTTPException) as ctx:
validate_fixture_override("malicious_data.json")
self.assertEqual(ctx.exception.status_code, 400)
# Allowed fixture in production without admin key -> 403
with self.assertRaises(HTTPException) as ctx:
validate_fixture_override("zyla_rome_sample.json")
self.assertEqual(ctx.exception.status_code, 403)
# In test mode -> allowed
os.environ["TESTING"] = "true"
self.assertEqual(validate_fixture_override("zyla_rome_sample.json"), "zyla_rome_sample.json")
if __name__ == "__main__":

View File

@ -1050,7 +1050,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
iconColor: Colors.amberAccent,
title: 'Zyla Waze Data #4814 (Backend Gateway)',
subtitle:
'Commercial licensed Waze Data feed via MotorSignal API backend. Disabled by default; client never holds credentials.',
'Experimental Zyla Waze Data #4814 feed via MotorSignal API backend. Disabled by default; client never holds credentials.',
status: zylaStatus,
isEnabled: zylaEnabled,
),

View File

@ -13,9 +13,13 @@ import 'waze_road_alert_provider.dart';
import 'waze_osint_road_alert_provider.dart';
import 'zyla_road_alert_provider.dart';
import 'ga511_road_alert_provider.dart';
import '../auth/auth_providers.dart';
final zylaProviderInstance = Provider<ZylaRoadAlertProvider>((ref) {
return ZylaRoadAlertProvider();
final authService = ref.watch(authServiceProvider);
return ZylaRoadAlertProvider(
getAuthToken: () => authService.getIdToken(),
);
});
final ga511ProviderInstance = Provider<Georgia511RoadAlertProvider>((ref) {

View File

@ -12,10 +12,12 @@ import 'road_alert_provider_interface.dart';
class ZylaRoadAlertProvider implements RoadAlertProvider {
final http.Client _client;
final String backendBaseUrl;
final Future<String?> Function()? getAuthToken;
ZylaRoadAlertProvider({
http.Client? client,
String? backendBaseUrl,
this.getAuthToken,
}) : _client = client ?? http.Client(),
backendBaseUrl = backendBaseUrl ??
const String.fromEnvironment(
@ -43,11 +45,35 @@ class ZylaRoadAlertProvider implements RoadAlertProvider {
RoadAlertCategory.other,
};
Future<Map<String, String>> _buildHeaders({String? apiKey}) async {
final headers = <String, String>{
'Accept': 'application/json',
};
if (getAuthToken != null) {
try {
final token = await getAuthToken!();
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
return headers;
}
} catch (e) {
if (kDebugMode) {
debugPrint('[ZylaRoadAlertProvider] Error resolving auth token: $e');
}
}
}
if (apiKey != null && apiKey.isNotEmpty) {
headers['Authorization'] = 'Bearer $apiKey';
}
return headers;
}
@override
Future<RoadAlertProviderStatus> testConnection(String apiKey) async {
try {
final headers = await _buildHeaders(apiKey: apiKey);
final uri = Uri.parse('$backendBaseUrl/api/v1/alerts/ops/health');
final res = await _client.get(uri).timeout(const Duration(seconds: 4));
final res = await _client.get(uri, headers: headers).timeout(const Duration(seconds: 4));
if (res.statusCode == 200) {
final data = json.decode(res.body);
final providers = data['providers'] as List<dynamic>? ?? [];
@ -62,6 +88,9 @@ class ZylaRoadAlertProvider implements RoadAlertProvider {
}
return RoadAlertProviderStatus.connected;
}
if (res.statusCode == 401 || res.statusCode == 403) {
return RoadAlertProviderStatus.authError;
}
return RoadAlertProviderStatus.networkError;
} catch (e) {
if (kDebugMode) {
@ -103,7 +132,14 @@ class ZylaRoadAlertProvider implements RoadAlertProvider {
);
try {
final response = await _client.get(uri).timeout(const Duration(seconds: 5));
final headers = await _buildHeaders(apiKey: apiKey);
final response = await _client.get(uri, headers: headers).timeout(const Duration(seconds: 5));
if (response.statusCode == 401 || response.statusCode == 403) {
if (kDebugMode) {
debugPrint('[ZylaRoadAlertProvider] Backend rejected request with auth status ${response.statusCode}');
}
return [];
}
if (response.statusCode != 200) {
if (kDebugMode) {
debugPrint('[ZylaRoadAlertProvider] Backend returned status ${response.statusCode}');
@ -157,7 +193,7 @@ class ZylaRoadAlertProvider implements RoadAlertProvider {
final expiresAtStr = raw['expires_at_utc']?.toString();
final expiresAt = expiresAtStr != null ? DateTime.tryParse(expiresAtStr) : null;
final confidence = (raw['confidence'] as num?)?.toDouble() ?? 0.7;
final confidence = (raw['confidence'] as num?)?.toDouble();
// Approximate distance
final dLat = (lat - userLat) * 111320.0;

View File

@ -337,6 +337,131 @@ void main() {
final status = await provider.testConnection('');
expect(status, equals(RoadAlertProviderStatus.connected));
});
test('transmits Authorization bearer token when getAuthToken is configured', () async {
String? capturedAuthHeader;
final mockClient = MockClient((request) async {
capturedAuthHeader = request.headers['authorization'] ?? request.headers['Authorization'];
return http.Response(
json.encode({
'alerts': [],
'count': 0,
'cached': false,
'attribution': 'Waze / Zyla Labs #4814'
}),
200,
headers: {'content-type': 'application/json'},
);
});
final provider = ZylaRoadAlertProvider(
client: mockClient,
backendBaseUrl: 'http://127.0.0.1:8000',
getAuthToken: () async => 'firebase-test-id-token-12345',
);
await provider.getAlerts(
latitude: 34.2560,
longitude: -85.1650,
radiusMeters: 5000,
);
expect(capturedAuthHeader, equals('Bearer firebase-test-id-token-12345'));
});
test('testConnection returns authError on 401 Unauthorized', () async {
final mockClient = MockClient((request) async {
return http.Response(
json.encode({'detail': 'Missing authentication credentials'}),
401,
headers: {'content-type': 'application/json'},
);
});
final provider = ZylaRoadAlertProvider(
client: mockClient,
backendBaseUrl: 'http://127.0.0.1:8000',
);
final status = await provider.testConnection('');
expect(status, equals(RoadAlertProviderStatus.authError));
});
test('getAlerts handles 401 Unauthorized gracefully by returning empty list', () async {
final mockClient = MockClient((request) async {
return http.Response(
json.encode({'detail': 'Invalid authentication token'}),
401,
headers: {'content-type': 'application/json'},
);
});
final provider = ZylaRoadAlertProvider(
client: mockClient,
backendBaseUrl: 'http://127.0.0.1:8000',
);
final alerts = await provider.getAlerts(
latitude: 34.2560,
longitude: -85.1650,
radiusMeters: 5000,
);
expect(alerts, isEmpty);
});
test('preserves honest 0.0 confidence and null confidence without synthetic 0.7 inflation', () async {
final mockClient = MockClient((request) async {
final responseBody = json.encode({
'provider_id': 'zyla_waze_4814',
'source': 'Zyla Waze Data #4814',
'alerts': [
{
'id': 'zyla_zero_conf',
'category': 'police',
'subtype': 'POLICE_VISIBLE',
'latitude': 34.2560,
'longitude': -85.1650,
'road_name': 'Martha Berry Hwy',
'description': 'Radar Trap',
'reported_at_utc': '2026-10-10T12:00:00Z',
'confidence': 0.0,
'source': 'Zyla Waze Data #4814'
},
{
'id': 'zyla_null_conf',
'category': 'hazard',
'subtype': 'HAZARD_ON_ROAD',
'latitude': 34.2570,
'longitude': -85.1660,
'road_name': 'Turner McCall Blvd',
'description': 'Road Obstacle',
'reported_at_utc': '2026-10-10T12:01:00Z',
'confidence': null,
'source': 'Zyla Waze Data #4814'
}
],
'count': 2,
});
return http.Response(responseBody, 200, headers: {'content-type': 'application/json'});
});
final provider = ZylaRoadAlertProvider(
client: mockClient,
backendBaseUrl: 'http://127.0.0.1:8000',
);
final alerts = await provider.getAlerts(
latitude: 34.2560,
longitude: -85.1650,
radiusMeters: 5000,
);
expect(alerts.length, equals(2));
expect(alerts[0].confidence, equals(0.0));
expect(alerts[1].confidence, isNull);
});
});
group('Georgia511RoadAlertProvider Tests', () {

View File

@ -123,7 +123,7 @@ void main() {
tester,
) async {
await open(tester);
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
await tester.pumpAndSettle();
// Deliberately resolve after the original implementation disposed its ref.
location.current.complete(
@ -160,7 +160,7 @@ void main() {
tester,
) async {
await open(tester);
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
await tester.pump();
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
@ -185,13 +185,13 @@ void main() {
testWidgets('repeated taps create one report', (tester) async {
await open(tester);
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
await tester.tap(find.text('Road Hazard'));
await tester.pump();
expect(
tester
.widget<ElevatedButton>(
find.widgetWithText(ElevatedButton, 'HAZARD / GRAVEL / DEBRIS'),
find.widgetWithText(ElevatedButton, 'Road Hazard'),
)
.onPressed,
isNull,
@ -219,7 +219,7 @@ void main() {
tester,
) async {
await open(tester);
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
location.current.completeError(const PermissionDeniedException('denied'));
await tester.pumpAndSettle();
expect(alerts.reports, isEmpty);
@ -232,7 +232,7 @@ void main() {
testWidgets('close cancels even before its exit animation finishes', (tester) async {
await open(tester);
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
await tester.tap(find.byIcon(Icons.close));
location.current.complete(_position());
await tester.pumpAndSettle();
@ -242,13 +242,13 @@ void main() {
testWidgets('GPS timeout falls back and missing GPS permits retry', (tester) async {
await open(tester);
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
await tester.pump(const Duration(seconds: 5));
await tester.pumpAndSettle();
expect(alerts.reports, isEmpty);
expect(find.text('Cannot report: GPS location unavailable.'), findsOneWidget);
expect(tester.widget<ElevatedButton>(find.widgetWithText(
ElevatedButton, 'HAZARD / GRAVEL / DEBRIS')).onPressed, isNotNull);
ElevatedButton, 'Road Hazard')).onPressed, isNotNull);
location.current.complete(_position());
await tester.pumpAndSettle();
expect(alerts.reports, isEmpty);
@ -258,7 +258,7 @@ void main() {
final lastKnown = Completer<Position?>();
location.lastKnown = () => lastKnown.future;
await open(tester);
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
location.current.completeError(StateError('GPS unavailable'));
await tester.pump();
await tester.pump(const Duration(seconds: 2));
@ -272,7 +272,7 @@ void main() {
testWidgets('confirmed cloud outcome has a persisted fake document', (tester) async {
final firestore = FakeFirebaseFirestore();
await open(tester, service: SwarmRoadAlertService(firestore: firestore));
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
location.current.complete(_position());
await tester.pumpAndSettle();
expect(alerts.reports, hasLength(1));
@ -291,7 +291,7 @@ void main() {
}
''');
await open(tester, service: SwarmRoadAlertService(firestore: firestore));
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
location.current.complete(_position());
await tester.pumpAndSettle();
expect(alerts.reports, hasLength(1));
@ -302,7 +302,7 @@ void main() {
testWidgets('pending acknowledgement returns locally without retrying write', (tester) async {
final firestore = _DelayedFirestore();
await open(tester, service: SwarmRoadAlertService(firestore: firestore));
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
location.current.complete(_position());
await tester.pump();
await tester.pump(const Duration(seconds: 6));
@ -319,7 +319,7 @@ void main() {
testWidgets('removing provider scope during cloud write avoids disposed service', (tester) async {
final firestore = _DelayedFirestore();
await open(tester, service: SwarmRoadAlertService(firestore: firestore));
await tester.tap(find.text('HAZARD / GRAVEL / DEBRIS'));
await tester.tap(find.text('Road Hazard'));
location.current.complete(_position());
await tester.pump();
expect(firestore.writes, 1);