initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Test package for elprisenligenu."""
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Test configuration and fixtures for elprisenligenu tests."""
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
"""Configure pytest with custom markers."""
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"integration: marks tests as integration tests (may require internet)",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_response() -> list[dict]:
|
||||
"""Fixture providing a mock API response."""
|
||||
return [
|
||||
{
|
||||
"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",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_hour_data() -> dict:
|
||||
"""Fixture providing sample HourData for testing."""
|
||||
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
return {
|
||||
"DKK_per_kWh": 1.2345,
|
||||
"EUR_per_kWh": 0.1659,
|
||||
"EXR": 7.4371,
|
||||
"time_start": datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz),
|
||||
"time_end": datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz),
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Integration tests for the elprisenligenu module."""
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
from elprisenligenu.api import API
|
||||
from elprisenligenu.dto.hour_data import HourData
|
||||
|
||||
|
||||
class TestElprisenLigenIntegration:
|
||||
"""Integration tests that make real API calls."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.api = API()
|
||||
self.tz = ZoneInfo("Europe/Copenhagen")
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_get_current_prices_dk1(self) -> None:
|
||||
"""Test fetching current day prices for DK1 (integration test)."""
|
||||
# Act
|
||||
result = self.api.get_daily_prices(zone="DK1")
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, list)
|
||||
assert all(isinstance(item, HourData) for item in result)
|
||||
|
||||
if result: # If data is available
|
||||
first_hour = result[0]
|
||||
assert isinstance(first_hour.DKK_per_kWh, float)
|
||||
assert isinstance(first_hour.EUR_per_kWh, float)
|
||||
assert isinstance(first_hour.EXR, float)
|
||||
assert isinstance(first_hour.time_start, datetime)
|
||||
assert isinstance(first_hour.time_end, datetime)
|
||||
|
||||
# Basic sanity checks
|
||||
assert first_hour.EXR > 0 # Exchange rate should be positive
|
||||
assert first_hour.time_start < first_hour.time_end
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_get_current_prices_dk2(self) -> None:
|
||||
"""Test fetching current day prices for DK2 (integration test)."""
|
||||
# Act
|
||||
result = self.api.get_daily_prices(zone="DK2")
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, list)
|
||||
assert all(isinstance(item, HourData) for item in result)
|
||||
|
||||
if result: # If data is available
|
||||
first_hour = result[0]
|
||||
assert isinstance(first_hour.DKK_per_kWh, float)
|
||||
assert isinstance(first_hour.EUR_per_kWh, float)
|
||||
assert isinstance(first_hour.EXR, float)
|
||||
assert isinstance(first_hour.time_start, datetime)
|
||||
assert isinstance(first_hour.time_end, datetime)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_get_historical_prices(self) -> None:
|
||||
"""Test fetching historical prices (integration test)."""
|
||||
# Arrange - Use a date that should have data (not too far back)
|
||||
historical_date = datetime(2024, 10, 1, tzinfo=self.tz)
|
||||
|
||||
# Act
|
||||
result = self.api.get_daily_prices(zone="DK1", date=historical_date)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, list)
|
||||
|
||||
if result: # If historical data is available
|
||||
assert len(result) <= 24 # Should be max 24 hours
|
||||
assert all(isinstance(item, HourData) for item in result)
|
||||
|
||||
# Check that all times are from the requested date
|
||||
for hour_data in result:
|
||||
assert hour_data.time_start.date() == historical_date.date()
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_price_consistency(self) -> None:
|
||||
"""Test that EUR and DKK prices are consistent with exchange rate."""
|
||||
# Act
|
||||
result = self.api.get_daily_prices(zone="DK1")
|
||||
|
||||
# Assert
|
||||
if result:
|
||||
for hour_data in result:
|
||||
# Calculate expected DKK price from EUR price and exchange rate
|
||||
expected_dkk = hour_data.EUR_per_kWh * hour_data.EXR
|
||||
|
||||
# Allow for small floating point differences
|
||||
difference = abs(hour_data.DKK_per_kWh - expected_dkk)
|
||||
assert difference < 0.001, (
|
||||
f"Price inconsistency: DKK={hour_data.DKK_per_kWh}, "
|
||||
f"EUR={hour_data.EUR_per_kWh}, EXR={hour_data.EXR}, "
|
||||
f"Expected DKK={expected_dkk}"
|
||||
)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_time_ordering(self) -> None:
|
||||
"""Test that hours are returned in chronological order."""
|
||||
# Act
|
||||
result = self.api.get_daily_prices(zone="DK1")
|
||||
|
||||
# Assert
|
||||
if len(result) > 1:
|
||||
for i in range(1, len(result)):
|
||||
assert result[i - 1].time_start <= result[i].time_start, (
|
||||
f"Hours not in chronological order: "
|
||||
f"{result[i - 1].time_start} > {result[i].time_start}"
|
||||
)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_hour_duration(self) -> None:
|
||||
"""Test that each hour period is exactly 1 hour long."""
|
||||
# Act
|
||||
result = self.api.get_daily_prices(zone="DK1")
|
||||
|
||||
# Assert
|
||||
if result:
|
||||
for hour_data in result:
|
||||
duration = hour_data.time_end - hour_data.time_start
|
||||
assert duration.total_seconds() == 3600, (
|
||||
f"Hour duration is not 1 hour: {duration}"
|
||||
)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_both_zones_same_day(self) -> None:
|
||||
"""Test that both zones return data for the same day."""
|
||||
# Arrange
|
||||
test_date = datetime(2024, 10, 15, tzinfo=self.tz)
|
||||
|
||||
# Act
|
||||
dk1_result = self.api.get_daily_prices(zone="DK1", date=test_date)
|
||||
dk2_result = self.api.get_daily_prices(zone="DK2", date=test_date)
|
||||
|
||||
# Assert
|
||||
# Both zones should have data for the same day
|
||||
if dk1_result and dk2_result:
|
||||
assert len(dk1_result) == len(dk2_result)
|
||||
|
||||
# Times should be the same
|
||||
for dk1_hour, dk2_hour in zip(dk1_result, dk2_result):
|
||||
assert dk1_hour.time_start == dk2_hour.time_start
|
||||
assert dk1_hour.time_end == dk2_hour.time_end
|
||||
|
||||
# Exchange rates should be the same
|
||||
assert dk1_hour.EXR == dk2_hour.EXR
|
||||
|
||||
# But prices can be different between zones
|
||||
# (That's the whole point of having two zones!)
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests package for elprisenligenu."""
|
||||
@@ -0,0 +1,321 @@
|
||||
"""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
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Unit tests for the HourData DTO model."""
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from elprisenligenu.dto.hour_data import HourData
|
||||
|
||||
|
||||
class TestHourData(unittest.TestCase):
|
||||
"""Test cases for the HourData model."""
|
||||
|
||||
def test_valid_hour_data_creation(self) -> None:
|
||||
"""Test creating a valid HourData instance."""
|
||||
# Arrange
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
start_time = datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz)
|
||||
end_time = datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz)
|
||||
|
||||
# Act
|
||||
hour_data = HourData(
|
||||
DKK_per_kWh=1.2345,
|
||||
EUR_per_kWh=0.1659,
|
||||
EXR=7.4371,
|
||||
time_start=start_time,
|
||||
time_end=end_time,
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(hour_data.DKK_per_kWh, 1.2345)
|
||||
self.assertEqual(hour_data.EUR_per_kWh, 0.1659)
|
||||
self.assertEqual(hour_data.EXR, 7.4371)
|
||||
self.assertEqual(hour_data.time_start, start_time)
|
||||
self.assertEqual(hour_data.time_end, end_time)
|
||||
|
||||
def test_hour_data_from_api_response(self) -> None:
|
||||
"""Test creating HourData from typical API response data."""
|
||||
|
||||
# Act - Pydantic can parse ISO datetime strings
|
||||
hour_data = HourData(
|
||||
DKK_per_kWh=1.3929,
|
||||
EUR_per_kWh=0.18729,
|
||||
EXR=7.437118,
|
||||
time_start=datetime.fromisoformat("2022-11-25T00:00:00+01:00"),
|
||||
time_end=datetime.fromisoformat("2022-11-25T01:00:00+01:00"),
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(hour_data.DKK_per_kWh, 1.3929)
|
||||
self.assertEqual(hour_data.EUR_per_kWh, 0.18729)
|
||||
self.assertEqual(hour_data.EXR, 7.437118)
|
||||
self.assertEqual(hour_data.time_start.year, 2022)
|
||||
self.assertEqual(hour_data.time_start.month, 11)
|
||||
self.assertEqual(hour_data.time_start.day, 25)
|
||||
self.assertEqual(hour_data.time_start.hour, 0)
|
||||
self.assertEqual(hour_data.time_end.hour, 1)
|
||||
|
||||
def test_hour_data_validation_missing_fields(self) -> None:
|
||||
"""Test validation fails when required fields are missing."""
|
||||
|
||||
# Arrange
|
||||
incomplete_data = {
|
||||
"DKK_per_kWh": 1.2345,
|
||||
"EUR_per_kWh": 0.1659,
|
||||
# Missing EXR, time_start, time_end
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
HourData(**incomplete_data) # type: ignore[arg-type]
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
missing_fields = {error["loc"][0] for error in errors}
|
||||
self.assertIn("EXR", missing_fields)
|
||||
self.assertIn("time_start", missing_fields)
|
||||
self.assertIn("time_end", missing_fields)
|
||||
|
||||
def test_hour_data_validation_invalid_types(self) -> None:
|
||||
"""Test validation fails with invalid data types."""
|
||||
# Arrange
|
||||
invalid_data = {
|
||||
"DKK_per_kWh": "not_a_number", # Should be float
|
||||
"EUR_per_kWh": 0.1659,
|
||||
"EXR": 7.4371,
|
||||
"time_start": "2024-10-29T12:00:00+01:00",
|
||||
"time_end": "2024-10-29T13:00:00+01:00",
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
HourData(**invalid_data) # type: ignore[arg-type]
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
self.assertTrue(any(error["loc"][0] == "DKK_per_kWh" for error in errors))
|
||||
|
||||
def test_hour_data_negative_prices(self) -> None:
|
||||
"""Test HourData accepts negative prices (real markets)."""
|
||||
# Arrange
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
|
||||
# Act
|
||||
hour_data = HourData(
|
||||
DKK_per_kWh=-0.5, # Negative price
|
||||
EUR_per_kWh=-0.067,
|
||||
EXR=7.4371,
|
||||
time_start=datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz),
|
||||
time_end=datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz),
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(hour_data.DKK_per_kWh, -0.5)
|
||||
self.assertEqual(hour_data.EUR_per_kWh, -0.067)
|
||||
|
||||
def test_hour_data_zero_prices(self) -> None:
|
||||
"""Test HourData accepts zero prices."""
|
||||
# Arrange
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
|
||||
# Act
|
||||
hour_data = HourData(
|
||||
DKK_per_kWh=0.0,
|
||||
EUR_per_kWh=0.0,
|
||||
EXR=7.4371,
|
||||
time_start=datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz),
|
||||
time_end=datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz),
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(hour_data.DKK_per_kWh, 0.0)
|
||||
self.assertEqual(hour_data.EUR_per_kWh, 0.0)
|
||||
|
||||
def test_hour_data_serialization(self) -> None:
|
||||
"""Test HourData can be serialized to JSON."""
|
||||
# Arrange
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
start_time = datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz)
|
||||
end_time = datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz)
|
||||
|
||||
hour_data = HourData(
|
||||
DKK_per_kWh=1.2345,
|
||||
EUR_per_kWh=0.1659,
|
||||
EXR=7.4371,
|
||||
time_start=start_time,
|
||||
time_end=end_time,
|
||||
)
|
||||
|
||||
# Act
|
||||
json_data = hour_data.model_dump()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(json_data["DKK_per_kWh"], 1.2345)
|
||||
self.assertEqual(json_data["EUR_per_kWh"], 0.1659)
|
||||
self.assertEqual(json_data["EXR"], 7.4371)
|
||||
self.assertEqual(json_data["time_start"], start_time)
|
||||
self.assertEqual(json_data["time_end"], end_time)
|
||||
|
||||
def test_hour_data_string_representation(self) -> None:
|
||||
"""Test HourData string representation is readable."""
|
||||
# Arrange
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
hour_data = HourData(
|
||||
DKK_per_kWh=1.2345,
|
||||
EUR_per_kWh=0.1659,
|
||||
EXR=7.4371,
|
||||
time_start=datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz),
|
||||
time_end=datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz),
|
||||
)
|
||||
|
||||
# Act
|
||||
str_repr = str(hour_data)
|
||||
|
||||
# Assert
|
||||
self.assertIn("DKK_per_kWh=1.2345", str_repr)
|
||||
self.assertIn("EUR_per_kWh=0.1659", str_repr)
|
||||
self.assertIn("EXR=7.4371", str_repr)
|
||||
|
||||
def test_hour_data_equality(self) -> None:
|
||||
"""Test HourData equality comparison."""
|
||||
# Arrange
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
start_time = datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz)
|
||||
end_time = datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz)
|
||||
|
||||
hour_data1 = HourData(
|
||||
DKK_per_kWh=1.2345,
|
||||
EUR_per_kWh=0.1659,
|
||||
EXR=7.4371,
|
||||
time_start=start_time,
|
||||
time_end=end_time,
|
||||
)
|
||||
hour_data2 = HourData(
|
||||
DKK_per_kWh=1.2345,
|
||||
EUR_per_kWh=0.1659,
|
||||
EXR=7.4371,
|
||||
time_start=start_time,
|
||||
time_end=end_time,
|
||||
)
|
||||
|
||||
# Act & Assert
|
||||
self.assertEqual(hour_data1, hour_data2)
|
||||
|
||||
def test_hour_data_inequality(self) -> None:
|
||||
"""Test HourData inequality comparison."""
|
||||
# Arrange
|
||||
tz = ZoneInfo("Europe/Copenhagen")
|
||||
start_time = datetime(2024, 10, 29, 12, 0, 0, tzinfo=tz)
|
||||
end_time = datetime(2024, 10, 29, 13, 0, 0, tzinfo=tz)
|
||||
|
||||
hour_data1 = HourData(
|
||||
DKK_per_kWh=1.2345,
|
||||
EUR_per_kWh=0.1659,
|
||||
EXR=7.4371,
|
||||
time_start=start_time,
|
||||
time_end=end_time,
|
||||
)
|
||||
|
||||
hour_data2 = HourData(
|
||||
DKK_per_kWh=2.0000, # Different price
|
||||
EUR_per_kWh=0.1659,
|
||||
EXR=7.4371,
|
||||
time_start=start_time,
|
||||
time_end=end_time,
|
||||
)
|
||||
|
||||
# Act & Assert
|
||||
self.assertNotEqual(hour_data1, hour_data2)
|
||||
Reference in New Issue
Block a user