Skip to content

Commit

Permalink
Use lru_cache for getting the UUID (#107)
Browse files Browse the repository at this point in the history
* Use lru_cache

* Fix typing
  • Loading branch information
Shutgun authored Mar 14, 2023
1 parent ba70c9f commit 2cb9d8e
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 40 deletions.
13 changes: 6 additions & 7 deletions devolo_home_control_api/mydevolo.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""my devolo"""
import logging
from functools import lru_cache
from typing import Any, Dict, List

import requests
Expand All @@ -19,7 +20,6 @@ def __init__(self) -> None:
self._logger = logging.getLogger(self.__class__.__name__)
self._user = ""
self._password = ""
self._uuid = ""
self._gateway_ids: List[int] = []

self.url = "https://www.mydevolo.com"
Expand All @@ -33,8 +33,8 @@ def user(self) -> str:
def user(self, user: str) -> None:
"""Invalidate uuid and gateway IDs on user name change."""
self._user = user
self._uuid = ""
self._gateway_ids = []
self.uuid.cache_clear()

@property
def password(self) -> str:
Expand All @@ -45,8 +45,8 @@ def password(self) -> str:
def password(self, password: str) -> None:
"""Invalidate uuid and gateway IDs on password change."""
self._password = password
self._uuid = ""
self._gateway_ids = []
self.uuid.cache_clear()

def credentials_valid(self) -> bool:
"""
Expand Down Expand Up @@ -140,14 +140,13 @@ def maintenance(self) -> bool:
self._logger.warning("devolo Home Control is in maintenance mode.")
return True

@lru_cache()
def uuid(self) -> str:
"""
The uuid is a central attribute in my devolo. Most URLs in the user's context contain it.
"""
if self._uuid == "":
self._logger.debug("Getting UUID")
self._uuid = self._call(f"{self.url.rstrip('/')}/v1/users/uuid")["uuid"]
return self._uuid
self._logger.debug("Getting UUID")
return self._call(f"{self.url.rstrip('/')}/v1/users/uuid")["uuid"]

def _call(self, url: str) -> Dict[str, Any]:
"""Make a call to any entry point with the user's context."""
Expand Down
24 changes: 15 additions & 9 deletions tests/fixtures/mydevolo.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
"""Fixtures used for mydevolo testing."""
from typing import Generator
from unittest.mock import patch

import pytest
from pytest_mock import MockerFixture

from devolo_home_control_api.mydevolo import Mydevolo, WrongCredentialsError, WrongUrlError

from ..mocks.mock_mydevolo import MockMydevolo


@pytest.fixture()
def mydevolo(request):
def mydevolo(request: pytest.FixtureRequest) -> Generator[Mydevolo, None, None]:
"""Create real mydevolo object with static test data."""
mydevolo_instance = Mydevolo()
mydevolo_instance._uuid = request.cls.user.get("uuid")
yield mydevolo_instance
with patch("devolo_home_control_api.mydevolo.Mydevolo.uuid", return_value=request.cls.user.get("uuid")):
yield Mydevolo()


@pytest.fixture()
def mock_mydevolo__call(mocker, request):
def mock_mydevolo__call(mocker: MockerFixture, request: pytest.FixtureRequest) -> None:
"""Mock calls to the mydevolo API."""
mock_mydevolo = MockMydevolo(request)
mocker.patch("devolo_home_control_api.mydevolo.Mydevolo._call", side_effect=mock_mydevolo._call)


@pytest.fixture()
def mock_mydevolo__call_raise_WrongUrlError(mocker):
def mock_mydevolo__call_raise_WrongUrlError(mocker: MockerFixture) -> None:
"""Respond with WrongUrlError on calls to the mydevolo API."""
mocker.patch("devolo_home_control_api.mydevolo.Mydevolo._call", side_effect=WrongUrlError)


@pytest.fixture()
def mock_mydevolo_uuid_raise_WrongCredentialsError(mocker):
def mock_mydevolo_uuid_raise_WrongCredentialsError(mydevolo: Mydevolo) -> Generator[Mydevolo, None, None]:
"""Respond with WrongCredentialsError on calls to the mydevolo API."""
mocker.patch("devolo_home_control_api.mydevolo.Mydevolo.uuid", side_effect=WrongCredentialsError)
with patch("devolo_home_control_api.mydevolo.Mydevolo.uuid", side_effect=WrongCredentialsError):
yield mydevolo


@pytest.fixture()
def mock_get_zwave_products(mocker):
def mock_get_zwave_products(mocker: MockerFixture) -> None:
"""Mock Z-Wave product information call to speed up tests."""
mocker.patch("devolo_home_control_api.mydevolo.Mydevolo.get_zwave_products", return_value={})
69 changes: 45 additions & 24 deletions tests/test_mydevolo.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,119 @@
"""Test mydevolo."""
import pytest

from devolo_home_control_api.mydevolo import GatewayOfflineError, Mydevolo, WrongCredentialsError, WrongUrlError


class TestMydevolo:
def test_credentials_valid(self, mydevolo):
def test_credentials_valid(self, mydevolo: Mydevolo) -> None:
"""Test credential validation."""
assert mydevolo.credentials_valid()

@pytest.mark.usefixtures("mock_mydevolo_uuid_raise_WrongCredentialsError")
def test_credentials_invalid(self, mydevolo):
def test_credentials_invalid(self, mydevolo: Mydevolo) -> None:
"""Test credential validation."""
assert not mydevolo.credentials_valid()

@pytest.mark.usefixtures("mock_mydevolo__call")
def test_gateway_ids(self, mydevolo):
def test_gateway_ids(self, mydevolo: Mydevolo) -> None:
"""Test getting gateway serial numbers."""
assert mydevolo.get_gateway_ids() == [self.gateway["id"]]

@pytest.mark.usefixtures("mock_mydevolo__call")
def test_gateway_ids_empty(self, mydevolo):
def test_gateway_ids_empty(self, mydevolo: Mydevolo) -> None:
"""Test raising on empty gateway list."""
with pytest.raises(IndexError):
mydevolo.get_gateway_ids()

@pytest.mark.usefixtures("mock_mydevolo__call")
def test_get_full_url(self, mydevolo):
def test_get_full_url(self, mydevolo: Mydevolo) -> None:
"""Test getting the remote mPRM URL."""
full_url = mydevolo.get_full_url(self.gateway["id"])
assert full_url == self.gateway["full_url"]

@pytest.mark.usefixtures("mock_mydevolo__call")
def test_get_gateway(self, mydevolo):
def test_get_gateway(self, mydevolo: Mydevolo) -> None:
"""Test getting gateway details."""
details = mydevolo.get_gateway(self.gateway["id"])
assert details.get("gatewayId") == self.gateway["id"]

@pytest.mark.usefixtures("mock_mydevolo__call_raise_WrongUrlError")
def test_get_gateway_invalid(self, mydevolo):
def test_get_gateway_invalid(self, mydevolo: Mydevolo) -> None:
"""Test raising on wrong gateway serial number."""
with pytest.raises(WrongUrlError):
mydevolo.get_gateway(self.gateway["id"])

@pytest.mark.usefixtures("mock_mydevolo__call")
def test_get_zwave_products(self, mydevolo):
def test_get_zwave_products(self, mydevolo: Mydevolo) -> None:
"""Test getting Z-Wave product information."""
device_info = mydevolo.get_zwave_products(manufacturer="0x0060", product_type="0x0001", product="0x000")
assert device_info.get("name") == "Everspring PIR Sensor SP814"

@pytest.mark.usefixtures("mock_mydevolo__call_raise_WrongUrlError")
def test_get_zwave_products_invalid(self, mydevolo):
def test_get_zwave_products_invalid(self, mydevolo: Mydevolo) -> None:
"""Test handling unknown Z-wave products."""
device_info = mydevolo.get_zwave_products(manufacturer="0x0070", product_type="0x0001", product="0x000")
assert device_info.get("name") == "Unknown"

@pytest.mark.usefixtures("mock_mydevolo__call")
@pytest.mark.parametrize("result", [True, False])
def test_maintenance(self, mydevolo, result):
def test_maintenance(self, mydevolo: Mydevolo, result: bool) -> None:
"""Test checking for maintenance mode."""
assert mydevolo.maintenance() == result

def test_set_password(self, mydevolo):
def test_set_password(self, mydevolo: Mydevolo) -> None:
"""Test setting a new password."""
mydevolo._gateway_ids = [self.gateway["id"]]
mydevolo.password = self.user["password"]

assert mydevolo._uuid == ""
assert mydevolo.uuid.cache_clear.call_count == 1
assert mydevolo._gateway_ids == []

def test_set_user(self, mydevolo):
def test_set_user(self, mydevolo: Mydevolo) -> None:
"""Test setting a new username."""
mydevolo._gateway_ids = [self.gateway["id"]]
mydevolo.user = self.user["username"]

assert mydevolo._uuid == ""
assert mydevolo.uuid.cache_clear.call_count == 1
assert mydevolo._gateway_ids == []

def test_get_user(self, mydevolo):
def test_get_user(self, mydevolo: Mydevolo) -> None:
"""Test getting the username."""
mydevolo.user = self.user["username"]
assert mydevolo.user == self.user["username"]

def test_get_password(self, mydevolo):
def test_get_password(self, mydevolo: Mydevolo) -> None:
"""Test getting the password."""
mydevolo.password = self.user["password"]
assert mydevolo.password == self.user["password"]

@pytest.mark.usefixtures("mock_mydevolo__call")
def test_uuid(self, mydevolo):
mydevolo._uuid = ""
def test_uuid(self) -> None:
"""Test getting the uuid."""
mydevolo = Mydevolo()
assert mydevolo.uuid() == self.user["uuid"]
assert mydevolo.uuid() == self.user["uuid"]
assert mydevolo.uuid.cache_info().hits == 1

@pytest.mark.usefixtures("mock_response_wrong_credentials_error")
def test_call_WrongCredentialsError(self):
def test_call_WrongCredentialsError(self) -> None:
"""Test raising on wrong credentials."""
mydevolo = Mydevolo()
with pytest.raises(WrongCredentialsError):
mydevolo._call("test")

@pytest.mark.usefixtures("mock_response_wrong_url_error")
def test_call_WrongUrlError(self):
def test_call_WrongUrlError(self) -> None:
"""Test raising on wrong URL."""
mydevolo = Mydevolo()
with pytest.raises(WrongUrlError):
mydevolo._call("test")

@pytest.mark.usefixtures("mock_response_gateway_offline")
def test_call_GatewayOfflineError(self, mydevolo):
def test_call_GatewayOfflineError(self, mydevolo: Mydevolo) -> None:
"""Test raising on offline gateway."""
with pytest.raises(GatewayOfflineError):
mydevolo._call("test")

@pytest.mark.usefixtures("mock_response_valid")
def test_call_valid(self, mydevolo):
def test_call_valid(self, mydevolo: Mydevolo) -> None:
"""Test valid calls."""
assert mydevolo._call("test").get("response") == "response"

0 comments on commit 2cb9d8e

Please sign in to comment.