discordBot/tests/test_fetch_weather.py

221 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pytest
import requests
from requests.exceptions import ConnectionError, Timeout, SSLError
from unittest.mock import patch, MagicMock
from utils.pogoda import fetch_weather, fetch_open_meteo
class TestFetchWeather:
"""Тесты функции fetch_weather() — получение погоды с retry-логикой."""
@patch("utils.pogoda._session.get")
async def test_fetch_weather_success(self, mock_get) -> None:
"""Успешный ответ должен вернуть JSON-данные."""
mock_response = MagicMock()
mock_response.json.return_value = {"current_condition": [{"temp_C": 20}]}
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = await fetch_weather("https://test.example.com")
assert result == {"current_condition": [{"temp_C": 20}]}
@patch("utils.pogoda._session.get")
async def test_fetch_weather_fallback_on_ssl_error(self, mock_get) -> None:
"""SSLError на первой попытке → fallback на Open-Meteo."""
mock_get.side_effect = [
SSLError("SSL Error"),
MagicMock(json=MagicMock(return_value={"result": "fallback"})),
]
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
mock_fallback.return_value = {"result": "fallback"}
result = await fetch_weather("https://test.example.com")
assert result == {"result": "fallback"}
@patch("utils.pogoda._session.get")
async def test_fetch_weather_fallback_on_connection_error(self, mock_get) -> None:
"""ConnectionError → fallback на Open-Meteo."""
mock_get.side_effect = ConnectionError("No connection")
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
mock_fallback.return_value = {"result": "fallback"}
result = await fetch_weather("https://test.example.com")
assert result == {"result": "fallback"}
@patch("utils.pogoda._session.get")
async def test_fetch_weather_fallback_on_timeout(self, mock_get) -> None:
"""Timeout → fallback на Open-Meteo."""
mock_get.side_effect = Timeout("Timed out")
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
mock_fallback.return_value = {"result": "fallback"}
result = await fetch_weather("https://test.example.com")
assert result == {"result": "fallback"}
@patch("utils.pogoda._session.get")
async def test_fetch_weather_all_retries_fail(self, mock_get) -> None:
"""Все попытки не удались → fallback на Open-Meteo."""
mock_get.side_effect = ConnectionError("No connection")
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
mock_fallback.return_value = None
result = await fetch_weather("https://test.example.com")
assert result is None
@patch("utils.pogoda._session.get")
async def test_fetch_weather_request_exception(self, mock_get) -> None:
"""Общий RequestException → fallback на Open-Meteo."""
mock_get.side_effect = requests.RequestException("Generic error")
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
mock_fallback.return_value = {"result": "fallback"}
result = await fetch_weather("https://test.example.com")
assert result == {"result": "fallback"}
@patch("utils.pogoda._session.get")
async def test_fetch_weather_http_error_no_fallback(self, mock_get) -> None:
"""HTTP-ошибка (raise_for_status) не ловится, падает."""
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
mock_get.return_value = mock_response
with pytest.raises(Exception):
await fetch_weather("https://test.example.com")
class TestFetchOpenMeteo:
"""Тесты функции fetch_open_meteo() — fallback на Open-Meteo API."""
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_success(self, mock_get) -> None:
"""Успешный ответ должен вернуть данные в формате current_condition."""
mock_response = MagicMock()
mock_response.json.return_value = {
"current": {
"temperature": 15,
"apparent_temperature": 12,
"weather_code": 3,
"wind_speed_10m": 5.5,
"relative_humidity_2m": 65,
"pressure_msl": 1013,
}
}
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = await fetch_open_meteo()
assert result is not None
assert "current_condition" in result
assert result["current_condition"][0]["temp_C"] == 15
assert result["current_condition"][0]["FeelsLikeC"] == 12
assert result["current_condition"][0]["humidity"] == 65
assert result["current_condition"][0]["pressure"] == 1013
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_custom_coords(self, mock_get) -> None:
"""Кастомные координаты должны быть в URL."""
mock_response = MagicMock()
mock_response.json.return_value = {"current": {"temperature": 25, "apparent_temperature": 22, "weather_code": 0, "wind_speed_10m": 3, "relative_humidity_2m": 50, "pressure_msl": 1020}}
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = await fetch_open_meteo(lat=55.7558, lon=37.6173)
assert result is not None
mock_get.assert_called_once()
call_url = mock_get.call_args[0][0]
assert "55.7558" in call_url
assert "37.6173" in call_url
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_missing_weather_code(self, mock_get) -> None:
"""Отсутствующий weather_code → 'Неизвестно'."""
mock_response = MagicMock()
mock_response.json.return_value = {"current": {"temperature": 10}}
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = await fetch_open_meteo()
assert result is not None
assert result["current_condition"][0]["weatherDesc"] == [{"value": "Неизвестно"}]
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_ssl_error(self, mock_get) -> None:
"""SSLError → вернуть None."""
mock_get.side_effect = SSLError("SSL Error")
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
# Внутренний fallback тоже падает, проверяем что возвращается None
pass
result = await fetch_open_meteo()
assert result is None
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_connection_error(self, mock_get) -> None:
"""ConnectionError → вернуть None."""
mock_get.side_effect = ConnectionError("No connection")
result = await fetch_open_meteo()
assert result is None
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_timeout(self, mock_get) -> None:
"""Timeout → вернуть None."""
mock_get.side_effect = Timeout("Timed out")
result = await fetch_open_meteo()
assert result is None
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_request_exception(self, mock_get) -> None:
"""Общий RequestException → вернуть None."""
mock_get.side_effect = requests.RequestException("Error")
result = await fetch_open_meteo()
assert result is None
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_json_parse_error(self, mock_get) -> None:
"""Ошибка парсинга JSON → вернуть None."""
mock_response = MagicMock()
mock_response.json.side_effect = requests.RequestException("JSON Error")
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = await fetch_open_meteo()
assert result is None
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_retry_on_error(self, mock_get) -> None:
"""Retry: первая попытка падает, вторая успешна."""
success_response = MagicMock()
success_response.json.return_value = {"current": {"temperature": 20, "apparent_temperature": 18, "weather_code": 1, "wind_speed_10m": 4, "relative_humidity_2m": 60, "pressure_msl": 1015}}
success_response.raise_for_status = MagicMock()
mock_get.side_effect = [ConnectionError("fail"), success_response]
result = await fetch_open_meteo(max_retries=2)
assert result is not None
assert mock_get.call_count == 2
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_all_retries_fail(self, mock_get) -> None:
"""Все попытки неудачны → None."""
mock_get.side_effect = [ConnectionError("fail"), ConnectionError("fail"), ConnectionError("fail")]
result = await fetch_open_meteo(max_retries=3)
assert result is None
assert mock_get.call_count == 3
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_http_error(self, mock_get) -> None:
"""HTTP 404 → raise_for_status бросит исключение → None."""
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.HTTPError("HTTP 404")
mock_get.return_value = mock_response
result = await fetch_open_meteo()
assert result is None
@patch("utils.pogoda._session.get")
async def test_fetch_open_meteo_wind_speed_0(self, mock_get) -> None:
"""Нулевая скорость ветра должна корректно обрабатываться."""
mock_response = MagicMock()
mock_response.json.return_value = {
"current": {
"temperature": 0,
"apparent_temperature": -2,
"weather_code": 45,
"wind_speed_10m": 0,
"relative_humidity_2m": 95,
"pressure_msl": 1000,
}
}
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = await fetch_open_meteo()
assert result is not None
assert result["current_condition"][0]["windspeedKmph"] == 0
assert result["current_condition"][0]["pressure"] == 1000