"""Unit tests for the API class.""" import json from datetime import datetime import unittest from unittest.mock import Mock, patch from zoneinfo import ZoneInfo import pytest import requests from elprisenligenu.api import API from elprisenligenu.dto.hour_data import HourData class TestAPI(unittest.TestCase): """Test cases for the API class.""" api: API tz: ZoneInfo @classmethod def setUpClass(cls) -> None: """Set up test fixtures before any tests are run.""" cls.api = API() cls.tz = ZoneInfo("Europe/Copenhagen") def test_api_initialization(self) -> None: """Test API class initialization.""" # Act api = API() # Assert self.assertIsNotNone(api.session) self.assertIsInstance(api.session, requests.Session) self.assertIsNotNone(api.logger) self.assertEqual(api.BASE_URL, "https://www.elprisenligenu.dk/api/v1/prices") self.assertEqual(api.TIMEZONE, ZoneInfo("Europe/Copenhagen")) def test_endpoint_construction_dk1(self) -> None: """Test endpoint URL construction for DK1.""" # Arrange date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act endpoint = API._endpoint(date, "DK1") # Assert expected = "https://www.elprisenligenu.dk/api/v1/prices/2024/10-29_DK1.json" self.assertEqual(endpoint, expected) def test_endpoint_construction_dk2(self) -> None: """Test endpoint URL construction for DK2.""" # Arrange date = datetime(2024, 1, 5, 12, 0, 0, tzinfo=self.tz) # Act endpoint = API._endpoint(date, "DK2") # Assert expected = "https://www.elprisenligenu.dk/api/v1/prices/2024/01-05_DK2.json" self.assertEqual(endpoint, expected) def test_endpoint_construction_edge_dates(self) -> None: """Test endpoint URL construction for edge case dates.""" # Test December 31st date = datetime(2023, 12, 31, 12, 0, 0, tzinfo=self.tz) endpoint = API._endpoint(date, "DK1") expected = "https://www.elprisenligenu.dk/api/v1/prices/2023/12-31_DK1.json" self.assertEqual(endpoint, expected) # Test January 1st date = datetime(2024, 1, 1, 12, 0, 0, tzinfo=self.tz) endpoint = API._endpoint(date, "DK2") expected = "https://www.elprisenligenu.dk/api/v1/prices/2024/01-01_DK2.json" self.assertEqual(endpoint, expected) @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_success_dk1(self, mock_get: Mock) -> None: """Test successful API call for DK1.""" # Arrange mock_response_data = [ { "DKK_per_kWh": 1.3929, "EUR_per_kWh": 0.18729, "EXR": 7.437118, "time_start": "2024-10-29T00:00:00+01:00", "time_end": "2024-10-29T01:00:00+01:00", }, { "DKK_per_kWh": 1.30291, "EUR_per_kWh": 0.17519, "EXR": 7.437118, "time_start": "2024-10-29T01:00:00+01:00", "time_end": "2024-10-29T02:00:00+01:00", }, ] mock_response = Mock() mock_response.json.return_value = mock_response_data mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act result = self.api.get_daily_prices(zone="DK1", date=date) # Assert self.assertEqual(len(result), 2) self.assertTrue(all(isinstance(item, HourData) for item in result)) self.assertAlmostEqual(result[0].DKK_per_kWh, 1.3929, places=4) self.assertAlmostEqual(result[1].DKK_per_kWh, 1.30291, places=4) # Verify API was called correctly expected_url = "https://www.elprisenligenu.dk/api/v1/prices/2024/10-29_DK1.json" mock_get.assert_called_once_with(expected_url) @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_success_dk2(self, mock_get: Mock) -> None: """Test successful API call for DK2.""" # Arrange mock_response_data = [ { "DKK_per_kWh": 1.5000, "EUR_per_kWh": 0.2000, "EXR": 7.5, "time_start": "2024-10-29T00:00:00+01:00", "time_end": "2024-10-29T01:00:00+01:00", } ] mock_response = Mock() mock_response.json.return_value = mock_response_data mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act result = self.api.get_daily_prices(zone="DK2", date=date) # Assert self.assertEqual(len(result), 1) self.assertAlmostEqual(result[0].DKK_per_kWh, 1.5000, places=4) # Verify API was called correctly expected_url = "https://www.elprisenligenu.dk/api/v1/prices/2024/10-29_DK2.json" mock_get.assert_called_once_with(expected_url) def test_get_daily_prices_invalid_zone(self) -> None: """Test validation error for invalid zone.""" # Arrange date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act & Assert with pytest.raises(ValueError, match="zone must be either 'DK1' or 'DK2'"): self.api.get_daily_prices(zone="DK3", date=date) # type: ignore def test_get_daily_prices_invalid_date_type(self) -> None: """Test validation error for invalid date type.""" # Act & Assert with pytest.raises(TypeError, match="date must be a datetime object"): self.api.get_daily_prices(zone="DK1", date="2024-10-29") # type: ignore def test_get_daily_prices_naive_datetime(self) -> None: """Test validation error for naive datetime.""" # Arrange naive_date = datetime(2024, 10, 29, 12, 0, 0) # No timezone # Act & Assert with pytest.raises(ValueError, match="date must be timezone-aware"): self.api.get_daily_prices(zone="DK1", date=naive_date) @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_timezone_conversion(self, mock_get: Mock) -> None: """Test automatic timezone conversion.""" # Arrange mock_response_data = [ { "DKK_per_kWh": 1.0, "EUR_per_kWh": 0.13, "EXR": 7.5, "time_start": "2024-10-29T00:00:00+01:00", "time_end": "2024-10-29T01:00:00+01:00", } ] mock_response = Mock() mock_response.json.return_value = mock_response_data mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response # Use UTC timezone instead of Copenhagen utc_date = datetime(2024, 10, 29, 11, 0, 0, tzinfo=ZoneInfo("UTC")) # Act result = self.api.get_daily_prices(zone="DK1", date=utc_date) # Assert self.assertEqual(len(result), 1) # Should still work, date gets converted internally @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_http_error(self, mock_get: Mock) -> None: """Test handling of HTTP errors.""" # Arrange mock_response = Mock() mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found") mock_get.return_value = mock_response date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act & Assert with pytest.raises(requests.HTTPError): self.api.get_daily_prices(zone="DK1", date=date) @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_invalid_json(self, mock_get: Mock) -> None: """Test handling of invalid JSON response.""" # Arrange mock_response = Mock() mock_response.raise_for_status.return_value = None mock_response.json.side_effect = json.JSONDecodeError("Invalid JSON", "", 0) mock_get.return_value = mock_response date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act & Assert with pytest.raises(json.JSONDecodeError): self.api.get_daily_prices(zone="DK1", date=date) @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_empty_response(self, mock_get: Mock) -> None: """Test handling of empty response.""" # Arrange mock_response = Mock() mock_response.json.return_value = [] mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act result = self.api.get_daily_prices(zone="DK1", date=date) # Assert self.assertEqual(result, []) @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_malformed_data(self, mock_get: Mock) -> None: """Test handling of malformed response data.""" # Arrange mock_response_data = [ { "DKK_per_kWh": "not_a_number", # Invalid type "EUR_per_kWh": 0.18729, "EXR": 7.437118, "time_start": "2024-10-29T00:00:00+01:00", "time_end": "2024-10-29T01:00:00+01:00", } ] mock_response = Mock() mock_response.json.return_value = mock_response_data mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act & Assert with pytest.raises(Exception): # Pydantic validation error self.api.get_daily_prices(zone="DK1", date=date) def test_get_daily_prices_default_parameters(self) -> None: """Test that default parameters work correctly.""" # This test verifies the method signature but doesn't make real API calls # We just check that the method can be called without parameters # Act & Assert - should not raise an error for the method signature try: # We expect this to fail with a network error in tests, but the # parameter validation should pass self.api.get_daily_prices() except (requests.RequestException, requests.ConnectionError): # Expected in test environment without internet pass @patch("elprisenligenu.api.requests.Session.get") def test_get_daily_prices_full_day_data(self, mock_get: Mock) -> None: """Test processing a full day of 24 hours.""" # Arrange - Create 24 hours of mock data mock_response_data = [] for hour in range(24): mock_response_data.append( { "DKK_per_kWh": 1.0 + hour * 0.1, "EUR_per_kWh": 0.13 + hour * 0.01, "EXR": 7.5, "time_start": f"2024-10-29T{hour:02d}:00:00+01:00", "time_end": f"2024-10-29T{(hour + 1) % 24:02d}:00:00+01:00", } ) mock_response = Mock() mock_response.json.return_value = mock_response_data mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response date = datetime(2024, 10, 29, 12, 0, 0, tzinfo=self.tz) # Act result = self.api.get_daily_prices(zone="DK1", date=date) # Assert self.assertEqual(len(result), 24) self.assertTrue(all(isinstance(item, HourData) for item in result)) # Check that prices increase as expected self.assertEqual(result[0].DKK_per_kWh, 1.0) self.assertAlmostEqual(result[23].DKK_per_kWh, 3.3, 3) # 1.0 + 23 * 0.1