mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 02:24:51 -05:00
Adding tests to syncthing (#170892)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
# ---
|
||||
@@ -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"}
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user