Files
elprisenligenu/tests/unit/test_api.py
T
Brian Bjarke Jensen 1676fd10c9
Python Code Quality / python-code-quality (push) Successful in 15s
Test Python Package / test (push) Failing after 16s
initial commit
2025-10-30 00:00:02 +01:00

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