Skip to content

Commit 7a8a9f3

Browse files
authored
Merge pull request #1135 from liudger/copilot/fix-1134
Increase test coverage to exceed 90%
2 parents cced080 + 3941c3f commit 7a8a9f3

7 files changed

Lines changed: 526 additions & 0 deletions

tests/test_api_initialization.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Tests for API data initialization error handling."""
2+
# pylint: disable=protected-access
3+
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from bsblan import BSBLAN
9+
from bsblan.bsblan import BSBLANConfig
10+
from bsblan.constants import API_DATA_NOT_INITIALIZED_ERROR_MSG, API_VERSION_ERROR_MSG
11+
from bsblan.exceptions import BSBLANError
12+
13+
14+
@pytest.mark.asyncio
15+
async def test_initialize_api_data_no_api_version() -> None:
16+
"""Test initializing API data with no API version set."""
17+
config = BSBLANConfig(host="example.com")
18+
bsblan = BSBLAN(config)
19+
20+
# API version is None by default
21+
with pytest.raises(BSBLANError, match=API_VERSION_ERROR_MSG):
22+
await bsblan._initialize_api_data()
23+
24+
25+
@pytest.mark.asyncio
26+
async def test_initialize_api_data_unexpected_none() -> None:
27+
"""Test edge case where API data is still None after initialization."""
28+
config = BSBLANConfig(host="example.com")
29+
bsblan = BSBLAN(config)
30+
31+
# Set API version but simulate data being None after initialization
32+
bsblan._api_version = "v3"
33+
34+
# Mock API_VERSIONS to return None for this specific test
35+
with patch("bsblan.bsblan.API_VERSIONS", {"v3": None}), pytest.raises(
36+
BSBLANError, match=API_DATA_NOT_INITIALIZED_ERROR_MSG
37+
):
38+
await bsblan._initialize_api_data()
39+
40+
41+
@pytest.mark.asyncio
42+
async def test_request_no_session() -> None:
43+
"""Test request method with no session initialized."""
44+
config = BSBLANConfig(host="example.com")
45+
bsblan = BSBLAN(config)
46+
47+
# Session is None by default
48+
with pytest.raises(BSBLANError, match="Session not initialized"):
49+
await bsblan._request()

tests/test_context_manager.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Tests for BSBLAN context manager features."""
2+
# pylint: disable=protected-access
3+
4+
from unittest.mock import AsyncMock, patch
5+
6+
import pytest
7+
8+
from bsblan import BSBLAN
9+
from bsblan.bsblan import BSBLANConfig
10+
11+
12+
@pytest.mark.asyncio
13+
async def test_context_manager_session_creation() -> None:
14+
"""Test that context manager creates and closes a session."""
15+
config = BSBLANConfig(host="example.com")
16+
17+
# Mock the initialize method to avoid actual API calls
18+
with patch.object(BSBLAN, "initialize", AsyncMock()) as mock_init:
19+
async with BSBLAN(config) as bsblan:
20+
# Check that session was created
21+
assert bsblan.session is not None
22+
assert bsblan._close_session is True
23+
24+
# Check that initialize was called
25+
mock_init.assert_called_once()
26+
27+
# After context exit, session.close should have been called
28+
# This is implicit since we're testing the context manager behavior
29+
30+
31+
@pytest.mark.asyncio
32+
async def test_context_manager_with_existing_session() -> None:
33+
"""Test that context manager uses an existing session if provided."""
34+
config = BSBLANConfig(host="example.com")
35+
36+
# Create a mock session
37+
session = AsyncMock()
38+
session.closed = False
39+
40+
# Mock initialize to avoid actual API calls
41+
with patch.object(BSBLAN, "initialize", AsyncMock()) as mock_init:
42+
async with BSBLAN(config, session=session) as bsblan:
43+
# Check that our session was used
44+
assert bsblan.session is session
45+
assert bsblan._close_session is False
46+
47+
# Check that initialize was called
48+
mock_init.assert_called_once()
49+
50+
# After context exit, session.close should not have been called
51+
session.close.assert_not_called()
52+
53+
54+
@pytest.mark.asyncio
55+
async def test_aexit_exception_handling() -> None:
56+
"""Test that BSBLAN doesn't swallow exceptions during session close."""
57+
config = BSBLANConfig(host="example.com")
58+
59+
# Create a mock session with a close method that raises an exception
60+
mock_session = AsyncMock()
61+
mock_session.close = AsyncMock(side_effect=Exception("Test exception"))
62+
63+
# Patch initialize to avoid actual API calls
64+
with patch.object(BSBLAN, "initialize", AsyncMock()):
65+
# Create BSBLAN instance with our mock session
66+
bsblan = BSBLAN(config, session=mock_session)
67+
bsblan._close_session = True # Force close on exit
68+
69+
# The exception from session.close should be propagated
70+
with pytest.raises(Exception, match="Test exception"):
71+
async with bsblan:
72+
pass # Just enter and exit the context

tests/test_entity_info.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Tests for EntityInfo value conversion error handling."""
2+
3+
import logging
4+
from datetime import time
5+
6+
import pytest
7+
8+
from bsblan.models import DataType, EntityInfo
9+
10+
11+
def test_entity_info_invalid_time_conversion() -> None:
12+
"""Test EntityInfo with invalid time format."""
13+
# Create EntityInfo with invalid time format
14+
entity = EntityInfo(
15+
name="Invalid Time",
16+
value="24:61", # Invalid time
17+
unit="",
18+
desc="",
19+
data_type=DataType.TIME,
20+
)
21+
22+
# The value should remain as string since conversion failed
23+
assert entity.value == "24:61"
24+
25+
26+
def test_entity_info_invalid_weekday_conversion() -> None:
27+
"""Test EntityInfo with invalid weekday format."""
28+
# Create EntityInfo with invalid weekday format
29+
entity = EntityInfo(
30+
name="Invalid Weekday",
31+
value="not-a-number", # Invalid weekday
32+
unit="",
33+
desc="",
34+
data_type=DataType.WEEKDAY,
35+
)
36+
37+
# The value should remain as string since conversion failed
38+
assert entity.value == "not-a-number"
39+
40+
41+
def test_entity_info_general_conversion_error(caplog: pytest.LogCaptureFixture) -> None:
42+
"""Test EntityInfo with general conversion error."""
43+
with caplog.at_level(logging.WARNING):
44+
# Create EntityInfo that will cause a conversion error
45+
entity = EntityInfo(
46+
name="Error Test",
47+
value=object(), # Object that can't be converted
48+
unit="",
49+
desc="",
50+
data_type=DataType.PLAIN_NUMBER,
51+
)
52+
53+
# The original value should be preserved
54+
assert isinstance(entity.value, object)
55+
assert "Failed to convert value" in caplog.text
56+
57+
58+
def test_entity_info_valid_time_conversion() -> None:
59+
"""Test EntityInfo with valid time format."""
60+
# Create EntityInfo with valid time format
61+
entity = EntityInfo(
62+
name="Valid Time",
63+
value="14:30",
64+
unit="",
65+
desc="",
66+
data_type=DataType.TIME,
67+
)
68+
69+
# The value should be converted to a time object
70+
assert isinstance(entity.value, time)
71+
assert entity.value.hour == 14
72+
assert entity.value.minute == 30
73+
74+
75+
def test_entity_info_enum_description() -> None:
76+
"""Test the enum_description property."""
77+
# Create EntityInfo with ENUM data type
78+
enum_entity = EntityInfo(
79+
name="Test Enum",
80+
value="1",
81+
unit="",
82+
desc="Enum Description",
83+
data_type=DataType.ENUM,
84+
)
85+
86+
# The enum_description should return the desc field
87+
assert enum_entity.enum_description == "Enum Description"
88+
89+
# Create EntityInfo with non-ENUM data type
90+
non_enum_entity = EntityInfo(
91+
name="Test Value",
92+
value="22",
93+
unit="°C",
94+
desc="Not an Enum",
95+
data_type=DataType.PLAIN_NUMBER,
96+
)
97+
98+
# The enum_description should return None
99+
assert non_enum_entity.enum_description is None

tests/test_reset_validation.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tests for APIValidator reset_validation method."""
2+
3+
from bsblan.utility import APIValidator
4+
5+
6+
def test_reset_validation_specific_section() -> None:
7+
"""Test resetting validation for a specific section."""
8+
# Create a test API config
9+
api_config = {
10+
"heating": {"param1": "value1"},
11+
"sensor": {"param2": "value2"},
12+
}
13+
14+
# Initialize APIValidator with some validated sections
15+
validator = APIValidator(api_config)
16+
validator.validated_sections.update({"heating", "sensor"})
17+
18+
# Verify initial state
19+
assert validator.is_section_validated("heating") is True
20+
assert validator.is_section_validated("sensor") is True
21+
22+
# Reset validation for just the heating section
23+
validator.reset_validation("heating")
24+
25+
# Verify only heating was reset
26+
assert validator.is_section_validated("heating") is False
27+
assert validator.is_section_validated("sensor") is True
28+
29+
30+
def test_reset_validation_nonexistent_section() -> None:
31+
"""Test resetting validation for a section that wasn't validated."""
32+
# Create a test API config
33+
api_config = {
34+
"heating": {"param1": "value1"},
35+
"sensor": {"param2": "value2"},
36+
}
37+
38+
# Initialize APIValidator with some validated sections
39+
validator = APIValidator(api_config)
40+
validator.validated_sections.add("heating")
41+
42+
# Verify initial state
43+
assert validator.is_section_validated("heating") is True
44+
assert validator.is_section_validated("nonexistent") is False
45+
46+
# Reset validation for a nonexistent section (should not error)
47+
validator.reset_validation("nonexistent")
48+
49+
# Verify state remains unchanged
50+
assert validator.is_section_validated("heating") is True
51+
assert validator.is_section_validated("nonexistent") is False

tests/test_temperature_unit.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Tests for temperature unit handling in BSBLAN."""
2+
# pylint: disable=protected-access
3+
4+
from unittest.mock import AsyncMock, patch
5+
6+
import pytest
7+
8+
from bsblan import BSBLAN
9+
from bsblan.bsblan import BSBLANConfig
10+
from bsblan.models import EntityInfo, StaticState
11+
12+
13+
@pytest.mark.asyncio
14+
async def test_temperature_unit_getter() -> None:
15+
"""Test the get_temperature_unit property."""
16+
config = BSBLANConfig(host="example.com")
17+
bsblan = BSBLAN(config)
18+
19+
# Test default unit
20+
assert bsblan.get_temperature_unit == "°C"
21+
22+
# Test with custom unit set
23+
bsblan._temperature_unit = "°F"
24+
assert bsblan.get_temperature_unit == "°F"
25+
26+
27+
@pytest.mark.asyncio
28+
async def test_initialize_temperature_range_celsius() -> None:
29+
"""Test initialization of temperature range with Celsius unit."""
30+
config = BSBLANConfig(host="example.com")
31+
bsblan = BSBLAN(config)
32+
33+
# Create mock static values with Celsius unit
34+
min_temp = EntityInfo(name="Min Temp", value="10", unit="°C", desc="", data_type=0)
35+
max_temp = EntityInfo(name="Max Temp", value="30", unit="°C", desc="", data_type=0)
36+
static_values = StaticState(min_temp=min_temp, max_temp=max_temp)
37+
38+
# Mock static_values method to return our test data
39+
with patch.object(bsblan, "static_values", AsyncMock(return_value=static_values)):
40+
await bsblan._initialize_temperature_range()
41+
42+
# Verify temperature range was set correctly
43+
assert bsblan._min_temp == 10.0
44+
assert bsblan._max_temp == 30.0
45+
assert bsblan._temperature_range_initialized is True
46+
assert bsblan._temperature_unit == "°C"
47+
48+
49+
@pytest.mark.asyncio
50+
async def test_initialize_temperature_range_fahrenheit() -> None:
51+
"""Test initialization of temperature range with Fahrenheit unit."""
52+
config = BSBLANConfig(host="example.com")
53+
bsblan = BSBLAN(config)
54+
55+
# Create mock static values with Fahrenheit unit
56+
min_temp = EntityInfo(name="Min Temp", value="50", unit="°F", desc="", data_type=0)
57+
max_temp = EntityInfo(name="Max Temp", value="86", unit="°F", desc="", data_type=0)
58+
static_values = StaticState(min_temp=min_temp, max_temp=max_temp)
59+
60+
# Mock static_values method to return our test data
61+
with patch.object(bsblan, "static_values", AsyncMock(return_value=static_values)):
62+
await bsblan._initialize_temperature_range()
63+
64+
# Verify temperature range was set correctly
65+
assert bsblan._min_temp == 50.0
66+
assert bsblan._max_temp == 86.0
67+
assert bsblan._temperature_range_initialized is True
68+
assert bsblan._temperature_unit == "°F"
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_initialize_temperature_range_alternate_celsius_format() -> None:
73+
"""Test initialization of temperature range with alternate Celsius format.
74+
75+
Tests with HTML entity format (°C) instead of unicode character.
76+
"""
77+
config = BSBLANConfig(host="example.com")
78+
bsblan = BSBLAN(config)
79+
80+
# Create mock static values with HTML degree symbol
81+
min_temp = EntityInfo(
82+
name="Min Temp", value="10", unit="°C", desc="", data_type=0
83+
)
84+
max_temp = EntityInfo(
85+
name="Max Temp", value="30", unit="°C", desc="", data_type=0
86+
)
87+
static_values = StaticState(min_temp=min_temp, max_temp=max_temp)
88+
89+
# Mock static_values method to return our test data
90+
with patch.object(bsblan, "static_values", AsyncMock(return_value=static_values)):
91+
await bsblan._initialize_temperature_range()
92+
93+
# Verify temperature range was set correctly
94+
assert bsblan._min_temp == 10.0
95+
assert bsblan._max_temp == 30.0
96+
assert bsblan._temperature_range_initialized is True
97+
assert bsblan._temperature_unit == "°C"

0 commit comments

Comments
 (0)