322 lines
12 KiB
Python
322 lines
12 KiB
Python
"""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
|