Files
PolyWeather/tests/test_web_observability.py
T
2026-04-08 07:13:54 +08:00

302 lines
10 KiB
Python

from fastapi.testclient import TestClient
from web.app import app
import web.routes as routes
from src.database.db_manager import DBManager
from src.database.runtime_state import TruthRecordRepository, TrainingFeatureRecordRepository
client = TestClient(app)
def test_healthz_returns_ok_shape():
response = client.get('/healthz')
assert response.status_code == 200
payload = response.json()
assert payload['status'] in {'ok', 'degraded'}
assert 'db' in payload
assert 'state_storage_mode' in payload
assert 'cities_count' in payload
def test_system_status_returns_summary_shape():
response = client.get('/api/system/status')
assert response.status_code == 200
payload = response.json()
assert 'db' in payload
assert 'state_storage_mode' in payload
assert 'features' in payload
assert 'integrations' in payload
assert 'cache' in payload
assert 'analysis' in payload['cache']
assert 'probability' in payload
assert 'rollout' in payload['probability']
assert payload['probability']['rollout']['decision']['decision'] in {'hold', 'observe', 'promote'}
assert 'training_data' in payload
assert 'station_networks' in payload
assert 'prewarm' in payload
assert 'truth_records' in payload['training_data']
assert 'training_features' in payload['training_data']
assert 'city_coverage' in payload['training_data']
assert 'model_city_coverage' in payload['training_data']
assert 'artifacts' in payload['training_data']
assert 'metar_entries' in payload['cache']
assert 'nmc_entries' in payload['cache']
assert 'runtime' in payload['prewarm']
assert 'last_summary_ok' in payload['prewarm']['runtime']
assert 'cities_count' in payload
def test_system_status_reads_shared_prewarm_runtime(monkeypatch):
monkeypatch.setenv("POLYWEATHER_DASHBOARD_PREWARM_ENABLED", "true")
monkeypatch.setenv("POLYWEATHER_PREWARM_INTERVAL_SEC", "300")
monkeypatch.setenv("POLYWEATHER_PREWARM_JITTER_SEC", "20")
DBManager().set_payment_runtime_state(
"dashboard_prewarm",
{
"cycle_count": 3,
"success_count": 3,
"failure_count": 0,
"last_started_at": "2026-04-08T12:00:00",
"last_finished_at": "2026-04-08T12:00:05",
"last_duration_sec": 5.0,
"last_success": True,
"last_http_status": 200,
"last_error": None,
"last_requested_cities": ["shanghai", "beijing"],
"last_requested_city_count": 2,
"last_include_detail": True,
"last_include_market": True,
"last_force_refresh": False,
"last_warmed_count": 2,
"last_summary_ok": 2,
"last_detail_ok": 2,
"last_market_ok": 2,
"last_failed_count": 0,
"last_heartbeat_ts": __import__("time").time(),
"writer_mode": "standalone_process",
"writer_pid": 12345,
"writer_thread_name": "MainThread",
},
)
response = client.get('/api/system/status')
assert response.status_code == 200
payload = response.json()
assert payload["prewarm"]["enabled"] is True
assert payload["prewarm"]["thread_alive"] is True
assert payload["prewarm"]["runtime"]["cycle_count"] >= 3
assert payload["prewarm"]["runtime"]["last_summary_ok"] == 2
def test_metrics_endpoint_returns_prometheus_payload():
response = client.get('/metrics')
assert response.status_code == 200
assert 'polyweather_http_requests_total' in response.text
def test_cities_endpoint_uses_denver_display_name_for_aurora_market():
response = client.get("/api/cities")
assert response.status_code == 200
payload = response.json()
aurora = next(item for item in payload["cities"] if item["name"] == "aurora")
assert aurora["display_name"] == "Denver"
assert aurora["network_provider"] == "global_metar"
assert aurora["deb_recent_tier"] in {"high", "medium", "low", "other"}
assert "deb_recent_sample_count" in aurora
def test_cities_endpoint_includes_new_wunderground_cities():
response = client.get("/api/cities")
assert response.status_code == 200
payload = response.json()
names = {item["name"] for item in payload["cities"]}
assert {
"busan",
"panama city",
"kuala lumpur",
"jakarta",
"helsinki",
"amsterdam",
}.issubset(names)
def test_payment_runtime_endpoint_returns_shape():
response = client.get('/api/payments/runtime')
assert response.status_code == 200
payload = response.json()
assert 'checkout' in payload
assert 'rpc' in payload
assert 'event_loop_state' in payload
assert 'recent_audit_events' in payload
def test_auth_me_auto_reconciles_missing_subscription(monkeypatch):
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
def _bind_identity(request):
request.state.auth_user_id = "user-1"
request.state.auth_email = "user@example.com"
monkeypatch.setattr(routes, "_bind_optional_supabase_identity", _bind_identity)
monkeypatch.setattr(routes, "_resolve_auth_points", lambda request: 0)
monkeypatch.setattr(routes, "_resolve_weekly_profile", lambda request: {"weekly_points": 0, "weekly_rank": None})
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "enabled", True)
calls = {"count": 0}
def _latest_subscription(user_id, respect_requirement=False):
calls["count"] += 1
if calls["count"] == 1:
return None
return {
"plan_code": "pro_monthly",
"starts_at": "2026-03-22T00:00:00+00:00",
"expires_at": "2026-04-21T00:00:00+00:00",
}
monkeypatch.setattr(
routes.SUPABASE_ENTITLEMENT,
"get_latest_active_subscription",
_latest_subscription,
)
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "enabled", True)
monkeypatch.setattr(
routes.PAYMENT_CHECKOUT,
"reconcile_latest_intent",
lambda user_id: {"ok": True, "action": "reconciled_confirmed_intent"},
)
response = client.get("/api/auth/me")
assert response.status_code == 200
payload = response.json()
assert payload["subscription_active"] is True
assert payload["subscription_plan_code"] == "pro_monthly"
def test_ops_memberships_prefers_supabase_auth_email(monkeypatch):
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "enabled", False)
class _FakeDB:
@staticmethod
def get_users_by_supabase_user_ids(user_ids):
return {
"user-1": {
"supabase_email": "stale@example.com",
"username": "tester",
"telegram_id": 1,
"created_at": "2026-03-01T00:00:00+00:00",
}
}
import src.database.db_manager as db_module
monkeypatch.setattr(db_module, "DBManager", lambda: _FakeDB())
monkeypatch.setattr(
routes.SUPABASE_ENTITLEMENT,
"list_active_subscriptions",
lambda limit=200: [
{
"user_id": "user-1",
"plan_code": "pro_monthly",
"starts_at": "2026-03-22T00:00:00+00:00",
"expires_at": "2026-04-21T00:00:00+00:00",
}
],
)
monkeypatch.setattr(
routes.SUPABASE_ENTITLEMENT,
"get_auth_users",
lambda user_ids: {
"user-1": {
"email": "fresh@example.com",
"created_at": "2026-03-02T00:00:00+00:00",
}
},
)
response = client.get("/api/ops/memberships")
assert response.status_code == 200
payload = response.json()
assert payload["memberships"][0]["email"] == "fresh@example.com"
def test_ops_truth_history_returns_filtered_rows(monkeypatch):
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
repo = TruthRecordRepository()
repo.upsert_truth(
city="taipei",
target_date="2026-04-02",
actual_high=26.0,
settlement_source="wunderground",
settlement_station_code="RCSS",
settlement_station_label="Taipei Songshan Airport Station",
truth_version="v1",
updated_by="test",
source_payload={"sample": True},
is_final=True,
)
response = client.get("/api/ops/truth-history?city=taipei&date_from=2026-04-01&date_to=2026-04-03&limit=10")
assert response.status_code == 200
payload = response.json()
assert "items" in payload
assert payload["filters"]["city"] == "taipei"
assert payload["items"][0]["city"] == "taipei"
assert payload["items"][0]["settlement_station_code"] == "RCSS"
def test_city_history_is_read_only_and_uses_sqlite_truth_and_features(monkeypatch):
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
def _fail_bootstrap(*args, **kwargs):
raise AssertionError("bootstrap should not be called by /api/history")
monkeypatch.setattr(routes, "bootstrap_recent_daily_history_if_missing", _fail_bootstrap, raising=False)
truth_repo = TruthRecordRepository()
truth_repo.upsert_truth(
city="ankara",
target_date="2026-04-02",
actual_high=16.0,
settlement_source="metar",
settlement_station_code="LTAC",
settlement_station_label="Ankara Esenboga Airport",
truth_version="v1",
updated_by="test",
source_payload={"sample": True},
is_final=True,
)
feature_repo = TrainingFeatureRecordRepository()
feature_repo.upsert_record(
"ankara",
"2026-04-02",
{
"deb_prediction": 16.4,
"mu": 16.2,
"forecasts": {"Open-Meteo": 15.8, "ECMWF": 16.1},
},
)
response = client.get("/api/history/ankara")
assert response.status_code == 200
payload = response.json()
assert payload["history"]
row = next(item for item in payload["history"] if item["date"] == "2026-04-02")
assert row["actual"] == 16.0
assert row["deb"] == 16.4
assert row["mu"] == 16.2
assert row["forecasts"]["Open-Meteo"] == 15.8
assert row["settlement_station_code"] == "LTAC"
assert row["truth_version"] == "v1"