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_does_not_reconcile_on_status_probe(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} reconcile_calls = {"count": 0} def _latest_subscription(user_id, respect_requirement=False): calls["count"] += 1 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) def _reconcile_latest_intent(user_id): reconcile_calls["count"] += 1 return {"ok": True, "action": "reconciled_confirmed_intent"} monkeypatch.setattr( routes.PAYMENT_CHECKOUT, "reconcile_latest_intent", _reconcile_latest_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" assert reconcile_calls["count"] == 0 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"