diff --git a/backend/alertsystem.py b/backend/alertsystem.py index bde3472..9345955 100644 --- a/backend/alertsystem.py +++ b/backend/alertsystem.py @@ -45,6 +45,7 @@ def fetch_gis_alert_data(): ) from flask_cors import CORS +from backend.risk_calculator import calculate_risk_scores, get_detailed_risks, generate_alerts # ========================================================= # APP CONFIG @@ -308,98 +309,13 @@ def get_weather_insights(): # RISK CALCULATIONS # ---------------------------------------------------- - flood_risk_metric = round( - min( - 1.0, - ( - rain_val * 0.6 + - humid_val * 0.3 + - wind_val * 0.1 - ) / 100 - ), - 3 - ) - - heat_risk_metric = round( - min( - 1.0, - ( - max(temp_val - 25, 0) * 2 + - humid_val * 0.3 - ) / 100 - ), - 3 - ) - - wildfire_risk_metric = round( - min( - 1.0, - ( - max(temp_val - 32, 0) * 1.5 + - (100 - humid_val) * 0.5 + - wind_val * 0.2 - ) / 100 - ), - 3 - ) - - cyclone_risk_metric = round( - min( - 1.0, - ( - wind_val * 1.5 + - rain_val * 0.5 - ) / 100 - ), - 3 - ) - - drought_risk_metric = round( - min( - 1.0, - ( - max(temp_val - 28, 0) + - (100 - humid_val) - ) / 100 - ), - 3 - ) + current_risk_scores = calculate_risk_scores(temp_val, humid_val, wind_val, rain_val) # ---------------------------------------------------- # ALERTS # ---------------------------------------------------- - calculated_alerts = [] - - if flood_risk_metric >= 0.6: - calculated_alerts.append( - "⚠ High Flood Risk Detected" - ) - - if heat_risk_metric >= 0.6: - calculated_alerts.append( - "🔥 Heatwave Conditions Possible" - ) - - if wildfire_risk_metric >= 0.6: - calculated_alerts.append( - "🌲 Elevated Wildfire Risk" - ) - - if cyclone_risk_metric >= 0.6: - calculated_alerts.append( - "🌀 Cyclone Risk Detected" - ) - - if drought_risk_metric >= 0.6: - calculated_alerts.append( - "☀ Drought Conditions Possible" - ) - - if not calculated_alerts: - calculated_alerts.append( - "✅ No major climate threats detected." - ) + calculated_alerts = generate_alerts(current_risk_scores) # ---------------------------------------------------- # FORECAST GENERATION @@ -430,60 +346,7 @@ def get_weather_insights(): "humidity": day_humidity, "rainfall": round(day_rain, 1), "wind_speed": day_wind, - "risks": { - "flood_risk": round( - min( - 1.0, - ( - day_rain * 0.6 + - day_humidity * 0.3 + - day_wind * 0.1 - ) / 100 - ), - 3 - ), - "heat_risk": round( - min( - 1.0, - ( - max(day_temp - 25, 0) * 2 + - day_humidity * 0.3 - ) / 100 - ), - 3 - ), - "wildfire_risk": round( - min( - 1.0, - ( - max(day_temp - 32, 0) * 1.5 + - (100 - day_humidity) * 0.5 + - day_wind * 0.2 - ) / 100 - ), - 3 - ), - "cyclone_risk": round( - min( - 1.0, - ( - day_wind * 1.5 + - day_rain * 0.5 - ) / 100 - ), - 3 - ), - "drought_risk": round( - min( - 1.0, - ( - max(day_temp - 28, 0) + - (100 - day_humidity) - ) / 100 - ), - 3 - ) - } + "risks": calculate_risk_scores(day_temp, day_humidity, day_wind, day_rain) }) return jsonify({ @@ -505,23 +368,7 @@ def get_weather_insights(): "wind_speed": wind_val }, - "risks": { - "flood_risk": round(flood_risk_metric, 3), - "flood_risk_confidence": round(flood_risk_metric * 100, 1), - "flood_risk_level": "HIGH" if flood_risk_metric >= 0.6 else "MEDIUM" if flood_risk_metric >= 0.3 else "LOW", - "heat_risk": round(heat_risk_metric, 3), - "heat_risk_confidence": round(heat_risk_metric * 100, 1), - "heat_risk_level": "HIGH" if heat_risk_metric >= 0.6 else "MEDIUM" if heat_risk_metric >= 0.3 else "LOW", - "wildfire_risk": round(wildfire_risk_metric, 3), - "wildfire_risk_confidence": round(wildfire_risk_metric * 100, 1), - "wildfire_risk_level": "HIGH" if wildfire_risk_metric >= 0.6 else "MEDIUM" if wildfire_risk_metric >= 0.3 else "LOW", - "cyclone_risk": round(cyclone_risk_metric, 3), - "cyclone_risk_confidence": round(cyclone_risk_metric * 100, 1), - "cyclone_risk_level": "HIGH" if cyclone_risk_metric >= 0.6 else "MEDIUM" if cyclone_risk_metric >= 0.3 else "LOW", - "drought_risk": round(drought_risk_metric, 3), - "drought_risk_confidence": round(drought_risk_metric * 100, 1), -"drought_risk_level": "HIGH" if drought_risk_metric >= 0.6 else "MEDIUM" if drought_risk_metric >= 0.3 else "LOW", - }, + "risks": get_detailed_risks(current_risk_scores), "forecast": forecast, diff --git a/backend/conftest.py b/backend/conftest.py new file mode 100644 index 0000000..d39e3c6 --- /dev/null +++ b/backend/conftest.py @@ -0,0 +1,55 @@ +import pytest +from backend.alertsystem import app + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client + +@pytest.fixture +def mock_openweather_geo(): + return [ + { + "name": "Delhi", + "lat": 28.6139, + "lon": 77.2090, + "country": "IN", + "state": "Delhi" + } + ] + +@pytest.fixture +def mock_openweather_weather(): + return { + "main": { + "temp": 35.0, + "humidity": 40 + }, + "wind": { + "speed": 5.0 + }, + "rain": { + "1h": 0 + } + } + +@pytest.fixture +def mock_openweather_forecast(): + return { + "list": [ + { + "dt_txt": "2023-10-01 12:00:00", + "main": { + "temp": 36.0, + "humidity": 45 + }, + "wind": { + "speed": 4.5 + }, + "rain": { + "3h": 0 + } + } + ] * 40 # Just enough items to simulate API response + } diff --git a/backend/risk_calculator.py b/backend/risk_calculator.py new file mode 100644 index 0000000..c26bca2 --- /dev/null +++ b/backend/risk_calculator.py @@ -0,0 +1,46 @@ +def calculate_risk_scores(temp_val, humid_val, wind_val, rain_val): + flood_risk_metric = round(min(1.0, (rain_val * 0.6 + humid_val * 0.3 + wind_val * 0.1) / 100), 3) + heat_risk_metric = round(min(1.0, (max(temp_val - 25, 0) * 2 + humid_val * 0.3) / 100), 3) + wildfire_risk_metric = round(min(1.0, (max(temp_val - 32, 0) * 1.5 + (100 - humid_val) * 0.5 + wind_val * 0.2) / 100), 3) + cyclone_risk_metric = round(min(1.0, (wind_val * 1.5 + rain_val * 0.5) / 100), 3) + drought_risk_metric = round(min(1.0, (max(temp_val - 28, 0) + (100 - humid_val)) / 100), 3) + + return { + "flood_risk": flood_risk_metric, + "heat_risk": heat_risk_metric, + "wildfire_risk": wildfire_risk_metric, + "cyclone_risk": cyclone_risk_metric, + "drought_risk": drought_risk_metric, + } + +def get_risk_level(metric): + if metric >= 0.6: + return "HIGH" + elif metric >= 0.3: + return "MEDIUM" + return "LOW" + +def get_detailed_risks(scores): + details = {} + for risk_type, metric in scores.items(): + details[risk_type] = metric + details[f"{risk_type}_confidence"] = round(metric * 100, 1) + details[f"{risk_type}_level"] = get_risk_level(metric) + return details + +def generate_alerts(scores): + alerts = [] + if scores["flood_risk"] >= 0.6: + alerts.append("⚠ High Flood Risk Detected") + if scores["heat_risk"] >= 0.6: + alerts.append("🔥 Heatwave Conditions Possible") + if scores["wildfire_risk"] >= 0.6: + alerts.append("🌲 Elevated Wildfire Risk") + if scores["cyclone_risk"] >= 0.6: + alerts.append("🌀 Cyclone Risk Detected") + if scores["drought_risk"] >= 0.6: + alerts.append("☀ Drought Conditions Possible") + + if not alerts: + alerts.append("✅ No major climate threats detected.") + return alerts diff --git a/backend/test_chatbot_api.py b/backend/test_chatbot_api.py new file mode 100644 index 0000000..ee561ab --- /dev/null +++ b/backend/test_chatbot_api.py @@ -0,0 +1,61 @@ +import pytest +import json + +def test_chatbot_api_missing_message(client): + response = client.post("/chatbot", json={}) + assert response.status_code == 400 + data = response.get_json() + assert data["success"] is False + assert "Please provide a message" in data["message"] + +def test_chatbot_api_greeting(client): + response = client.post("/chatbot", json={"message": "hello"}) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + assert "response" in data + assert any(phrase in data["response"] for phrase in ["Hello", "Hi", "Greetings"]) + +def test_chatbot_api_knowledge_base(client): + response = client.post("/chatbot", json={"message": "what is a flood?"}) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + assert "flood" in data["response"].lower() or "submerge" in data["response"].lower() + +def test_chatbot_api_with_context(client): + payload = { + "message": "is it safe?", + "context": { + "location": {"city": "Mumbai", "state": "MH", "country": "IN"}, + "weather": {"temperature": 32, "humidity": 85, "rainfall": 120, "wind_speed": 15}, + "risks": { + "flood_risk": 0.85, + "heat_risk": 0.2, + "wildfire_risk": 0.05, + "cyclone_risk": 0.3, + "drought_risk": 0.1 + } + } + } + response = client.post("/chatbot", json=payload) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + # The chatbot should mention the highest risk (Flood) based on context + assert "Flood" in data["response"] + assert "0.850" in data["response"] + +def test_chatbot_api_unknown_topic(client): + response = client.post("/chatbot", json={"message": "how do I cook pasta?"}) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + # The chatbot should gracefully handle unknown topic + assert isinstance(data["response"], str) and len(data["response"]) > 0 + +def test_chatbot_api_empty_payload(client): + response = client.post("/chatbot", data="") + assert response.status_code == 400 + data = response.get_json() + assert data["success"] is False diff --git a/backend/test_risk_calculator.py b/backend/test_risk_calculator.py new file mode 100644 index 0000000..845b080 --- /dev/null +++ b/backend/test_risk_calculator.py @@ -0,0 +1,96 @@ +import pytest +from backend.risk_calculator import calculate_risk_scores, get_risk_level, get_detailed_risks, generate_alerts + +def test_calculate_risk_scores_normal(): + # Moderate temperature, normal humidity, low wind, no rain + scores = calculate_risk_scores(temp_val=25, humid_val=50, wind_val=10, rain_val=0) + + assert 0 <= scores["flood_risk"] <= 1.0 + assert 0 <= scores["heat_risk"] <= 1.0 + assert 0 <= scores["wildfire_risk"] <= 1.0 + assert 0 <= scores["cyclone_risk"] <= 1.0 + assert 0 <= scores["drought_risk"] <= 1.0 + + # Check specific values based on formula + # heat_risk = max(25-25,0)*2 + 50*0.3 / 100 = 15/100 = 0.15 + assert scores["heat_risk"] == 0.15 + +def test_calculate_risk_scores_extreme_heat(): + # High temp, low humidity + scores = calculate_risk_scores(temp_val=45, humid_val=10, wind_val=5, rain_val=0) + + # heat_risk = (max(45-25,0)*2 + 10*0.3)/100 = (40 + 3)/100 = 0.43 + assert scores["heat_risk"] == 0.43 + + # wildfire_risk = (max(45-32)*1.5 + 90*0.5 + 5*0.2)/100 = (19.5 + 45 + 1)/100 = 0.655 + assert scores["wildfire_risk"] == 0.655 + +def test_calculate_risk_scores_extreme_flood(): + # Heavy rain, high wind + scores = calculate_risk_scores(temp_val=20, humid_val=95, wind_val=50, rain_val=150) + + # flood = (150*0.6 + 95*0.3 + 50*0.1)/100 = (90 + 28.5 + 5)/100 = 1.235 -> capped at 1.0 + assert scores["flood_risk"] == 1.0 + + # cyclone = (50*1.5 + 150*0.5)/100 = (75 + 75)/100 = 1.5 -> capped at 1.0 + assert scores["cyclone_risk"] == 1.0 + +def test_calculate_risk_scores_extreme_drought(): + # Very high temp, extremely low humidity, zero rain + scores = calculate_risk_scores(temp_val=40, humid_val=5, wind_val=20, rain_val=0) + + # drought = (max(40-28, 0) + (100 - 5)) / 100 = (12 + 95)/100 = 1.07 -> capped at 1.0 + assert scores["drought_risk"] == 1.0 + +def test_calculate_risk_scores_extreme_cold(): + # Cold temperatures should yield 0 for heat and wildfire risk contribution from temp + scores = calculate_risk_scores(temp_val=-10, humid_val=80, wind_val=15, rain_val=0) + + assert scores["heat_risk"] == 0.24 # (max(-10-25, 0)*2 + 80*0.3)/100 = (0 + 24)/100 = 0.24 + assert scores["wildfire_risk"] == 0.13 # (max(-10-32, 0)*1.5 + 20*0.5 + 15*0.2)/100 = (0 + 10 + 3)/100 = 0.13 + assert scores["drought_risk"] == 0.2 # (max(-10-28, 0) + 20)/100 = 0.2 + +def test_get_risk_level(): + assert get_risk_level(0.1) == "LOW" + assert get_risk_level(0.3) == "MEDIUM" + assert get_risk_level(0.59) == "MEDIUM" + assert get_risk_level(0.6) == "HIGH" + assert get_risk_level(1.0) == "HIGH" + +def test_get_detailed_risks(): + scores = {"flood_risk": 0.65, "heat_risk": 0.2} + detailed = get_detailed_risks(scores) + + assert detailed["flood_risk"] == 0.65 + assert detailed["flood_risk_confidence"] == 65.0 + assert detailed["flood_risk_level"] == "HIGH" + + assert detailed["heat_risk"] == 0.2 + assert detailed["heat_risk_confidence"] == 20.0 + assert detailed["heat_risk_level"] == "LOW" + +def test_generate_alerts(): + # High risk + high_risk_scores = { + "flood_risk": 0.8, + "heat_risk": 0.2, + "wildfire_risk": 0.1, + "cyclone_risk": 0.9, + "drought_risk": 0.0 + } + alerts = generate_alerts(high_risk_scores) + assert len(alerts) == 2 + assert "⚠ High Flood Risk Detected" in alerts + assert "🌀 Cyclone Risk Detected" in alerts + + # Low risk + low_risk_scores = { + "flood_risk": 0.1, + "heat_risk": 0.1, + "wildfire_risk": 0.1, + "cyclone_risk": 0.1, + "drought_risk": 0.1 + } + alerts_low = generate_alerts(low_risk_scores) + assert len(alerts_low) == 1 + assert "✅ No major climate threats detected." in alerts_low diff --git a/backend/test_weather_api.py b/backend/test_weather_api.py new file mode 100644 index 0000000..c796fbb --- /dev/null +++ b/backend/test_weather_api.py @@ -0,0 +1,101 @@ +import pytest +from unittest.mock import patch +import json +import os +import requests + +def test_weather_api_missing_fields(client): + response = client.post("/weather", json={"city": "Delhi"}) + assert response.status_code == 400 + data = response.get_json() + assert data["success"] is False + assert "Please fill all fields" in data["message"] + +@patch.dict(os.environ, {}, clear=True) +def test_weather_api_missing_api_key(client): + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + assert response.status_code == 500 + data = response.get_json() + assert data["success"] is False + assert "Weather service configuration error" in data["message"] + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_success(mock_get, client, mock_openweather_geo, mock_openweather_weather, mock_openweather_forecast): + class MockResponse: + def __init__(self, json_data, status_code=200): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + def side_effect(*args, **kwargs): + url = args[0] + if "geo/1.0/direct" in url: + return MockResponse(mock_openweather_geo) + elif "data/2.5/weather" in url: + return MockResponse(mock_openweather_weather) + elif "data/2.5/forecast" in url: + return MockResponse(mock_openweather_forecast) + return MockResponse({}, 404) + + mock_get.side_effect = side_effect + + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + + assert response.status_code == 200 + data = response.get_json() + + assert data["success"] is True + assert data["location"]["city"] == "Delhi" + assert data["weather"]["temperature"] == 35.0 + assert "flood_risk" in data["risks"] + assert len(data["forecast"]) == 5 + assert len(data["alerts"]) > 0 + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_location_not_found(mock_get, client): + class MockResponse: + def __init__(self, json_data, status_code=200): + self.json_data = json_data + self.status_code = status_code + def json(self): + return self.json_data + def raise_for_status(self): + pass + + mock_get.return_value = MockResponse([]) # Empty list means not found + + response = client.post("/weather", json={"city": "FakeCity", "state": "FakeState", "country": "XX"}) + + assert response.status_code == 404 + data = response.get_json() + assert data["success"] is False + assert "Location not found" in data["message"] + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_timeout(mock_get, client): + mock_get.side_effect = requests.exceptions.Timeout("Connection timed out") + + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + + assert response.status_code == 500 + data = response.get_json() + assert data["success"] is False + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_connection_error(mock_get, client): + mock_get.side_effect = requests.exceptions.ConnectionError("Connection error") + + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + + assert response.status_code == 500 + data = response.get_json() + assert data["success"] is False