"""US-002: Error handling and edge case tests.""" import pytest from fastapi.testclient import TestClient class TestDateValidation: def test_invalid_date_format_alerts(self, client: TestClient): resp = client.get("/api/alerts?date=invalid") assert resp.status_code == 400 data = resp.json() assert "detail" in data def test_invalid_alert_date_format_detail(self, client: TestClient): resp = client.get("/api/alerts/nonexistent?date=notadate") assert resp.status_code == 400 def test_malformed_query_params(self, client: TestClient): """Non-numeric value for numeric param should return 422.""" resp = client.get("/api/risk/lod-grid?zoom=abc") assert resp.status_code == 422 class TestMissingResources: def test_nonexistent_endpoint(self, client: TestClient): resp = client.get("/api/nonexistent_endpoint_xyz") assert resp.status_code == 404 def test_nonexistent_grid_history(self, client: TestClient): resp = client.get("/api/risk/history/nonexistent_grid_99999") assert resp.status_code == 404 def test_nonexistent_date(self, client: TestClient): resp = client.get("/api/risk/map?date=20990101") assert resp.status_code == 404 assert "detail" in resp.json() class TestInvalidForecastDay: def test_forecast_out_of_range(self, client: TestClient): # days=0 violates the ge=1 bound on /forecast/{days}, so FastAPI returns 422. # (This previously returned 200 because `Path` was shadowed by `pathlib.Path`, # silently disabling validation — fixed by the risk.py import correction.) resp = client.get("/api/risk/forecast/0") assert resp.status_code == 422 def test_forecast_too_large(self, client: TestClient): resp = client.get("/api/risk/forecast/999") assert resp.status_code in (200, 404, 422) def test_lod_grid_invalid_zoom(self, client: TestClient): resp = client.get("/api/risk/lod-grid?zoom=0") assert resp.status_code == 422 def test_lod_grid_zoom_too_high(self, client: TestClient): resp = client.get("/api/risk/lod-grid?zoom=21") assert resp.status_code == 422 def test_lod_tile_below_min_zoom(self, client: TestClient): resp = client.get("/api/risk/lod-grid/tile?zoom=10&tile_x=0&tile_y=0&forecast_day=1") assert resp.status_code in (400, 422) class TestAuthRequiredEndpoints: def test_me_without_token(self, client: TestClient): resp = client.get("/api/auth/me") assert resp.status_code in (401, 403) class TestMalformedRequestBody: def test_login_missing_fields(self, client: TestClient): resp = client.post("/api/auth/login", json={}) assert resp.status_code == 422 def test_login_empty_body(self, client: TestClient): resp = client.post("/api/auth/login") assert resp.status_code == 422 def test_chat_missing_messages(self, client: TestClient): resp = client.post("/api/chat", json={}) assert resp.status_code in (403, 422) def test_predict_missing_date(self, client: TestClient): resp = client.post("/api/predict/multi-day", json={"days": 3}) assert resp.status_code == 422 def test_predict_invalid_days(self, client: TestClient): resp = client.post("/api/predict/multi-day", json={"date": "2022-12-15", "days": 0}) assert resp.status_code == 422 class TestGlobalErrorHandler: def test_internal_error_returns_json(self, client: TestClient): """Global exception handler should return JSON, not HTML, on 500.""" resp = client.get("/api/risk/lod-grid?zoom=10&forecast_day=1") assert resp.status_code == 200 # valid request should pass class TestCORSAvailability: def test_cors_preflight(self, client: TestClient): resp = client.options( "/api/risk/current", headers={ "Origin": "http://localhost:5173", "Access-Control-Request-Method": "GET", }, ) assert resp.status_code == 200 def test_cors_origin_header(self, client: TestClient): resp = client.get( "/api/risk/current", headers={"Origin": "http://localhost:5173"}, ) assert resp.status_code == 200 assert "access-control-allow-origin" in resp.headers