-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconftest.py
More file actions
320 lines (255 loc) · 11.5 KB
/
Copy pathconftest.py
File metadata and controls
320 lines (255 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
from __future__ import annotations
import io
import pathlib # noqa: TC003
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, Mock, patch
import aiohttp
import pytest
from anyio import Path
from homeassistant.components.mqtt.client import MQTT
from homeassistant.components.mqtt.models import DATA_MQTT, MqttData
from homeassistant.components.notify.const import DOMAIN
from homeassistant.components.notify.legacy import BaseNotificationService
from homeassistant.config_entries import ConfigEntryItems
from homeassistant.const import (
STATE_HOME,
STATE_NOT_HOME,
)
from homeassistant.core import HomeAssistant, ServiceRegistry, State, StateMachine, SupportsResponse, callback
from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.helpers.issue_registry import IssueRegistry
from homeassistant.helpers.template import Template
from pytest_httpserver import HTTPServer
from custom_components.supernotify.archive import NotificationArchive
from custom_components.supernotify.common import DupeChecker
from custom_components.supernotify.const import CONF_MOBILE_APP_ID, CONF_MOBILE_DEVICES, CONF_MOBILE_DISCOVERY, CONF_PERSON
from custom_components.supernotify.context import Context
from custom_components.supernotify.delivery import Delivery, DeliveryRegistry
from custom_components.supernotify.hass_api import HomeAssistantAPI
from custom_components.supernotify.media_grab import MediaStorage
from custom_components.supernotify.people import PeopleRegistry
from custom_components.supernotify.scenario import Scenario, ScenarioRegistry
from custom_components.supernotify.snoozer import Snoozer
from custom_components.supernotify.transport import Transport
from custom_components.supernotify.transports.chime import ChimeTransport
from custom_components.supernotify.transports.email import EmailTransport
from custom_components.supernotify.transports.mobile_push import MobilePushTransport
from tests.components.supernotify.doubles_lib import MockImageEntity
from tests.components.supernotify.hass_setup_lib import MockableHomeAssistant
if TYPE_CHECKING:
from collections.abc import Generator
from ssl import SSLContext
IMAGE_PATH: Path = Path("tests") / "components" / "supernotify" / "fixtures" / "media"
@dataclass
class TestImage:
__test__ = False
contents: bytes
path: Path
ext: str
mime_type: str
class MockAction(BaseNotificationService):
"""A test class for notification services."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.calls: list[tuple[str, str | None, str | None, dict[str, Any]]] = []
@callback
async def async_send_message(
self, message: str = "", title: str | None = None, target: str | None = None, **kwargs: dict[str, Any]
) -> None:
self.calls.append((message, title, target, kwargs))
@pytest.fixture
def mock_device_registry() -> DeviceRegistry:
mocked = Mock(spec=DeviceRegistry)
mocked.devices = {}
return mocked
@pytest.fixture
def mock_entity_registry() -> EntityRegistry:
return Mock(spec=EntityRegistry)
@pytest.fixture
def mock_issue_registry() -> IssueRegistry:
return Mock(spec=IssueRegistry)
@pytest.fixture
def mock_hass(
mock_device_registry: DeviceRegistry, mock_entity_registry: EntityRegistry, mock_issue_registry: IssueRegistry
) -> HomeAssistant:
hass = Mock(spec=MockableHomeAssistant)
hass.states = Mock(StateMachine)
hass.states.async_entity_ids.return_value = ["supernotify.test_1", "supernotify.test_2"]
hass.services = Mock(ServiceRegistry)
hass.services.async_call = AsyncMock()
hass.config.internal_url = "http://127.0.0.1:28123"
hass.config.external_url = "https://my.home"
hass.data = {}
hass.data["device_registry"] = mock_device_registry
hass.data["entity_registry"] = mock_entity_registry
hass.data["issue_registry"] = mock_issue_registry
hass.data[DATA_MQTT] = Mock(spec=MqttData)
hass.data[DATA_MQTT].client = AsyncMock(spec=MQTT)
hass.data[DATA_MQTT].client.connected = True
hass.config_entries._entries = ConfigEntryItems(hass)
hass.loop_thread_id = "99999"
return hass
@pytest.fixture
def mock_people_registry(mock_hass_api: HomeAssistantAPI) -> PeopleRegistry:
registry = Mock(spec=PeopleRegistry)
registry.hass_api = mock_hass_api
registry.people = {
"person.new_home_owner": {CONF_PERSON: "person.new_home_owner"},
"person.bidey_in": {
CONF_PERSON: "person.bidey_in",
CONF_MOBILE_DISCOVERY: False,
CONF_MOBILE_DEVICES: [{CONF_MOBILE_APP_ID: "mobile_app_iphone"}, {CONF_MOBILE_APP_ID: "mobile_app_nophone"}],
},
}
registry.determine_occupancy.return_value = {
STATE_HOME: [{CONF_PERSON: "person.bidey_in"}],
STATE_NOT_HOME: [{CONF_PERSON: "person.new_home_owner"}],
}
registry.enabled_recipients.return_value = registry.people.values()
return registry
@pytest.fixture
def mock_scenario_registry() -> ScenarioRegistry:
registry = AsyncMock(spec=ScenarioRegistry)
registry.scenarios = {}
return registry
@pytest.fixture
def mock_delivery_registry() -> DeliveryRegistry:
registry = AsyncMock(spec=DeliveryRegistry)
registry.deliveries = {}
registry.transports = {}
return registry
@pytest.fixture
def hass_api(hass: HomeAssistant) -> HomeAssistantAPI:
return HomeAssistantAPI(hass)
@pytest.fixture
async def hass_api_with_image(hass_api: HomeAssistantAPI, sample_image: TestImage) -> HomeAssistantAPI:
image_entity = MockImageEntity(sample_image.path)
await image_entity.load()
hass_api._hass.data["image"] = Mock(spec=EntityComponent) # type: ignore[attr-defined,union-attr]
hass_api._hass.data["image"].get_entity = Mock(return_value=image_entity) # type: ignore[attr-defined,union-attr]
return hass_api
@pytest.fixture
def mock_hass_api(mock_hass: HomeAssistant) -> HomeAssistantAPI:
mocked = AsyncMock(spec=HomeAssistantAPI)
mocked._hass = mock_hass
mocked._hass.get_state = Mock(return_value=Mock(spec=State))
mocked.template = Mock(return_value=Mock(spec=Template))
mock_http_session: AsyncMock = AsyncMock(spec=aiohttp.ClientSession)
mock_http_session.get = AsyncMock()
mocked.http_session.return_value = mock_http_session
mocked.create_job = AsyncMock()
return mocked
@pytest.fixture
def tmp_aiopath(tmp_path: pathlib.Path):
return Path(tmp_path)
@pytest.fixture
def mock_context(
mock_hass: HomeAssistant,
mock_people_registry: PeopleRegistry,
mock_scenario_registry: ScenarioRegistry,
mock_hass_api: HomeAssistantAPI,
mock_delivery_registry: DeliveryRegistry,
tmp_aiopath: Path,
) -> Context:
context = Mock(spec=Context)
context.scenario_registry = mock_scenario_registry
context.people_registry = mock_people_registry
context.delivery_registry = mock_delivery_registry
context.media_storage = Mock()
context.media_storage.media_path = tmp_aiopath / "media"
context.hass_api = mock_hass_api
context.cameras = {}
context.snoozer = Snoozer()
context._fallback_by_default = []
context.mobile_actions = {}
context.hass_api.internal_url = "http://hass-dev"
context.hass_api.external_url = "http://hass-dev.nabu.casa"
context.custom_template_path = tmp_aiopath / "templates"
mock_delivery_registry._deliveries = {
"plain_email": Delivery("plain_email", {}, EmailTransport(context)),
"mobile": Delivery("mobile", {}, MobilePushTransport(context)),
"chime": Delivery("chime", {}, ChimeTransport(context)),
}
return context
@pytest.fixture(scope="module", params=["jpeg", "png", "gif"])
def sample_image(request) -> TestImage:
path = IMAGE_PATH / f"example_image.{request.param}"
return TestImage(io.FileIO(path, "rb").readall(), path, request.param, f"image/{request.param}")
@pytest.fixture
def sample_jpeg(request) -> TestImage:
path = IMAGE_PATH / "example_image.jpeg"
return TestImage(io.FileIO(path, "rb").readall(), path, "jpeg", "image/jpeg")
@pytest.fixture
async def sample_image_entity_id(mock_hass_api: HomeAssistantAPI, sample_image: TestImage) -> str:
image_entity = MockImageEntity(sample_image.path)
await image_entity.load()
mock_hass_api.domain_entity.return_value = Mock(return_value=image_entity) # type: ignore[attr-defined]
return "image.testing"
@pytest.fixture
def deliveries(mock_context: Context) -> dict[str, Delivery]:
return mock_context.delivery_registry._deliveries
@pytest.fixture
def mock_notify(hass: HomeAssistant) -> MockAction:
mock_action: MockAction = MockAction()
hass.services.async_register(DOMAIN, "mock", mock_action, supports_response=SupportsResponse.NONE) # type: ignore
return mock_action
@pytest.fixture
def mock_transport() -> AsyncMock:
m = AsyncMock(spec=Transport)
m.name = "unit_test"
m.delivery_config = Mock(return_value={})
m.deliver = AsyncMock(return_value=True)
return m
@pytest.fixture
def dummy_scenario(mock_hass_api, mock_delivery_registry) -> Scenario:
return Scenario("mockery", {}, mock_delivery_registry, mock_hass_api)
@pytest.fixture
def unmocked_hass_api(hass: HomeAssistant) -> HomeAssistantAPI:
return HomeAssistantAPI(hass)
@pytest.fixture
async def unmocked_config(uninitialized_unmocked_config: Context, mock_hass: HomeAssistant) -> Context:
config = uninitialized_unmocked_config
await config.initialize()
config.people_registry.initialize()
hass_api = HomeAssistantAPI(mock_hass)
await config.delivery_registry.initialize(uninitialized_unmocked_config)
await config.scenario_registry.initialize(config.delivery_registry, {}, hass_api)
return config
@pytest.fixture
def uninitialized_unmocked_config(mock_hass_api: HomeAssistantAPI, tmp_path) -> Context:
people_registry = PeopleRegistry([], mock_hass_api)
scenario_registry = ScenarioRegistry({})
delivery_registry = DeliveryRegistry({})
dupe_checker = DupeChecker({})
media_storage = MediaStorage(tmp_path / "media", days=1)
archive = NotificationArchive({}, mock_hass_api)
return Context(
mock_hass_api, people_registry, scenario_registry, delivery_registry, dupe_checker, archive, media_storage, Snoozer()
)
@pytest.fixture
def local_server(httpserver_ssl_context: SSLContext | None, socket_enabled: Any) -> Generator[HTTPServer]:
"""pytest-socket will fail at fixture creation time, before test that uses it"""
server = HTTPServer(host="127.0.0.1", port=0, ssl_context=httpserver_ssl_context)
server.start()
yield server
server.clear() # type: ignore
if server.is_running():
server.stop() # type: ignore
@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations: Any) -> None:
"""Enable custom integrations in all tests."""
return
# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent
# notifications. These calls would fail without this fixture since the persistent_notification
# integration is never loaded during a test.
@pytest.fixture(name="skip_notifications", autouse=True)
def skip_notifications_fixture() -> Generator[None]:
"""Skip notification calls."""
with (
patch("homeassistant.components.persistent_notification.async_create"),
patch("homeassistant.components.persistent_notification.async_dismiss"),
):
yield