From 8680ec365fd6e235c29a36ccf7dbf43b752581aa Mon Sep 17 00:00:00 2001 From: Robert Alfaro Date: Mon, 29 Jan 2024 15:27:55 -0800 Subject: [PATCH 1/2] First pass fixing various unit tests --- aiopvapi/helpers/api_base.py | 6 +++++- aiopvapi/helpers/constants.py | 1 + requirements-dev.txt | 1 - tests/fake_server.py | 2 +- tests/test_apiresource.py | 10 +++++++--- tests/test_hub.py | 19 ++++++++++--------- tests/test_room.py | 9 +++++---- tests/test_scene.py | 13 ++++++++----- tests/test_scene_members.py | 2 +- tests/test_shade.py | 28 ++++++++++++++++------------ 10 files changed, 54 insertions(+), 37 deletions(-) diff --git a/aiopvapi/helpers/api_base.py b/aiopvapi/helpers/api_base.py index 101f9be..e170aa1 100644 --- a/aiopvapi/helpers/api_base.py +++ b/aiopvapi/helpers/api_base.py @@ -60,7 +60,11 @@ def _parse(self, *keys, converter=None, data=None): _LOGGER.debug("Key '%s' missing", err) return None if converter: - return converter(val) + try: + return converter(val) + except UnicodeDecodeError as err: + _LOGGER.error("UnicodeDecodeError converting '%s', err=%s", val, err) + return None return val diff --git a/aiopvapi/helpers/constants.py b/aiopvapi/helpers/constants.py index fa30181..bd59bf5 100644 --- a/aiopvapi/helpers/constants.py +++ b/aiopvapi/helpers/constants.py @@ -66,6 +66,7 @@ FWVERSION = "fwversion" USER_DATA = "userData" +MID_POSITION_V2 = 32767 MAX_POSITION_V2 = 65535 SHADE_BATTERY_STRENGTH = "batteryStrength" diff --git a/requirements-dev.txt b/requirements-dev.txt index 26b527b..f22e2bd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,3 @@ nox pytest-cov python-coveralls wheel -json diff --git a/tests/fake_server.py b/tests/fake_server.py index 77f3d04..f9d3803 100644 --- a/tests/fake_server.py +++ b/tests/fake_server.py @@ -208,7 +208,7 @@ def __init__(self, *, loop, api_version=2): web.put("/api/shades/{shade_id}", self.add_shade_to_room), web.delete("/api/sceneMembers", self.remove_shade_from_scene), web.get("/api/fwversion", self.get_fwversion), - web.get("/userdata", self.get_user_data), + web.get("/api/userdata", self.get_user_data), ] ) self.runner = None diff --git a/tests/test_apiresource.py b/tests/test_apiresource.py index ba3ae91..cdaeaca 100644 --- a/tests/test_apiresource.py +++ b/tests/test_apiresource.py @@ -1,6 +1,7 @@ from unittest.mock import Mock from aiopvapi.helpers.api_base import ApiResource, ApiEntryPoint +from aiopvapi.helpers.aiorequest import AioRequest from tests.fake_server import TestFakeServer, FAKE_BASE_URL @@ -13,9 +14,10 @@ def get_resource_uri(self): def get_resource(self): """Get the resource being tested.""" - _request = Mock() + _request = Mock(spec=AioRequest) _request.hub_ip = FAKE_BASE_URL _request.api_version = 2 + _request.api_path = "api" return ApiResource(_request, "base", self.get_resource_raw_data()) def setUp(self): @@ -31,7 +33,7 @@ def test_id_property(self): def test_full_path(self): self.assertEqual( - self.resource._base_path, "http://{}/api/base".format(FAKE_BASE_URL) + self.resource.base_path, "http://{}/api/base".format(FAKE_BASE_URL) ) def test_name_property(self): @@ -60,11 +62,12 @@ def get_resource(self): _request = Mock() _request.hub_ip = FAKE_BASE_URL _request.api_version = 3 + _request.api_path = "home" return ApiResource(_request, "base", self.get_resource_raw_data()) def test_full_path(self): self.assertEqual( - self.resource._base_path, "http://{}/home/base".format(FAKE_BASE_URL) + self.resource.base_path, "http://{}/home/base".format(FAKE_BASE_URL) ) # def test_delete_200(self, mocked): @@ -184,6 +187,7 @@ def test_clean_names(): req = Mock() req.hub_ip = "123.123.123" req.api_version = 2 + req.api_path = "api" api = ApiEntryPoint(req, "abc") try: api._sanitize_resources(test_data1) diff --git a/tests/test_hub.py b/tests/test_hub.py index 2c9d6cc..e8564c3 100644 --- a/tests/test_hub.py +++ b/tests/test_hub.py @@ -20,9 +20,9 @@ def fake_aiorequest(): def test_version(): version1 = Version(123, 456, 789) - assert version1._build == 123 - assert version1._revision == 456 - assert version1._sub_revision == 789 + assert version1._revision == 123 + assert version1._sub_revision == 456 + assert version1._build == 789 version2 = Version(123, 456, 789) @@ -47,14 +47,14 @@ async def go(): hub = self.loop.run_until_complete(go()) - assert hub._base_path == "http://" + FAKE_BASE_URL + "/api" + assert hub.base_path == "http://" + FAKE_BASE_URL + "/api" # self.request.get.mock.assert_called_once_with(FAKE_BASE_URL + "/userdata") data = json.loads(USER_DATA_VALUE) assert hub.main_processor_info == data["userData"]["firmware"]["mainProcessor"] - assert hub.main_processor_version == "BUILD: 395 REVISION: 2 SUB_REVISION: 0" - assert hub.radio_version == "BUILD: 1307 REVISION: 2 SUB_REVISION: 0" + assert hub.main_processor_version == Version(2, 0, 395) # "REVISION: 2 SUB_REVISION: 0 BUILD: 395" + assert hub.radio_version == [Version(2, 0, 1307)] # ["REVISION: 2 SUB_REVISION: 0 BUILD: 1307"] assert hub.ssid == "cisco789" assert hub.name == "Hubby" @@ -73,12 +73,13 @@ async def go(): hub = self.loop.run_until_complete(go()) - assert hub._base_path == "http://" + FAKE_BASE_URL + "/gateway" + assert hub.base_path == "http://" + FAKE_BASE_URL + "/home" # self.request.get.mock.assert_called_once_with(FAKE_BASE_URL + "/userdata") data = json.loads(USER_DATA_VALUE) assert hub.main_processor_info == data["userData"]["firmware"]["mainProcessor"] - assert hub.main_processor_version == "BUILD: 395 REVISION: 2 SUB_REVISION: 0" - assert hub.radio_version == ["BUILD: 1307 REVISION: 2 SUB_REVISION: 0"] + assert hub.main_processor_version == Version(2, 0, 395) # "REVISION: 2 SUB_REVISION: 0 BUILD: 395" + assert hub.radio_version == [Version(2, 0, 1307)] # ["REVISION: 2 SUB_REVISION: 0 BUILD: 1307"] assert hub.ssid == "cisco789" + assert hub.name == "00:26:74:af:fd:ae" diff --git a/tests/test_room.py b/tests/test_room.py index 718b4ab..974c552 100644 --- a/tests/test_room.py +++ b/tests/test_room.py @@ -26,23 +26,24 @@ def get_resource(self): _request = Mock() _request.hub_ip = FAKE_BASE_URL _request.api_version = 2 + _request.api_path = "api" return Room(ROOM_RAW_DATA, _request) def test_full_path(self): self.assertEqual( - self.resource._base_path, "http://{}/api/rooms".format(FAKE_BASE_URL) + self.resource.base_path, "http://{}/api/rooms".format(FAKE_BASE_URL) ) def test_name_property(self): # No name_unicode, so base64 encoded is returned - self.assertEqual("RGluaW5nIFJvb20=", self.resource.name) + self.assertEqual("Dining Room", self.resource.name) def test_delete_room_success(self): """Tests deleting a room""" async def go(): await self.start_fake_server() - room = Room(self.get_resource_raw_data(), self.request) + room = self.get_resource() resp = await room.delete() return resp @@ -54,7 +55,7 @@ def test_delete_room_fail(self): async def go(): await self.start_fake_server() - room = Room(self.get_resource_raw_data(), self.request) + room = self.get_resource() room._resource_path += "1" resp = await room.delete() return resp diff --git a/tests/test_scene.py b/tests/test_scene.py index 4ee0248..f45ea2d 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -1,6 +1,7 @@ from unittest.mock import Mock from aiopvapi.helpers.aiorequest import PvApiResponseStatusError +from aiopvapi.helpers.aiorequest import AioRequest from aiopvapi.resources.scene import Scene from tests.fake_server import FAKE_BASE_URL @@ -24,21 +25,23 @@ def get_resource_uri(self): return "http://{}/api/scenes/37217".format(FAKE_BASE_URL) def get_resource(self): - _request = Mock() + _request = Mock(spec=AioRequest) _request.hub_ip = FAKE_BASE_URL _request.api_version = 2 + _request.api_path = "api" return Scene(SCENE_RAW_DATA, _request) def test_name_property(self): # No name_unicode, so base64 encoded is returned - self.assertEqual("RGluaW5nIFZhbmVzIE9wZW4=", self.resource.name) + # "RGluaW5nIFZhbmVzIE9wZW4=" + self.assertEqual("Dining Vanes Open", self.resource.name) def test_room_id_property(self): self.assertEqual(26756, self.resource.room_id) def test_full_path(self): self.assertEqual( - self.resource._base_path, + self.resource.base_path, "http://{}/api/scenes".format(FAKE_BASE_URL), ) @@ -47,7 +50,7 @@ def test_activate_200(self): async def go(): await self.start_fake_server() - scene = Scene({"id": 10}, self.request) + scene = self.get_resource() resp = await scene.activate() return resp @@ -59,7 +62,7 @@ def test_activate_404(self): async def go(): await self.start_fake_server() - scene = Scene({"id": 11}, self.request) + scene = self.get_resource() resp = await scene.activate() return resp diff --git a/tests/test_scene_members.py b/tests/test_scene_members.py index 545654f..e137396 100644 --- a/tests/test_scene_members.py +++ b/tests/test_scene_members.py @@ -21,7 +21,7 @@ class TestSceneMembers(unittest.TestCase): def setUp(self): self.fake_ip = '123.123.123.123' - @mock.patch('aiopvapi.helpers.aiorequest.check_response', new=AsyncMock()) + @mock.patch('aiopvapi.helpers.aiorequest.AioRequest.check_response', new=AsyncMock()) def test_remove_shade_from_scene(self): """Tests create new scene.""" loop = asyncio.get_event_loop() diff --git a/tests/test_shade.py b/tests/test_shade.py index c7147d6..241b540 100644 --- a/tests/test_shade.py +++ b/tests/test_shade.py @@ -1,7 +1,7 @@ from unittest.mock import Mock from aiopvapi.helpers.aiorequest import AioRequest -from aiopvapi.resources.shade import BaseShade, shade_type +from aiopvapi.resources.shade import BaseShade, ShadeType from aiopvapi.helpers.constants import ( ATTR_POSKIND1, ATTR_POSITION1, @@ -24,7 +24,7 @@ "type": 6, "batteryStatus": 0, "batteryStrength": 0, - "name": "UmlnaHQ=", + "name": "UmlnaHQ=", # "Right" "roomId": 12372, "groupId": 18480, "positions": {"posKind1": 1, "position1": 0}, @@ -43,21 +43,23 @@ def get_resource(self): _request = Mock(spec=AioRequest) _request.hub_ip = FAKE_BASE_URL _request.api_version = 2 - return BaseShade(SHADE_RAW_DATA, shade_type(0, ""), _request) + _request.api_path = "api" + return BaseShade(SHADE_RAW_DATA, ShadeType(0, "undefined type"), _request) def test_full_path(self): self.assertEqual( - self.resource._base_path, "http://{}/api/shades".format(FAKE_BASE_URL) + self.resource.base_path, "http://{}/api/shades".format(FAKE_BASE_URL) ) def test_name_property(self): - # No name_unicode, so base64 encoded is returned - self.assertEqual("UmlnaHQ=", self.resource.name) + # No name_unicode, although name is base64 encoded + # thus base64 decoded is returned + self.assertEqual("Right", self.resource.name) def test_add_shade_to_room(self): async def go(): await self.start_fake_server() - shade = BaseShade({"id": 111}, shade_type(0, ""), self.request) + shade = self.get_resource() res = await shade.add_shade_to_room(123) return res @@ -96,21 +98,23 @@ def get_resource(self): _request = Mock(spec=AioRequest) _request.hub_ip = FAKE_BASE_URL _request.api_version = 3 - return BaseShade(SHADE_RAW_DATA, shade_type(0, ""), _request) + _request.api_path = "home" + return BaseShade(SHADE_RAW_DATA, ShadeType(0, "undefined type"), _request) def test_full_path(self): self.assertEqual( - self.resource._base_path, "http://{}/home/shades".format(FAKE_BASE_URL) + self.resource.base_path, "http://{}/home/shades".format(FAKE_BASE_URL) ) def test_name_property(self): - # No name_unicode, so base64 encoded is returned - self.assertEqual("UmlnaHQ=", self.resource.name) + # No name_unicode, although name is base64 encoded + # thus base64 decoded is returned + self.assertEqual("Right", self.resource.name) def test_add_shade_to_room(self): async def go(): await self.start_fake_server() - shade = BaseShade({"id": 111}, shade_type(0, ""), self.request) + shade = self.get_resource() res = await shade.add_shade_to_room(123) return res From 7e1e7e6774a9233ebae1484b7147ecd55493dc28 Mon Sep 17 00:00:00 2001 From: Robert Alfaro Date: Wed, 31 Jan 2024 18:20:29 -0800 Subject: [PATCH 2/2] Fix remaining tests for rooms, scenes, shades --- aiopvapi/resources/scene.py | 2 +- tests/test_room.py | 24 ++++++++++++++++++++---- tests/test_scene.py | 30 +++++++++++++++++++++++------- tests/test_shade.py | 15 ++++++++------- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/aiopvapi/resources/scene.py b/aiopvapi/resources/scene.py index 6f341da..b41dd86 100644 --- a/aiopvapi/resources/scene.py +++ b/aiopvapi/resources/scene.py @@ -42,7 +42,7 @@ async def activate(self) -> list[int]: _val = await self.request.get( self.base_path, params={ATTR_SCENE_ID: self._id} ) - # v2 returns format {'sceneIds': ids} so flattening the list to align v3 + # v2 returns format {'shadeIds': ids} so flattening the list to align v3 _val = _val.get(ATTR_SHADE_IDS) # should return an array of ID's that belong to the scene return _val diff --git a/tests/test_room.py b/tests/test_room.py index 974c552..58b0bc9 100644 --- a/tests/test_room.py +++ b/tests/test_room.py @@ -1,9 +1,13 @@ +import asyncio +from aiohttp import ClientResponse from unittest.mock import Mock -from aiopvapi.helpers.aiorequest import PvApiResponseStatusError +from aiopvapi.helpers.aiorequest import AioRequest, PvApiResponseStatusError from aiopvapi.resources.room import Room from tests.fake_server import FAKE_BASE_URL from tests.test_apiresource import TestApiResource +from tests.test_scene_members import AsyncMock + ROOM_RAW_DATA = { "order": 2, @@ -23,7 +27,7 @@ def get_resource_uri(self): return "http://{}/api/rooms/26756".format(FAKE_BASE_URL) def get_resource(self): - _request = Mock() + _request = Mock(spec=AioRequest) _request.hub_ip = FAKE_BASE_URL _request.api_version = 2 _request.api_path = "api" @@ -35,7 +39,8 @@ def test_full_path(self): ) def test_name_property(self): - # No name_unicode, so base64 encoded is returned + # No name_unicode, although name is base64 encoded + # thus base64 decoded is returned self.assertEqual("Dining Room", self.resource.name) def test_delete_room_success(self): @@ -55,8 +60,19 @@ def test_delete_room_fail(self): async def go(): await self.start_fake_server() - room = self.get_resource() + # room = self.get_resource() + + loop = asyncio.get_event_loop() + request = AioRequest(FAKE_BASE_URL, loop, api_version=2) + + response = Mock(spec=ClientResponse) + response.status = 500 + response.release = AsyncMock(return_value=None) + request.websession.delete = AsyncMock(return_value=response) + + room = Room(ROOM_RAW_DATA, request) room._resource_path += "1" + resp = await room.delete() return resp diff --git a/tests/test_scene.py b/tests/test_scene.py index f45ea2d..e7f2212 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -1,15 +1,18 @@ +import asyncio +from aiohttp import ClientResponse from unittest.mock import Mock -from aiopvapi.helpers.aiorequest import PvApiResponseStatusError -from aiopvapi.helpers.aiorequest import AioRequest +from aiopvapi.helpers.aiorequest import AioRequest, PvApiResponseStatusError from aiopvapi.resources.scene import Scene from tests.fake_server import FAKE_BASE_URL from tests.test_apiresource import TestApiResource +from tests.test_scene_members import AsyncMock + SCENE_RAW_DATA = { "roomId": 26756, - "name": "RGluaW5nIFZhbmVzIE9wZW4=", + "name": "RGluaW5nIFZhbmVzIE9wZW4=", # "Dining Vanes Open" "colorId": 0, "iconId": 0, "id": 37217, @@ -32,8 +35,8 @@ def get_resource(self): return Scene(SCENE_RAW_DATA, _request) def test_name_property(self): - # No name_unicode, so base64 encoded is returned - # "RGluaW5nIFZhbmVzIE9wZW4=" + # No name_unicode, although name is base64 encoded + # thus base64 decoded is returned self.assertEqual("Dining Vanes Open", self.resource.name) def test_room_id_property(self): @@ -51,18 +54,31 @@ def test_activate_200(self): async def go(): await self.start_fake_server() scene = self.get_resource() + scene.request.get = AsyncMock(return_value={'shadeIds': [10]}) resp = await scene.activate() return resp resp = self.loop.run_until_complete(go()) - self.assertEqual(resp["id"], 10) + self.assertEqual(resp[0], 10) def test_activate_404(self): """Test scene activation""" async def go(): await self.start_fake_server() - scene = self.get_resource() + # scene = self.get_resource() + + loop = asyncio.get_event_loop() + request = AioRequest(FAKE_BASE_URL, loop, api_version=2) + + response = Mock(spec=ClientResponse) + response.status = 404 + response.release = AsyncMock(return_value=None) + request.websession.get = AsyncMock(return_value=response) + + scene = Scene(SCENE_RAW_DATA, request) + scene._resource_path += "1" + resp = await scene.activate() return resp diff --git a/tests/test_shade.py b/tests/test_shade.py index 241b540..3e163fe 100644 --- a/tests/test_shade.py +++ b/tests/test_shade.py @@ -1,7 +1,7 @@ from unittest.mock import Mock from aiopvapi.helpers.aiorequest import AioRequest -from aiopvapi.resources.shade import BaseShade, ShadeType +from aiopvapi.resources.shade import BaseShade, ShadeType, ShadePosition from aiopvapi.helpers.constants import ( ATTR_POSKIND1, ATTR_POSITION1, @@ -69,21 +69,22 @@ async def go(): def test_convert_g2(self): shade = self.get_resource() self.assertEqual( - shade.convert_to_v2({ATTR_PRIMARY: MAX_POSITION}), - {ATTR_POSITION1: MAX_POSITION_V2, ATTR_POSKIND1: 1}, + shade.percent_to_api(MAX_POSITION, ATTR_PRIMARY), + MAX_POSITION_V2 ) self.assertEqual( - shade.convert_to_v2({ATTR_TILT: MID_POSITION}), - {ATTR_POSITION1: MID_POSITION_V2, ATTR_POSKIND1: 3}, + shade.percent_to_api(MID_POSITION, ATTR_TILT), + MID_POSITION_V2 ) + positions = shade.structured_to_raw(ShadePosition(MAX_POSITION, None, MID_POSITION))['shade']['positions'] self.assertEqual( - shade.convert_to_v2({ATTR_PRIMARY: MAX_POSITION, ATTR_TILT: MID_POSITION}), + positions, { ATTR_POSKIND1: 1, ATTR_POSITION1: MAX_POSITION_V2, ATTR_POSKIND2: 3, ATTR_POSITION2: MID_POSITION_V2, - }, + } )