Adding tests to syncthing (#170892)

This commit is contained in:
Muhammad Hamza Khan
2026-07-02 16:50:08 +02:00
committed by GitHub
parent faaf86281e
commit 983507e1e2
14 changed files with 879 additions and 31 deletions
+68
View File
@@ -1 +1,69 @@
"""Tests for the syncthing integration."""
import asyncio
from unittest.mock import AsyncMock, MagicMock
from homeassistant.components.syncthing.const import DOMAIN
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from tests.common import load_json_object_fixture
URL = "http://127.0.0.1:8384"
TOKEN = "token"
VERIFY_SSL = True
MOCK_ENTRY = {
CONF_URL: URL,
CONF_TOKEN: TOKEN,
CONF_VERIFY_SSL: VERIFY_SSL,
}
SERVER_ID = "YZXABCD-ABCDEFG-HIJKLMN-OPQRSTU-VWXYZAB-CDEFGHI-JKLMNOP-QRSTUVW"
SERVER_NAME = "This Device"
FOLDER_ID = "test-folder"
FOLDER_LABEL = "Test Folder"
SERVER_ID_SHORT_HA = SERVER_ID.split("-", maxsplit=1)[0]
URL_HA = URL.lower().replace("://", "_").replace(".", "_").replace(":", "_")
def create_mock_syncthing_client() -> MagicMock:
"""Create a mocked Syncthing client."""
mock_client = MagicMock()
mock_system = MagicMock()
mock_database = MagicMock()
mock_events = MagicMock()
mock_system.status = AsyncMock(
return_value=load_json_object_fixture("system_status.json", DOMAIN)
)
mock_system.version = AsyncMock(
return_value=load_json_object_fixture("system_version.json", DOMAIN)
)
mock_system.ping = AsyncMock(
return_value=load_json_object_fixture("ping.json", DOMAIN)
)
mock_system.config = AsyncMock(
return_value=load_json_object_fixture("config.json", DOMAIN)
)
mock_database.status = AsyncMock(
return_value=load_json_object_fixture("folder_status.json", DOMAIN)
)
async def mock_listen():
"""Mock events.listen that doesn't block."""
while True:
await asyncio.sleep(3600)
yield None
mock_events.listen = mock_listen
mock_events.last_seen_id = 0
mock_client.system = mock_system
mock_client.database = mock_database
mock_client.events = mock_events
mock_client.close = AsyncMock()
mock_client.url = URL
return mock_client
+56
View File
@@ -0,0 +1,56 @@
"""Common test fixtures."""
from collections.abc import AsyncIterator
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.syncthing.const import DOMAIN
from homeassistant.core import HomeAssistant
from . import MOCK_ENTRY, SERVER_ID, create_mock_syncthing_client
from tests.common import MockConfigEntry
@pytest.fixture
def entry(hass: HomeAssistant) -> MockConfigEntry:
"""Create a mock ConfigEntry for Syncthing component."""
entry = MockConfigEntry(
domain=DOMAIN,
data=MOCK_ENTRY,
unique_id=SERVER_ID,
)
entry.add_to_hass(hass)
return entry
@pytest.fixture
def mock_syncthing_client() -> MagicMock:
"""Create a mock Syncthing client."""
return create_mock_syncthing_client()
@pytest.fixture
async def mock_syncthing(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing_client: MagicMock,
) -> AsyncIterator[MagicMock]:
"""Create a mock Syncthing client and set up the config entry."""
with (
patch(
"homeassistant.components.syncthing.aiosyncthing.Syncthing",
autospec=True,
) as mock_class,
patch(
"homeassistant.components.syncthing.config_flow.aiosyncthing.Syncthing",
new=mock_class,
),
):
mock_class.return_value = mock_syncthing_client
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
yield mock_syncthing_client
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
@@ -0,0 +1,14 @@
{
"folders": [
{
"id": "test-folder",
"label": "Test Folder"
}
],
"devices": [
{
"deviceID": "YZXABCD-ABCDEFG-HIJKLMN-OPQRSTU-VWXYZAB-CDEFGHI-JKLMNOP-QRSTUVW",
"name": "This Device"
}
]
}
@@ -0,0 +1,10 @@
{
"id": 7,
"globalID": 7,
"type": "FolderPaused",
"time": "2024-01-01T00:06:00.000000000Z",
"data": {
"id": "test-folder",
"label": "Test Folder"
}
}
@@ -0,0 +1,27 @@
{
"errors": 0,
"globalBytes": 1000000,
"globalDeleted": 0,
"globalDirectories": 10,
"globalFiles": 100,
"globalSymlinks": 0,
"globalTotalItems": 110,
"ignorePatterns": false,
"inSyncBytes": 1000000,
"inSyncFiles": 100,
"invalid": "",
"localBytes": 1000000,
"localDeleted": 0,
"localDirectories": 10,
"localFiles": 100,
"localSymlinks": 0,
"localTotalItems": 110,
"needBytes": 0,
"needDeletes": 0,
"needDirectories": 0,
"needFiles": 0,
"needSymlinks": 0,
"needTotalItems": 0,
"pullErrors": 0,
"state": "idle"
}
@@ -0,0 +1,36 @@
{
"id": 5,
"globalID": 5,
"type": "FolderSummary",
"time": "2024-01-01T00:04:00.000000000Z",
"data": {
"folder": "test-folder",
"summary": {
"errors": 0,
"globalBytes": 1000000,
"globalDeleted": 0,
"globalDirectories": 10,
"globalFiles": 100,
"globalSymlinks": 0,
"globalTotalItems": 110,
"ignorePatterns": false,
"inSyncBytes": 1000000,
"inSyncFiles": 100,
"invalid": "",
"localBytes": 1000000,
"localDeleted": 0,
"localDirectories": 10,
"localFiles": 100,
"localSymlinks": 0,
"localTotalItems": 110,
"needBytes": 0,
"needDeletes": 0,
"needDirectories": 0,
"needFiles": 0,
"needSymlinks": 0,
"needTotalItems": 0,
"pullErrors": 0,
"state": "idle"
}
}
}
@@ -0,0 +1 @@
{ "ping": "pong" }
@@ -0,0 +1,11 @@
{
"id": 6,
"globalID": 6,
"type": "StateChanged",
"time": "2024-01-01T00:05:00.000000000Z",
"data": {
"folder": "test-folder",
"from": "idle",
"to": "syncing"
}
}
@@ -0,0 +1 @@
{ "myID": "YZXABCD-ABCDEFG-HIJKLMN-OPQRSTU-VWXYZAB-CDEFGHI-JKLMNOP-QRSTUVW" }
@@ -0,0 +1 @@
{ "version": "v1.23.0" }
@@ -0,0 +1,309 @@
# serializer version: 1
# name: test_folder_sensor_updates_on_event[folder_paused_event.json][sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'YZXABCD test-folder Test Folder',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'YZXABCD test-folder Test Folder',
'platform': 'syncthing',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'syncthing',
'unique_id': 'YZXABCD-test-folder',
'unit_of_measurement': None,
})
# ---
# name: test_folder_sensor_updates_on_event[folder_paused_event.json][sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'errors': 0,
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Syncthing (http://127.0.0.1:8384) YZXABCD test-folder Test Folder',
'global_bytes': 1000000,
'global_deleted': 0,
'global_directories': 10,
'global_files': 100,
'global_symlinks': 0,
'global_total_items': 110,
'id': 'test-folder',
'ignore_patterns': False,
'in_sync_bytes': 1000000,
'in_sync_files': 100,
'invalid': '',
'label': 'Test Folder',
'local_bytes': 1000000,
'local_deleted': 0,
'local_directories': 10,
'local_files': 100,
'local_symlinks': 0,
'local_total_items': 110,
'need_bytes': 0,
'need_deletes': 0,
'need_directories': 0,
'need_files': 0,
'need_symlinks': 0,
'need_total_items': 0,
'pull_errors': 0,
'state': 'paused',
}),
'context': <ANY>,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'paused',
})
# ---
# name: test_folder_sensor_updates_on_event[folder_summary_event.json][sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'YZXABCD test-folder Test Folder',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'YZXABCD test-folder Test Folder',
'platform': 'syncthing',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'syncthing',
'unique_id': 'YZXABCD-test-folder',
'unit_of_measurement': None,
})
# ---
# name: test_folder_sensor_updates_on_event[folder_summary_event.json][sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'errors': 0,
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Syncthing (http://127.0.0.1:8384) YZXABCD test-folder Test Folder',
'global_bytes': 1000000,
'global_deleted': 0,
'global_directories': 10,
'global_files': 100,
'global_symlinks': 0,
'global_total_items': 110,
'id': 'test-folder',
'ignore_patterns': False,
'in_sync_bytes': 1000000,
'in_sync_files': 100,
'invalid': '',
'label': 'Test Folder',
'local_bytes': 1000000,
'local_deleted': 0,
'local_directories': 10,
'local_files': 100,
'local_symlinks': 0,
'local_total_items': 110,
'need_bytes': 0,
'need_deletes': 0,
'need_directories': 0,
'need_files': 0,
'need_symlinks': 0,
'need_total_items': 0,
'pull_errors': 0,
'state': 'idle',
}),
'context': <ANY>,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'idle',
})
# ---
# name: test_folder_sensor_updates_on_event[state_changed_event.json][sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'YZXABCD test-folder Test Folder',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'YZXABCD test-folder Test Folder',
'platform': 'syncthing',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'syncthing',
'unique_id': 'YZXABCD-test-folder',
'unit_of_measurement': None,
})
# ---
# name: test_folder_sensor_updates_on_event[state_changed_event.json][sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'errors': 0,
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Syncthing (http://127.0.0.1:8384) YZXABCD test-folder Test Folder',
'global_bytes': 1000000,
'global_deleted': 0,
'global_directories': 10,
'global_files': 100,
'global_symlinks': 0,
'global_total_items': 110,
'id': 'test-folder',
'ignore_patterns': False,
'in_sync_bytes': 1000000,
'in_sync_files': 100,
'invalid': '',
'label': 'Test Folder',
'local_bytes': 1000000,
'local_deleted': 0,
'local_directories': 10,
'local_files': 100,
'local_symlinks': 0,
'local_total_items': 110,
'need_bytes': 0,
'need_deletes': 0,
'need_directories': 0,
'need_files': 0,
'need_symlinks': 0,
'need_total_items': 0,
'pull_errors': 0,
'state': 'syncing',
}),
'context': <ANY>,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'syncing',
})
# ---
# name: test_sensor_platform_setup[sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'YZXABCD test-folder Test Folder',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'YZXABCD test-folder Test Folder',
'platform': 'syncthing',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'syncthing',
'unique_id': 'YZXABCD-test-folder',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_platform_setup[sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'errors': 0,
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Syncthing (http://127.0.0.1:8384) YZXABCD test-folder Test Folder',
'global_bytes': 1000000,
'global_deleted': 0,
'global_directories': 10,
'global_files': 100,
'global_symlinks': 0,
'global_total_items': 110,
'id': 'test-folder',
'ignore_patterns': False,
'in_sync_bytes': 1000000,
'in_sync_files': 100,
'invalid': '',
'label': 'Test Folder',
'local_bytes': 1000000,
'local_deleted': 0,
'local_directories': 10,
'local_files': 100,
'local_symlinks': 0,
'local_total_items': 110,
'need_bytes': 0,
'need_deletes': 0,
'need_directories': 0,
'need_files': 0,
'need_symlinks': 0,
'need_total_items': 0,
'pull_errors': 0,
'state': 'idle',
}),
'context': <ANY>,
'entity_id': 'sensor.syncthing_http_127_0_0_1_8384_yzxabcd_test_folder_test_folder',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'idle',
})
# ---
+13 -31
View File
@@ -6,24 +6,14 @@ from aiosyncthing.exceptions import UnauthorizedError
from homeassistant import config_entries
from homeassistant.components.syncthing.const import DOMAIN
from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import MOCK_ENTRY, SERVER_ID, TOKEN, URL, VERIFY_SSL
from tests.common import MockConfigEntry
NAME = "Syncthing"
URL = "http://127.0.0.1:8384"
TOKEN = "token"
VERIFY_SSL = True
MOCK_ENTRY = {
CONF_NAME: NAME,
CONF_URL: URL,
CONF_TOKEN: TOKEN,
CONF_VERIFY_SSL: VERIFY_SSL,
}
async def test_show_setup_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served."""
@@ -39,7 +29,7 @@ async def test_show_setup_form(hass: HomeAssistant) -> None:
async def test_flow_successful(hass: HomeAssistant) -> None:
"""Test with required fields only."""
with (
patch("aiosyncthing.system.System.status", return_value={"myID": "server-id"}),
patch("aiosyncthing.system.System.status", return_value={"myID": SERVER_ID}),
patch(
"homeassistant.components.syncthing.async_setup_entry",
return_value=True,
@@ -48,29 +38,21 @@ async def test_flow_successful(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "user"},
data={
CONF_NAME: NAME,
CONF_URL: URL,
CONF_TOKEN: TOKEN,
CONF_VERIFY_SSL: VERIFY_SSL,
},
data=MOCK_ENTRY,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "http://127.0.0.1:8384"
assert result["data"][CONF_NAME] == NAME
assert result["title"] == URL
assert result["data"][CONF_URL] == URL
assert result["data"][CONF_TOKEN] == TOKEN
assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL
assert len(mock_setup_entry.mock_calls) == 1
async def test_flow_already_configured(hass: HomeAssistant) -> None:
"""Test name is already configured."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY, unique_id="server-id")
entry.add_to_hass(hass)
with patch("aiosyncthing.system.System.status", return_value={"myID": "server-id"}):
async def test_flow_already_configured(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test the server ID is already configured."""
with patch("aiosyncthing.system.System.status", return_value={"myID": SERVER_ID}):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "user"},
@@ -92,7 +74,7 @@ async def test_flow_invalid_auth(hass: HomeAssistant) -> None:
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["token"] == "invalid_auth"
assert result["errors"] == {"token": "invalid_auth"}
async def test_flow_cannot_connect(hass: HomeAssistant) -> None:
@@ -106,4 +88,4 @@ async def test_flow_cannot_connect(hass: HomeAssistant) -> None:
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "cannot_connect"
assert result["errors"] == {"base": "cannot_connect"}
+143
View File
@@ -0,0 +1,143 @@
"""Tests for the syncthing integration setup and client."""
import asyncio
from unittest.mock import MagicMock, patch
from aiosyncthing.exceptions import SyncthingError
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.syncthing.const import (
DOMAIN,
EVENTS,
RECONNECT_INTERVAL,
SERVER_AVAILABLE,
SERVER_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import dispatcher
from . import SERVER_ID
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_object_fixture,
)
async def test_syncthing_client_reconnect_on_error(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test SyncthingClient reconnects when server becomes unavailable."""
call_count = 0
async def mock_listen():
"""Mock listen that raises error first, then succeeds."""
nonlocal call_count
call_count += 1
if call_count == 1:
raise SyncthingError("Connection lost")
while True:
yield await hass.async_add_executor_job(
load_json_object_fixture, "state_changed_event.json", DOMAIN
)
mock_syncthing_client.events.last_seen_id = 10
mock_syncthing_client.events.listen = mock_listen
server_unavailable_calls = []
server_available_calls = []
async def server_unavailable_handler() -> None:
"""Handle server unavailable signal."""
server_unavailable_calls.append(True)
async def server_available_handler() -> None:
"""Handle server available signal."""
server_available_calls.append(True)
dispatcher.async_dispatcher_connect(
hass,
f"{SERVER_UNAVAILABLE}-{SERVER_ID}",
server_unavailable_handler,
)
dispatcher.async_dispatcher_connect(
hass,
f"{SERVER_AVAILABLE}-{SERVER_ID}",
server_available_handler,
)
with patch(
"homeassistant.components.syncthing.aiosyncthing.Syncthing",
autospec=True,
) as mock_class:
mock_class.return_value = mock_syncthing_client
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(server_unavailable_calls) >= 1
assert len(server_available_calls) == 0
freezer.tick(RECONNECT_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert len(server_available_calls) >= 1
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
@pytest.mark.parametrize(
"event_fixture",
[
"folder_summary_event.json",
"state_changed_event.json",
"folder_paused_event.json",
],
)
async def test_client_dispatches_event(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing_client: MagicMock,
event_fixture: str,
) -> None:
"""Test SyncthingClient dispatches the expected signal for syncthing events."""
event = await hass.async_add_executor_job(
load_json_object_fixture, event_fixture, DOMAIN
)
async def mock_listen():
yield event
await asyncio.Event().wait()
mock_syncthing_client.events.listen = mock_listen
mock_syncthing_client.events.last_seen_id = 10
folder = event["data"].get("folder") or event["data"]["id"]
signal = f"{EVENTS[event['type']]}-{SERVER_ID}-{folder}"
received = asyncio.Event()
captured: list[dict] = []
@callback
def handler(received_event: dict) -> None:
captured.append(received_event)
received.set()
dispatcher.async_dispatcher_connect(hass, signal, handler)
with patch(
"homeassistant.components.syncthing.aiosyncthing.Syncthing",
autospec=True,
) as mock_class:
mock_class.return_value = mock_syncthing_client
assert await hass.config_entries.async_setup(entry.entry_id)
async with asyncio.timeout(5):
await received.wait()
assert captured == [event]
+189
View File
@@ -0,0 +1,189 @@
"""Tests for the syncthing sensor platform."""
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock, patch
from aiosyncthing.exceptions import SyncthingError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.syncthing.const import (
DOMAIN,
EVENTS,
SCAN_INTERVAL,
SERVER_AVAILABLE,
SERVER_UNAVAILABLE,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import dispatcher, entity_registry as er
from homeassistant.util import dt as dt_util
from . import FOLDER_ID, SERVER_ID, SERVER_ID_SHORT_HA
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_object_fixture,
snapshot_platform,
)
async def test_sensor_platform_setup(
hass: HomeAssistant,
entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_syncthing: MagicMock,
) -> None:
"""Test sensor platform sets up folder sensors."""
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
async def test_sensor_platform_no_sensors_on_config_error(
hass: HomeAssistant,
entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_syncthing_client: MagicMock,
) -> None:
"""Test sensor platform does not create folder sensors when config fetch fails."""
mock_syncthing_client.system.config.side_effect = SyncthingError("Connection error")
with patch(
"homeassistant.components.syncthing.aiosyncthing.Syncthing",
autospec=True,
) as mock_class:
mock_class.return_value = mock_syncthing_client
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
entity_id = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SERVER_ID_SHORT_HA}-{FOLDER_ID}"
)
assert entity_id is None
@pytest.mark.parametrize(
"event_fixture",
[
"folder_summary_event.json",
"state_changed_event.json",
"folder_paused_event.json",
],
)
async def test_folder_sensor_updates_on_event(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing: MagicMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
event_fixture: str,
) -> None:
"""Test folder sensor updates when receiving different events."""
event = await hass.async_add_executor_job(
load_json_object_fixture, event_fixture, DOMAIN
)
folder = event["data"].get("folder") or event["data"]["id"]
signal = f"{EVENTS[event['type']]}-{SERVER_ID}-{folder}"
dispatcher.async_dispatcher_send(hass, signal, event)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
async def test_folder_sensor_unavailable_on_server_unavailable(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test folder sensor becomes unavailable when server is unavailable."""
entity_id = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SERVER_ID_SHORT_HA}-{FOLDER_ID}"
)
state = hass.states.get(entity_id) if entity_id else None
assert state is not None and state.state == "idle"
dispatcher.async_dispatcher_send(
hass,
f"{SERVER_UNAVAILABLE}-{SERVER_ID}",
)
await hass.async_block_till_done()
state = hass.states.get(entity_id) if entity_id else None
assert state is not None and state.state == STATE_UNAVAILABLE
async def test_folder_sensor_available_on_server_available(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test folder sensor becomes available when server comes back online."""
dispatcher.async_dispatcher_send(
hass,
f"{SERVER_UNAVAILABLE}-{SERVER_ID}",
)
await hass.async_block_till_done()
entity_id = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SERVER_ID_SHORT_HA}-{FOLDER_ID}"
)
state = hass.states.get(entity_id) if entity_id else None
assert state is not None and state.state == STATE_UNAVAILABLE
dispatcher.async_dispatcher_send(
hass,
f"{SERVER_AVAILABLE}-{SERVER_ID}",
)
await hass.async_block_till_done()
state = hass.states.get(entity_id) if entity_id else None
assert state is not None and state.state == "idle"
async def test_folder_sensor_polls_status(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test folder sensor polls for status updates."""
syncing_status = await hass.async_add_executor_job(
load_json_object_fixture, "folder_status.json", DOMAIN
)
syncing_status["state"] = "syncing"
mock_syncthing.database.status = AsyncMock(return_value=syncing_status)
future = dt_util.utcnow() + SCAN_INTERVAL + timedelta(seconds=1)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
entity_id = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SERVER_ID_SHORT_HA}-{FOLDER_ID}"
)
state = hass.states.get(entity_id) if entity_id else None
assert state is not None and state.state == "syncing"
async def test_folder_sensor_error_makes_unavailable(
hass: HomeAssistant,
entry: MockConfigEntry,
mock_syncthing: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test folder sensor becomes unavailable on status error."""
mock_syncthing.database.status = AsyncMock(side_effect=SyncthingError("Error"))
future = dt_util.utcnow() + SCAN_INTERVAL + timedelta(seconds=1)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
entity_id = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SERVER_ID_SHORT_HA}-{FOLDER_ID}"
)
state = hass.states.get(entity_id) if entity_id else None
assert state is not None and state.state == STATE_UNAVAILABLE