Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 148 additions & 3 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file starts with a blank line which is inconsistent with Python conventions. Python files should not start with blank lines.

Suggested change

Copilot uses AI. Check for mistakes.
import logging
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch

import dimo as dimo_sdk
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from custom_components.dimo import DOMAIN
from custom_components.dimo import (DOMAIN, PLATFORMS,
async_remove_config_entry_device,
async_setup_entry, async_unload_entry)
from custom_components.dimo.__init__ import DimoUpdateCoordinator, VehicleData
from custom_components.dimo.config_flow import InvalidAuth, NoVehiclesException
from custom_components.dimo.dimoapi import (InvalidApiKeyFormat,
InvalidClientIdError,
InvalidCredentialsError)


@pytest.fixture
def hass() -> HomeAssistant:
"""Return a dummy HomeAssistant instance."""
return MagicMock(spec=HomeAssistant)
hass_mock = MagicMock(spec=HomeAssistant)
hass_mock.config_entries = MagicMock()
hass_mock.async_add_executor_job = AsyncMock()
return hass_mock


@pytest.fixture
Expand Down Expand Up @@ -273,3 +284,137 @@ async def test_poll_interval_default_when_not_in_options():

# Verify that the update_interval is set to the default
assert coordinator.update_interval.total_seconds() == DEFAULT_POLL_INTERVAL

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between test functions. According to PEP 8, there should be two blank lines between top-level function definitions.

Suggested change

Copilot uses AI. Check for mistakes.
@pytest.mark.asyncio
async def test_async_setup_entry_invalid_auth(hass, entry):
with patch("custom_components.dimo.DimoClient") as mock_client_class:
mock_client = mock_client_class.return_value
# async_add_executor_job throws InvalidAuth
hass.async_add_executor_job.side_effect = InvalidAuth()
result = await async_setup_entry(hass, entry)
assert result is False
Comment on lines +289 to +295

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test patches DimoClient but this is incomplete. The test should also mock Auth (which is imported from dimoapi and instantiated at line 41-45 in init.py) to prevent actual instantiation. Additionally, the mock_client_class is patched but never properly configured, and the test doesn't mock the remaining parts of async_setup_entry like coordinator initialization and platform setup. Consider following the pattern from test_update_listener_registered_on_setup (lines 187-224) for more complete mocking.

Copilot uses AI. Check for mistakes.


@pytest.mark.asyncio
async def test_async_setup_entry_no_vehicles(hass, entry):
with patch("custom_components.dimo.DimoClient") as mock_client_class:
hass.async_add_executor_job.side_effect = NoVehiclesException()
with pytest.raises(ConfigEntryNotReady):
await async_setup_entry(hass, entry)
Comment on lines +299 to +303

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous test, this test has incomplete mocking. It patches DimoClient but doesn't mock Auth or other dependencies. Additionally, after the exception is raised at line 303, the remaining setup code won't execute, so mocks for coordinator, async_forward_entry_setups, and entry.async_on_unload should be added to prevent AttributeErrors.

Copilot uses AI. Check for mistakes.


@pytest.mark.asyncio
async def test_async_setup_entry_general_exception(hass, entry):
with patch("custom_components.dimo.DimoClient") as mock_client_class:
hass.async_add_executor_job.side_effect = Exception("General error")
with pytest.raises(ConfigEntryNotReady):
await async_setup_entry(hass, entry)
Comment on lines +307 to +311

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issues as the previous two tests - incomplete mocking of Auth and other dependencies. The test should follow the mocking pattern established in test_update_listener_registered_on_setup.

Copilot uses AI. Check for mistakes.


@pytest.mark.asyncio
async def test_async_remove_config_entry_device(hass, entry):
result = await async_remove_config_entry_device(hass, entry, None)

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test passes None as the device_entry parameter, but this doesn't meaningfully test the function. While async_remove_config_entry_device always returns True regardless of input, a more realistic test should pass a proper mock device_entry object to better simulate actual usage.

Suggested change
result = await async_remove_config_entry_device(hass, entry, None)
device_entry = MagicMock()
result = await async_remove_config_entry_device(hass, entry, device_entry)

Copilot uses AI. Check for mistakes.
assert result is True


@pytest.mark.asyncio
async def test_async_unload_entry(hass, entry):
hass.config_entries.async_unload_platforms = AsyncMock(return_value=True)
result = await async_unload_entry(hass, entry)
assert result is True
hass.config_entries.async_unload_platforms.assert_called_once_with(entry, PLATFORMS)


@pytest.mark.asyncio
async def test_get_api_data_exceptions(hass, entry):
coordinator = DimoUpdateCoordinator(hass, entry, MagicMock())

with pytest.raises(InvalidClientIdError):
hass.async_add_executor_job.side_effect = InvalidClientIdError()
await coordinator.get_api_data(MagicMock())

with pytest.raises(InvalidApiKeyFormat):
hass.async_add_executor_job.side_effect = InvalidApiKeyFormat()
await coordinator.get_api_data(MagicMock())

with pytest.raises(NoVehiclesException):
hass.async_add_executor_job.side_effect = NoVehiclesException()
await coordinator.get_api_data(MagicMock())

Comment on lines +334 to +343

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test sets side_effect on hass.async_add_executor_job multiple times without resetting the mock between test cases. This could cause issues if the mock retains state from previous exception tests. Consider either resetting the mock between cases with hass.async_add_executor_job.reset_mock(side_effect=True) or splitting this into separate test functions for each exception type to ensure test isolation.

Suggested change
await coordinator.get_api_data(MagicMock())
with pytest.raises(InvalidApiKeyFormat):
hass.async_add_executor_job.side_effect = InvalidApiKeyFormat()
await coordinator.get_api_data(MagicMock())
with pytest.raises(NoVehiclesException):
hass.async_add_executor_job.side_effect = NoVehiclesException()
await coordinator.get_api_data(MagicMock())
await coordinator.get_api_data(MagicMock())
hass.async_add_executor_job.reset_mock(side_effect=True)
with pytest.raises(InvalidApiKeyFormat):
hass.async_add_executor_job.side_effect = InvalidApiKeyFormat()
await coordinator.get_api_data(MagicMock())
hass.async_add_executor_job.reset_mock(side_effect=True)
with pytest.raises(NoVehiclesException):
hass.async_add_executor_job.side_effect = NoVehiclesException()
await coordinator.get_api_data(MagicMock())
hass.async_add_executor_job.reset_mock(side_effect=True)

Copilot uses AI. Check for mistakes.
with pytest.raises(Exception):
hass.async_add_executor_job.side_effect = Exception("Some other error")
await coordinator.get_api_data(MagicMock())

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between test functions. According to PEP 8, there should be two blank lines between top-level function definitions.

Suggested change

Copilot uses AI. Check for mistakes.
@pytest.mark.asyncio
async def test_get_vehicles_data(hass, entry):
coordinator = DimoUpdateCoordinator(hass, entry, MagicMock())
# Return valid vehicle data
with patch.object(coordinator, "get_api_data", return_value={"data": {"vehicles": {"nodes": [{"tokenId": "v1", "definition": {"make": "Ford"}}]}}}):
await coordinator.get_vehicles_data()
assert "v1" in coordinator.vehicle_data
assert coordinator.vehicle_data["v1"].definition["make"] == "Ford"

# Return None
with patch.object(coordinator, "get_api_data", return_value=None):
coordinator.vehicle_data = {}
await coordinator.get_vehicles_data()
assert not coordinator.vehicle_data

@pytest.mark.asyncio

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between test functions. According to PEP 8, there should be two blank lines between top-level function definitions.

Copilot uses AI. Check for mistakes.
async def test_async_initialise(hass, entry):
coordinator = DimoUpdateCoordinator(hass, entry, MagicMock())
coordinator.vehicle_data = {"v1": VehicleData(definition={"make": "Test"})}

with patch.object(coordinator, "get_vehicles_data", new_callable=AsyncMock) as mock_get_veh:
with patch.object(coordinator, "create_dimo_device") as mock_create_dimo:
with patch.object(coordinator, "get_dimo_sensor_data", new_callable=AsyncMock) as mock_dimo_sens:
with patch.object(coordinator, "_async_setup_single_vehicle", new_callable=AsyncMock) as mock_setup_veh:
await coordinator.async_initialise()
mock_get_veh.assert_called_once()
mock_create_dimo.assert_called_once()
mock_dimo_sens.assert_called_once()
mock_setup_veh.assert_called_once_with("v1")
Comment on lines +364 to +376

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test doesn't account for the conditional logic in async_initialise. The actual implementation at line 152 in init.py checks if DIMO_SENSORS: before calling create_dimo_device and get_dimo_sensor_data. Since DIMO_SENSORS is defined in const.py and the test doesn't mock it as empty, the test currently works. However, consider adding an assertion to verify DIMO_SENSORS is not empty, or add a separate test case where DIMO_SENSORS is empty to verify those methods aren't called.

Copilot uses AI. Check for mistakes.

@pytest.mark.asyncio

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between test functions. According to PEP 8, there should be two blank lines between top-level function definitions.

Copilot uses AI. Check for mistakes.
async def test_get_available_signals_for_vehicle(hass, entry):
coordinator = DimoUpdateCoordinator(hass, entry, MagicMock())
coordinator.vehicle_data = {"v1": VehicleData(definition={})}

with patch.object(coordinator, "get_api_data", return_value={"data": {"availableSignals": ["speed"]}}):
await coordinator.get_available_signals_for_vehicle("v1")
assert coordinator.vehicle_data["v1"].available_signals == ["speed"]
Comment on lines +383 to +385

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test patches get_api_data to return data with "data.availableSignals" but doesn't import or mock the get_key helper function which is used in the actual implementation to extract this value. The test should either import get_key from helpers or verify that the helper is properly extracting the data.

Suggested change
with patch.object(coordinator, "get_api_data", return_value={"data": {"availableSignals": ["speed"]}}):
await coordinator.get_available_signals_for_vehicle("v1")
assert coordinator.vehicle_data["v1"].available_signals == ["speed"]
with patch("custom_components.dimo.__init__.get_key", return_value=["speed"]) as mock_get_key:
with patch.object(coordinator, "get_api_data", return_value={"data": {"availableSignals": ["speed"]}}):
await coordinator.get_available_signals_for_vehicle("v1")
mock_get_key.assert_called_once()
assert coordinator.vehicle_data["v1"].available_signals == ["speed"]

Copilot uses AI. Check for mistakes.

# Test unknown vehicle
await coordinator.get_available_signals_for_vehicle("v2")

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test for unknown vehicle (line 388) doesn't verify any behavior - it just calls the function and doesn't assert anything. This should either verify that a warning is logged or that the vehicle_data remains unchanged for the unknown vehicle.

Copilot uses AI. Check for mistakes.

@pytest.mark.asyncio

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between test functions. According to PEP 8, there should be two blank lines between top-level function definitions.

Copilot uses AI. Check for mistakes.
async def test_get_signals_data_for_vehicle(hass, entry):
coordinator = DimoUpdateCoordinator(hass, entry, MagicMock())
coordinator.vehicle_data = {"v1": VehicleData(definition={}, available_signals=["speed"])}

with patch.object(coordinator, "get_api_data", return_value={"data": {"signalsLatest": {"speed": 100}}, "errors": None}):
with patch.object(coordinator, "_update_token_rewards", new_callable=AsyncMock):
await coordinator.get_signals_data_for_vehicle("v1")
assert coordinator.vehicle_data["v1"].signal_data == {"speed": 100}

# Unknown vehicle
await coordinator.get_signals_data_for_vehicle("v2")

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous test, the unknown vehicle case (line 401) doesn't assert any expected behavior. It should verify that an error is logged or that the vehicle_data for "v2" is not created/modified.

Copilot uses AI. Check for mistakes.

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between test functions. According to PEP 8, there should be two blank lines between top-level function definitions.

Suggested change

Copilot uses AI. Check for mistakes.
@pytest.mark.asyncio
async def test_update_token_rewards(hass, entry):
coordinator = DimoUpdateCoordinator(hass, entry, MagicMock())
coordinator.vehicle_data = {"v1": VehicleData(definition={}, signal_data={"speed": 100})}

with patch.object(coordinator, "get_api_data", return_value={"data": {"vehicle": {"earnings": {"totalTokens": 50}}}}):
await coordinator._update_token_rewards("v1")
assert "tokenRewards" in coordinator.vehicle_data["v1"].signal_data
assert coordinator.vehicle_data["v1"].signal_data["tokenRewards"]["value"] == 50

@pytest.mark.asyncio

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between test functions. According to PEP 8, there should be two blank lines between top-level function definitions.

Copilot uses AI. Check for mistakes.
async def test_async_update_data(hass, entry):
coordinator = DimoUpdateCoordinator(hass, entry, MagicMock())
coordinator.vehicle_data = {"v1": VehicleData(definition={})}
with patch.object(coordinator, "get_dimo_sensor_data", new_callable=AsyncMock):
with patch.object(coordinator, "get_signals_data_for_vehicle", new_callable=AsyncMock):
res = await coordinator.async_update_data()
assert res is True