Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 310 additions & 0 deletions weather-app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
import requests

def get_temperature(city_name, api_key):
"""
Fetches the current temperature in Fahrenheit for a given U.S. city
using the OpenWeatherMap API.

Parameters:
city_name (str): The name of the U.S. city.
api_key (str): Your OpenWeatherMap API key.

Returns:
float: Current temperature in Fahrenheit if successful.
None: If an error occurs or the data cannot be retrieved.
"""
if not api_key or not isinstance(api_key, str):
print("Error: Invalid or missing API key.")
return None

base_url = "https://api.openweathermap.org/data/2.5/weather"
params = {
'q': f"{city_name},US",
'appid': api_key,
'units': 'imperial'
}

try:
response = requests.get(base_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()

if 'main' in data and 'temp' in data['main']:
return data['main']['temp']
else:
print("Error: Unexpected response structure.")
return None
except requests.exceptions.HTTPError as http_err:
print(f"HTTP error occurred: {http_err}")
except requests.exceptions.ConnectionError:
print("Error: Network connection error.")
except requests.exceptions.Timeout:
print("Error: The request timed out.")
except requests.exceptions.RequestException as err:
print(f"Error: An unexpected error occurred: {err}")
Comment on lines +37 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

HTTP error logging may leak the OpenWeather API key

except requests.exceptions.HTTPError as http_err: followed by:

print(f"HTTP error occurred: {http_err}")

will typically include the full request URL in the exception message, which for OpenWeather contains the appid query parameter (your API key). This risks exposing the secret in logs/terminal output.

Consider logging only the status code and reason (without the full URL) or redacting the query string before printing.

🤖 Prompt for AI Agents
In weather-app.py around lines 37 to 44, the HTTP error printout may expose the
OpenWeather API key because the exception message can include the full request
URL; replace the current print of the raw exception with code that avoids
leaking the URL by either (a) extracting and logging only
http_err.response.status_code and http_err.response.reason (or response.text if
needed) or (b) if you must log the URL, parse and redact the query string before
printing; ensure you reference http_err.response safely (check for None) and
never include the full request URL or query parameters in logs.

except ValueError:
print("Error: Failed to parse JSON response.")

return None

def main():
import os
api_key = os.getenv('OPENWEATHER_API_KEY')
if not api_key:
print("Error: Please set the OPENWEATHER_API_KEY environment variable")
return

city = input("Enter a major U.S. city: ").strip()
if not city:
print("Error: City name cannot be empty")
return

temp = get_temperature(city, api_key)
if temp is not None:
print(f"The current temperature in {city.title()} is {temp:.1f}°F.")
else:
print("Sorry, couldn't find weather data for that city.")

if __name__ == "__main__":
main()


import unittest
from unittest.mock import patch, Mock, MagicMock
import requests
from weather-app import get_temperature, main


class TestGetTemperature(unittest.TestCase):
"""Test cases for the get_temperature function."""

@patch('weather-app.requests.get')
def test_successful_temperature_fetch(self, mock_get):
"""Test successful temperature retrieval."""
# Mock successful API response
mock_response = Mock()
mock_response.json.return_value = {
'main': {'temp': 72.5}
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response

result = get_temperature("New York", "valid_api_key")

self.assertEqual(result, 72.5)
mock_get.assert_called_once_with(
"https://api.openweathermap.org/data/2.5/weather",
params={'q': 'New York,US', 'appid': 'valid_api_key', 'units': 'imperial'},
timeout=10
)

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_invalid_api_key_none(self, mock_print, mock_get):
"""Test with None API key."""
result = get_temperature("Chicago", None)

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: Invalid or missing API key.")
mock_get.assert_not_called()

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_invalid_api_key_empty_string(self, mock_print, mock_get):
"""Test with empty string API key."""
result = get_temperature("Chicago", "")

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: Invalid or missing API key.")
mock_get.assert_not_called()

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_invalid_api_key_non_string(self, mock_print, mock_get):
"""Test with non-string API key."""
result = get_temperature("Chicago", 12345)

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: Invalid or missing API key.")
mock_get.assert_not_called()

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_http_error_401_unauthorized(self, mock_print, mock_get):
"""Test HTTP 401 Unauthorized error."""
mock_response = Mock()
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Unauthorized")
mock_get.return_value = mock_response

result = get_temperature("Boston", "invalid_key")

self.assertIsNone(result)
mock_print.assert_called_once()
self.assertIn("HTTP error occurred", str(mock_print.call_args))

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_http_error_404_not_found(self, mock_print, mock_get):
"""Test HTTP 404 Not Found error."""
mock_response = Mock()
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Not Found")
mock_get.return_value = mock_response

result = get_temperature("InvalidCity", "valid_key")

self.assertIsNone(result)
mock_print.assert_called_once()
self.assertIn("HTTP error occurred", str(mock_print.call_args))

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_connection_error(self, mock_print, mock_get):
"""Test network connection error."""
mock_get.side_effect = requests.exceptions.ConnectionError("Network unreachable")

result = get_temperature("Seattle", "valid_key")

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: Network connection error.")

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_timeout_error(self, mock_print, mock_get):
"""Test request timeout."""
mock_get.side_effect = requests.exceptions.Timeout("Request timed out")

result = get_temperature("Miami", "valid_key")

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: The request timed out.")

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_generic_request_exception(self, mock_print, mock_get):
"""Test generic request exception."""
mock_get.side_effect = requests.exceptions.RequestException("Unknown error")

result = get_temperature("Phoenix", "valid_key")

self.assertIsNone(result)
mock_print.assert_called_once()
self.assertIn("An unexpected error occurred", str(mock_print.call_args))

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_json_parsing_error(self, mock_print, mock_get):
"""Test JSON parsing error."""
mock_response = Mock()
mock_response.raise_for_status = Mock()
mock_response.json.side_effect = ValueError("Invalid JSON")
mock_get.return_value = mock_response

result = get_temperature("Denver", "valid_key")

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: Failed to parse JSON response.")

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_unexpected_response_structure_missing_main(self, mock_print, mock_get):
"""Test response with missing 'main' key."""
mock_response = Mock()
mock_response.json.return_value = {'weather': [{'description': 'clear sky'}]}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response

result = get_temperature("Atlanta", "valid_key")

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: Unexpected response structure.")

@patch('weather-app.requests.get')
@patch('builtins.print')
def test_unexpected_response_structure_missing_temp(self, mock_print, mock_get):
"""Test response with missing 'temp' key."""
mock_response = Mock()
mock_response.json.return_value = {'main': {'humidity': 65}}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response

result = get_temperature("Portland", "valid_key")

self.assertIsNone(result)
mock_print.assert_called_once_with("Error: Unexpected response structure.")


class TestMain(unittest.TestCase):
"""Test cases for the main function."""

@patch('weather-app.get_temperature')
@patch('builtins.input')
@patch('weather-app.os.getenv')
@patch('builtins.print')
def test_successful_flow(self, mock_print, mock_getenv, mock_input, mock_get_temp):
"""Test successful execution with valid inputs."""
mock_getenv.return_value = "test_api_key"
mock_input.return_value = " San Francisco "
mock_get_temp.return_value = 68.3

main()

mock_getenv.assert_called_once_with('OPENWEATHER_API_KEY')
mock_input.assert_called_once_with("Enter a major U.S. city: ")
mock_get_temp.assert_called_once_with("San Francisco", "test_api_key")
mock_print.assert_called_once_with("The current temperature in San Francisco is 68.3°F.")

@patch('weather-app.os.getenv')
@patch('builtins.print')
def test_missing_api_key_env_var(self, mock_print, mock_getenv):
"""Test when OPENWEATHER_API_KEY environment variable is not set."""
mock_getenv.return_value = None

main()

mock_getenv.assert_called_once_with('OPENWEATHER_API_KEY')
mock_print.assert_called_once_with("Error: Please set the OPENWEATHER_API_KEY environment variable")

@patch('builtins.input')
@patch('weather-app.os.getenv')
@patch('builtins.print')
def test_empty_city_name(self, mock_print, mock_getenv, mock_input):
"""Test when user enters empty city name."""
mock_getenv.return_value = "test_api_key"
mock_input.return_value = " "

main()

mock_print.assert_called_once_with("Error: City name cannot be empty")

@patch('weather-app.get_temperature')
@patch('builtins.input')
@patch('weather-app.os.getenv')
@patch('builtins.print')
def test_get_temperature_returns_none(self, mock_print, mock_getenv, mock_input, mock_get_temp):
"""Test when get_temperature fails and returns None."""
mock_getenv.return_value = "test_api_key"
mock_input.return_value = "InvalidCity"
mock_get_temp.return_value = None

main()

mock_get_temp.assert_called_once_with("InvalidCity", "test_api_key")
mock_print.assert_called_once_with("Sorry, couldn't find weather data for that city.")

@patch('weather-app.get_temperature')
@patch('builtins.input')
@patch('weather-app.os.getenv')
@patch('builtins.print')
def test_temperature_formatting(self, mock_print, mock_getenv, mock_input, mock_get_temp):
"""Test temperature output formatting with various values."""
mock_getenv.return_value = "test_api_key"
mock_input.return_value = "los angeles"
mock_get_temp.return_value = 75.678

main()

mock_print.assert_called_once_with("The current temperature in Los Angeles is 75.7°F.")


if __name__ == '__main__':
unittest.main()