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 321 322 323 324 325 326 327 328 329 330 331 332 333
|
"""PyTest fixtures and test helpers."""
import copy
import json as _json
import re
from datetime import date, timedelta
import logging
from unittest.mock import AsyncMock
from deepmerge import Merger
from unittest import mock
from aiohttp import ClientResponseError, ClientSession
import pytest
from blebox_uniapi.box_types import get_latest_conf
from blebox_uniapi.session import ApiHost
from blebox_uniapi.box import Box
from blebox_uniapi.error import UnsupportedBoxVersion
_LOGGER = logging.getLogger(__name__)
retype = type(re.compile(""))
@pytest.fixture
def aioclient_mock():
return AsyncMock(spec=ClientSession)
def array_merge(config, path, base, nxt):
"""Replace an array element with the merge result of elements."""
if len(nxt):
if isinstance(nxt[0], dict):
index = 0
for item in nxt:
if not isinstance(item, dict):
raise NotImplementedError
my_merger.merge(base[index], item)
index += 1
return base
elif isinstance(nxt[0], int):
return [nxt[0]]
else:
raise NotImplementedError
my_merger = Merger(
# pass in a list of tuple, with the
# strategies you are looking to apply
# to each type.
[(list, [array_merge]), (dict, ["merge"])],
# next, choose the fallback strategies,
# applied to all other types:
["override"],
# finally, choose the strategies in
# the case where the types conflict:
["override"],
)
def jmerge(base, ext):
"""Create new fixtures by adjusting existing ones."""
result = copy.deepcopy(base)
my_merger.merge(result, _json.loads(ext))
return result
def future_date(delta_days=300):
"""Generate future date string in 'YYYYMMDD' format."""
future_date = date.today() + timedelta(days=delta_days)
return future_date.strftime("%Y%m%d")
HTTP_MOCKS = {}
def json_get_expect(mock, url, **kwargs):
json = kwargs["json"]
if mock not in HTTP_MOCKS:
HTTP_MOCKS[mock] = {}
HTTP_MOCKS[mock][url] = json
class EffectWhenGet:
def __init__(self, key):
self._key = key
def __call__(self, url, **kwargs):
data = HTTP_MOCKS[self._key][url]
response = _json.dumps(data).encode("utf-8")
status = 200
return AiohttpClientMockResponse("GET", url, status, response)
mock.get = AsyncMock(side_effect=EffectWhenGet(mock))
def json_post_expect(mock, url, **kwargs):
json = kwargs["json"]
params = kwargs["params"]
# TODO: check
# headers = kwargs.get("headers")
if mock not in HTTP_MOCKS:
HTTP_MOCKS[mock] = {}
if url not in HTTP_MOCKS[mock]:
HTTP_MOCKS[mock][url] = {}
HTTP_MOCKS[mock][url][params] = json
class EffectWhenPost:
def __init__(self, key):
self._key = key
def __call__(self, url, **kwargs):
# TODO: timeout
params = kwargs.get("data")
# TODO: better checking of params (content vs raw json)
data = HTTP_MOCKS[self._key][url][params]
response = _json.dumps(data).encode("utf-8")
status = 200
return AiohttpClientMockResponse("POST", url, status, response)
mock.post = AsyncMock(side_effect=EffectWhenPost(mock))
class DefaultBoxTest:
"""Base class with methods common to BleBox integration tests."""
IP = "172.0.0.1"
LOGGER = _LOGGER
async def async_entities(self, session):
"""Get a created entity at the given index."""
host = self.IP
port = 80
timeout = 2
api_host = ApiHost(host, port, timeout, session, None, self.LOGGER)
product = await Box.async_from_host(api_host)
return [
self.ENTITY_CLASS(feature) for feature in product.features[self.DEVCLASS]
]
async def allow_get_info(self, aioclient_mock, info=None):
"""Stub a HTTP GET request for the device state."""
data = self.DEVICE_INFO if info is None else info
json_get_expect(
aioclient_mock, f"http://{self.IP}:80/api/device/state", json=data
)
if (hasattr(self, "DEVICE_EXTENDED_INFO")) and (
path := getattr(self, "DEVICE_EXTENDED_INFO_PATH")
):
data = self.DEVICE_EXTENDED_INFO or info
json_get_expect(
aioclient_mock,
f"http://{self.IP}:80/{path.lstrip('/')}",
json=data,
)
def allow_get_state(self, aioclient_mock, data):
"""Stub a HTTP GET request for the product-specific state."""
json_get_expect(
aioclient_mock, f"http://{self.IP}:80/{self.DEV_INFO_PATH}", json=data
)
def allow_get(self, aioclient_mock, api_path, data):
"""Stub a HTTP GET request."""
json_get_expect(
aioclient_mock, f"http://{self.IP}:80/{api_path[1:]}", json=data
)
async def allow_post(self, code, aioclient_mock, api_path, post_data, response):
"""Stub a HTTP POST request."""
json_post_expect(
aioclient_mock,
f"http://{self.IP}:80/{api_path[1:]}",
params=post_data,
headers={"content-type": "application/json"},
json=response,
)
await code()
# TODO: rename?
async def updated(self, aioclient_mock, state, index=0):
"""Return an entry on which update has already been called."""
await self.allow_get_info(aioclient_mock)
entity = (await self.async_entities(aioclient_mock))[index]
self.allow_get_state(aioclient_mock, state)
await entity.async_update()
return entity
async def test_future_version(self, aioclient_mock):
"""
Test support for future versions, that is last supported entry in config type file.
"""
await self.allow_get_info(aioclient_mock, self.DEVICE_INFO_FUTURE)
entity = (await self.async_entities(aioclient_mock))[0]
assert entity._feature.product._config is get_latest_conf(
entity._feature.product.type
)
async def test_latest_version(self, aioclient_mock):
"""
Test support for latest versions, that is last supported entry in config type file.
"""
await self.allow_get_info(aioclient_mock, self.DEVICE_INFO_LATEST)
entity = (await self.async_entities(aioclient_mock))[0]
assert entity._feature.product._config is get_latest_conf(
entity._feature.product.type
)
async def test_unsupported_version(self, aioclient_mock):
"""Test version support."""
# only gateBox is same
if self.DEVICE_INFO != self.DEVICE_INFO_UNSUPPORTED:
await self.allow_get_info(aioclient_mock, self.DEVICE_INFO_UNSUPPORTED)
with pytest.raises(UnsupportedBoxVersion):
await self.async_entities(aioclient_mock)
async def test_unspecified_version(self, aioclient_mock):
"""
Test default_api_level when api level is not specified in device info.
"""
if self.DEVICE_INFO_UNSPECIFIED_API is not None:
await self.allow_get_info(aioclient_mock, self.DEVICE_INFO_UNSPECIFIED_API)
with pytest.raises(UnsupportedBoxVersion):
await self.async_entities(aioclient_mock)
class AiohttpClientMockResponse:
"""Mock Aiohttp client response."""
def __init__(
self, method, url, status, response, cookies=None, exc=None, headers=None
):
"""Initialize a fake response."""
self.method = method
self._url = url
self.status = status
self.response = response
self.exc = exc
self._headers = headers or {}
self._cookies = {}
if cookies:
for name, data in cookies.items():
cookie = mock.MagicMock()
cookie.value = data
self._cookies[name] = cookie
@property
def headers(self):
return self._headers
@property
def cookies(self):
return self._cookies
@property
def url(self):
return self._url
@property
def content_type(self):
return self._headers.get("content-type")
async def read(self):
return self.response
async def text(self, encoding="utf-8"):
return self.response.decode(encoding)
async def json(self, encoding="utf-8"):
return _json.loads(self.response.decode(encoding))
async def release(self):
pass
def raise_for_status(self):
"""Raise error if status is 400 or higher."""
if self.status >= 400:
request_info = mock.Mock(real_url="http://example.com")
# TODO: coverage
raise ClientResponseError(
request_info=request_info,
history=None,
code=self.status,
headers=self.headers,
)
def close(self):
pass
class CommonEntity:
def __init__(self, feature):
self._feature = feature
@property
def name(self):
return self._feature.full_name
@property
def unique_id(self):
return self._feature.unique_id
async def async_update(self):
await self._feature.async_update()
@property
def device_info(self):
product = self._feature.product
return {
"name": product.name,
"mac": product.unique_id,
"manufacturer": product.brand,
"model": product.model,
"sw_version": product.firmware_version,
}
|