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
102 changes: 101 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Tests for the Victron Energy VRM API client."""

import logging
import unittest.mock

import aiohttp
import pytest
import random

from victron_vrm import VictronVRMClient
from victron_vrm.exceptions import VictronVRMError, AuthorizationError
from victron_vrm.exceptions import VictronVRMError, AuthorizationError, NotFoundError, ClientError
from victron_vrm.models import Site
from victron_vrm.models.auth import AuthToken
from victron_vrm.mqtt import VRMMQTTClient

# Set up logging
logging.basicConfig(
Expand All @@ -21,6 +24,34 @@
AUTH_DEMO_URL = "https://vrmapi.victronenergy.com/v2/auth/loginAsDemo"


# Helper classes for mocking
class _MockUser:
"""Mock user for testing."""
email = "test@example.com"
Comment on lines +29 to +30

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

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

The _MockUser class is missing required attributes from the actual User model. According to the User model in victron_vrm/models/user.py, it requires id, name, email, and country fields. Add these missing fields to make the mock match the expected structure.

Suggested change
"""Mock user for testing."""
email = "test@example.com"
"""Mock user for testing."""
id = 1
name = "Test User"
email = "test@example.com"
country = "NL"

Copilot uses AI. Check for mistakes.


class _MockSite:
"""Mock site for testing."""
identifier = "test-vrm-id"
mqtt_hostname = "mqtt.victronenergy.com"


class _MockSiteNoHostname:
"""Mock site without MQTT hostname for testing."""
identifier = "test-vrm-id"
mqtt_hostname = None


def _create_mock_token():
"""Create a mock auth token for testing."""
return AuthToken(
access_token="mock_access_token",
token_type="Bearer",
expires_in=3600,
scope="read"
)


@pytest.fixture(scope="session")
async def demo_token():
"""Get a demo token for testing (session-scoped to avoid 429s)."""
Expand Down Expand Up @@ -373,3 +404,72 @@ async def test_stats_false_to_none_transformation(vrm_client):
except VictronVRMError as e:
logger.warning(f"Error in mock test: {e}")
pytest.skip(f"Error in mock test: {e}")


@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_success():
"""Test getting MQTT client for a valid installation."""
# Create a mock client without needing actual network access
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
async def mock_gather(*args):
return _create_mock_token(), _MockUser(), [_MockSite()]

# Mock asyncio.gather to avoid actual API calls
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

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

Mocking asyncio.gather globally is too broad and risky. This patches all asyncio.gather calls, which could affect other async operations in the client or test infrastructure. Instead, mock the specific methods called by get_mqtt_client_for_installation: client._get_auth_token(), client.users.get_me(), and client.users.list_sites(). This approach is more targeted, safer, and follows the pattern used in the existing test test_stats_false_to_none_transformation which mocks specific methods.

Copilot uses AI. Check for mistakes.
# Get MQTT client
mqtt_client = await client.get_mqtt_client_for_installation(67890)

# Verify the client is properly configured
assert mqtt_client is not None
assert isinstance(mqtt_client, VRMMQTTClient)

# Verify client attributes
assert mqtt_client.host == "mqtt.victronenergy.com"
assert mqtt_client.username == "test@example.com"
assert mqtt_client.password == "Bearer mock_access_token"
assert mqtt_client.installation_id == "test-vrm-id"

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

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

Accessing mqtt_client.installation_id may fail because this attribute is passed to the parent VictronMQTTHub class and may not be directly accessible as a public attribute on the VRMMQTTClient instance. The VRMMQTTClient constructor only exposes the parameters passed to init, not the parent class attributes. Remove this assertion or verify that the parent class exposes this attribute publicly.

Suggested change
assert mqtt_client.installation_id == "test-vrm-id"
installation_id = getattr(mqtt_client, "installation_id", None)
if installation_id is not None:
assert installation_id == "test-vrm-id"

Copilot uses AI. Check for mistakes.

logger.info("Successfully created MQTT client with mocked data")


@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_not_found():
"""Test getting MQTT client for a non-existent installation."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
async def mock_gather(*args):
# Return empty list for installations
return _create_mock_token(), _MockUser(), []

# Mock asyncio.gather to return empty installations list
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Should raise NotFoundError
with pytest.raises(NotFoundError) as exc_info:
await client.get_mqtt_client_for_installation(999999999)

# Verify the error message
assert "not found" in str(exc_info.value).lower()
assert "999999999" in str(exc_info.value)

logger.info("NotFoundError correctly raised for invalid installation ID")


@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_missing_hostname():
"""Test getting MQTT client for installation without MQTT hostname."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
async def mock_gather(*args):
return _create_mock_token(), _MockUser(), [_MockSiteNoHostname()]

# Mock asyncio.gather
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Should raise ClientError
with pytest.raises(ClientError) as exc_info:
await client.get_mqtt_client_for_installation(67890)

# Verify the error message
assert "mqtt hostname" in str(exc_info.value).lower()
assert "67890" in str(exc_info.value)

Comment on lines +414 to +474

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

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

Mocking asyncio.gather globally is too broad and risky. This patches all asyncio.gather calls, which could affect other async operations in the client or test infrastructure. Instead, mock the specific methods called by get_mqtt_client_for_installation: client._get_auth_token(), client.users.get_me(), and client.users.list_sites(). This approach is more targeted, safer, and follows the pattern used in the existing test test_stats_false_to_none_transformation which mocks specific methods.

Suggested change
async def mock_gather(*args):
return _create_mock_token(), _MockUser(), [_MockSite()]
# Mock asyncio.gather to avoid actual API calls
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Get MQTT client
mqtt_client = await client.get_mqtt_client_for_installation(67890)
# Verify the client is properly configured
assert mqtt_client is not None
assert isinstance(mqtt_client, VRMMQTTClient)
# Verify client attributes
assert mqtt_client.host == "mqtt.victronenergy.com"
assert mqtt_client.username == "test@example.com"
assert mqtt_client.password == "Bearer mock_access_token"
assert mqtt_client.installation_id == "test-vrm-id"
logger.info("Successfully created MQTT client with mocked data")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_not_found():
"""Test getting MQTT client for a non-existent installation."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
async def mock_gather(*args):
# Return empty list for installations
return _create_mock_token(), _MockUser(), []
# Mock asyncio.gather to return empty installations list
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Should raise NotFoundError
with pytest.raises(NotFoundError) as exc_info:
await client.get_mqtt_client_for_installation(999999999)
# Verify the error message
assert "not found" in str(exc_info.value).lower()
assert "999999999" in str(exc_info.value)
logger.info("NotFoundError correctly raised for invalid installation ID")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_missing_hostname():
"""Test getting MQTT client for installation without MQTT hostname."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
async def mock_gather(*args):
return _create_mock_token(), _MockUser(), [_MockSiteNoHostname()]
# Mock asyncio.gather
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Should raise ClientError
with pytest.raises(ClientError) as exc_info:
await client.get_mqtt_client_for_installation(67890)
# Verify the error message
assert "mqtt hostname" in str(exc_info.value).lower()
assert "67890" in str(exc_info.value)
# Mock the specific async methods used by get_mqtt_client_for_installation
mock_get_auth_token = unittest.mock.AsyncMock(return_value=_create_mock_token())
mock_get_me = unittest.mock.AsyncMock(return_value=_MockUser())
mock_list_sites = unittest.mock.AsyncMock(return_value=[_MockSite()])
with unittest.mock.patch.object(client, "_get_auth_token", mock_get_auth_token), \
unittest.mock.patch.object(client.users, "get_me", mock_get_me), \
unittest.mock.patch.object(client.users, "list_sites", mock_list_sites):
# Get MQTT client
mqtt_client = await client.get_mqtt_client_for_installation(67890)
# Verify the client is properly configured
assert mqtt_client is not None
assert isinstance(mqtt_client, VRMMQTTClient)
# Verify client attributes
assert mqtt_client.host == "mqtt.victronenergy.com"
assert mqtt_client.username == "test@example.com"
assert mqtt_client.password == "Bearer mock_access_token"
assert mqtt_client.installation_id == "test-vrm-id"
logger.info("Successfully created MQTT client with mocked data")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_not_found():
"""Test getting MQTT client for a non-existent installation."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
# Mock the specific async methods used by get_mqtt_client_for_installation
mock_get_auth_token = unittest.mock.AsyncMock(return_value=_create_mock_token())
mock_get_me = unittest.mock.AsyncMock(return_value=_MockUser())
# Return empty list for installations
mock_list_sites = unittest.mock.AsyncMock(return_value=[])
with unittest.mock.patch.object(client, "_get_auth_token", mock_get_auth_token), \
unittest.mock.patch.object(client.users, "get_me", mock_get_me), \
unittest.mock.patch.object(client.users, "list_sites", mock_list_sites):
# Should raise NotFoundError
with pytest.raises(NotFoundError) as exc_info:
await client.get_mqtt_client_for_installation(999999999)
# Verify the error message
assert "not found" in str(exc_info.value).lower()
assert "999999999" in str(exc_info.value)
logger.info("NotFoundError correctly raised for invalid installation ID")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_missing_hostname():
"""Test getting MQTT client for installation without MQTT hostname."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
# Mock the specific async methods used by get_mqtt_client_for_installation
mock_get_auth_token = unittest.mock.AsyncMock(return_value=_create_mock_token())
mock_get_me = unittest.mock.AsyncMock(return_value=_MockUser())
mock_list_sites = unittest.mock.AsyncMock(return_value=[_MockSiteNoHostname()])
with unittest.mock.patch.object(client, "_get_auth_token", mock_get_auth_token), \
unittest.mock.patch.object(client.users, "get_me", mock_get_me), \
unittest.mock.patch.object(client.users, "list_sites", mock_list_sites):
# Should raise ClientError
with pytest.raises(ClientError) as exc_info:
await client.get_mqtt_client_for_installation(67890)
# Verify the error message
assert "mqtt hostname" in str(exc_info.value).lower()
assert "67890" in str(exc_info.value)

Copilot uses AI. Check for mistakes.
logger.info("ClientError correctly raised for installation without MQTT hostname")
Comment on lines +414 to +475

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

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

Mocking asyncio.gather globally is too broad and risky. This patches all asyncio.gather calls, which could affect other async operations in the client or test infrastructure. Instead, mock the specific methods called by get_mqtt_client_for_installation: client._get_auth_token(), client.users.get_me(), and client.users.list_sites(). This approach is more targeted, safer, and follows the pattern used in the existing test test_stats_false_to_none_transformation which mocks specific methods.

Suggested change
async def mock_gather(*args):
return _create_mock_token(), _MockUser(), [_MockSite()]
# Mock asyncio.gather to avoid actual API calls
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Get MQTT client
mqtt_client = await client.get_mqtt_client_for_installation(67890)
# Verify the client is properly configured
assert mqtt_client is not None
assert isinstance(mqtt_client, VRMMQTTClient)
# Verify client attributes
assert mqtt_client.host == "mqtt.victronenergy.com"
assert mqtt_client.username == "test@example.com"
assert mqtt_client.password == "Bearer mock_access_token"
assert mqtt_client.installation_id == "test-vrm-id"
logger.info("Successfully created MQTT client with mocked data")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_not_found():
"""Test getting MQTT client for a non-existent installation."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
async def mock_gather(*args):
# Return empty list for installations
return _create_mock_token(), _MockUser(), []
# Mock asyncio.gather to return empty installations list
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Should raise NotFoundError
with pytest.raises(NotFoundError) as exc_info:
await client.get_mqtt_client_for_installation(999999999)
# Verify the error message
assert "not found" in str(exc_info.value).lower()
assert "999999999" in str(exc_info.value)
logger.info("NotFoundError correctly raised for invalid installation ID")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_missing_hostname():
"""Test getting MQTT client for installation without MQTT hostname."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
async def mock_gather(*args):
return _create_mock_token(), _MockUser(), [_MockSiteNoHostname()]
# Mock asyncio.gather
with unittest.mock.patch("asyncio.gather", side_effect=mock_gather):
# Should raise ClientError
with pytest.raises(ClientError) as exc_info:
await client.get_mqtt_client_for_installation(67890)
# Verify the error message
assert "mqtt hostname" in str(exc_info.value).lower()
assert "67890" in str(exc_info.value)
logger.info("ClientError correctly raised for installation without MQTT hostname")
# Mock the specific async methods used by get_mqtt_client_for_installation
client._get_auth_token = unittest.mock.AsyncMock(return_value=_create_mock_token())
client.users.get_me = unittest.mock.AsyncMock(return_value=_MockUser())
client.users.list_sites = unittest.mock.AsyncMock(return_value=[_MockSite()])
# Get MQTT client
mqtt_client = await client.get_mqtt_client_for_installation(67890)
# Verify the client is properly configured
assert mqtt_client is not None
assert isinstance(mqtt_client, VRMMQTTClient)
# Verify client attributes
assert mqtt_client.host == "mqtt.victronenergy.com"
assert mqtt_client.username == "test@example.com"
assert mqtt_client.password == "Bearer mock_access_token"
assert mqtt_client.installation_id == "test-vrm-id"
logger.info("Successfully created MQTT client with mocked data")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_not_found():
"""Test getting MQTT client for a non-existent installation."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
# Mock the specific async methods used by get_mqtt_client_for_installation
client._get_auth_token = unittest.mock.AsyncMock(return_value=_create_mock_token())
client.users.get_me = unittest.mock.AsyncMock(return_value=_MockUser())
# Return empty list for installations to simulate "not found"
client.users.list_sites = unittest.mock.AsyncMock(return_value=[])
# Should raise NotFoundError
with pytest.raises(NotFoundError) as exc_info:
await client.get_mqtt_client_for_installation(999999999)
# Verify the error message
assert "not found" in str(exc_info.value).lower()
assert "999999999" in str(exc_info.value)
logger.info("NotFoundError correctly raised for invalid installation ID")
@pytest.mark.asyncio
async def test_get_mqtt_client_for_installation_missing_hostname():
"""Test getting MQTT client for installation without MQTT hostname."""
# Create a mock client
async with VictronVRMClient(token="mock_token", token_type="Bearer") as client:
# Mock the specific async methods used by get_mqtt_client_for_installation
client._get_auth_token = unittest.mock.AsyncMock(return_value=_create_mock_token())
client.users.get_me = unittest.mock.AsyncMock(return_value=_MockUser())
client.users.list_sites = unittest.mock.AsyncMock(return_value=[_MockSiteNoHostname()])
# Should raise ClientError
with pytest.raises(ClientError) as exc_info:
await client.get_mqtt_client_for_installation(67890)
# Verify the error message
assert "mqtt hostname" in str(exc_info.value).lower()
assert "67890" in str(exc_info.value)
logger.info("ClientError correctly raised for installation without MQTT hostname")

Copilot uses AI. Check for mistakes.
Loading