From 3f589ee76d70299083ed1ef58dda95eda92ca84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B6rrle?= Date: Fri, 1 Dec 2023 17:04:34 +0100 Subject: [PATCH] mark old tests as legacy and add some new tests for utils --- tests/legacy_test_hvac.py | 127 +++++++ tests/legacy_test_lock_unlock.py | 107 ++++++ tests/legacy_test_myt.py | 527 ++++++++++++++++++++++++++ tests/legacy_test_myt_online.py | 38 ++ tests/legacy_test_parking_location.py | 27 ++ tests/legacy_test_statistics.py | 20 + tests/legacy_test_utils.py | 74 ++++ tests/legacy_test_utils_logs.py | 63 +++ tests/legacy_test_vehicle.py | 210 ++++++++++ tests/legcy_test_sensors.py | 225 +++++++++++ tests/test_client.py | 30 ++ tests/test_utils/test_conversions.py | 101 +++++ tests/test_utils/test_formatter.py | 72 ++++ tests/test_utils/test_locale.py | 57 +++ tests/test_utils/test_logs.py | 89 +++++ 15 files changed, 1767 insertions(+) create mode 100644 tests/legacy_test_hvac.py create mode 100644 tests/legacy_test_lock_unlock.py create mode 100644 tests/legacy_test_myt.py create mode 100644 tests/legacy_test_myt_online.py create mode 100644 tests/legacy_test_parking_location.py create mode 100644 tests/legacy_test_statistics.py create mode 100644 tests/legacy_test_utils.py create mode 100644 tests/legacy_test_utils_logs.py create mode 100644 tests/legacy_test_vehicle.py create mode 100644 tests/legcy_test_sensors.py create mode 100644 tests/test_client.py create mode 100644 tests/test_utils/test_conversions.py create mode 100644 tests/test_utils/test_formatter.py create mode 100644 tests/test_utils/test_locale.py create mode 100644 tests/test_utils/test_logs.py diff --git a/tests/legacy_test_hvac.py b/tests/legacy_test_hvac.py new file mode 100644 index 00000000..40d5a1ad --- /dev/null +++ b/tests/legacy_test_hvac.py @@ -0,0 +1,127 @@ +"""pytest tests for mytoyota.models.hvac.Hvac.""" + +from mytoyota.models.hvac import Hvac + + +class TestHvac: + """pytest functions to test Hvac.""" + + @staticmethod + def _create_example_data(): + """Create hvac with predefined data.""" + return Hvac( + { + "currentTemperatureIndication": { + "timestamp": "2020-10-16T03:50:15Z", + "unit": "string", + "value": 22, + }, + "targetTemperature": { + "timestamp": "2020-10-16T03:50:15Z", + "unit": "string", + "value": 21, + }, + "startedAt": "", + "status": "", + "type": "", + "duration": 1, + "options": { + "frontDefogger": "", + "frontDriverSeatHeater": "", + "frontPassengerSeatHeater": "", + "mirrorHeater": "", + "rearDefogger": "", + "rearDriverSeatHeater": "", + "rearPassengerSeatHeater": "", + "steeringHeater": "", + }, + "commandId": "", + }, + ) + + @staticmethod + def _create_example_legacy_data(): + """Create legacy hvac with predefined data.""" + return Hvac( + { + "BlowerStatus": 0, + "FrontDefoggerStatus": 0, + "InsideTemperature": 22, + "LatestAcStartTime": "2020-10-16T03:50:15Z", + "RearDefoggerStatus": 0, + "RemoteHvacMode": 0, + "RemoteHvacProhibitionSignal": 1, + "SettingTemperature": 21, + "TemperatureDisplayFlag": 0, + "Temperaturelevel": 29, + }, + legacy=True, + ) + + def test_hvac(self): + """Test Hvac.""" + hvac = self._create_example_data() + + assert hvac.legacy is False + + assert hvac.current_temperature == 22 + assert hvac.target_temperature == 21 + assert hvac.started_at == "" + assert hvac.status == "" + assert hvac.type == "" + assert hvac.duration == 1 + assert hvac.command_id == "" + assert isinstance(hvac.options, dict) + assert hvac.options == { + "frontDefogger": "", + "frontDriverSeatHeater": "", + "frontPassengerSeatHeater": "", + "mirrorHeater": "", + "rearDefogger": "", + "rearDriverSeatHeater": "", + "rearPassengerSeatHeater": "", + "steeringHeater": "", + } + + assert hvac.last_updated == "2020-10-16T03:50:15Z" + + assert hvac.front_defogger_is_on is None + assert hvac.rear_defogger_is_on is None + assert hvac.blower_on is None + + def test_hvac_legacy(self): + """Test legacy Hvac.""" + hvac = self._create_example_legacy_data() + + assert hvac.legacy is True + + assert hvac.current_temperature == 22 + assert hvac.target_temperature == 21 + assert hvac.blower_on == 0 + assert hvac.front_defogger_is_on is False + assert hvac.rear_defogger_is_on is False + assert hvac.last_updated is None + + assert hvac.started_at is None + assert hvac.status is None + assert hvac.type is None + assert hvac.duration is None + assert hvac.options is None + assert hvac.command_id is None + + def test_hvac_no_data(self): + """Test Hvac with no initialization data.""" + hvac = Hvac({}) + + assert hvac.legacy is False + + assert hvac.current_temperature is None + assert hvac.target_temperature is None + assert hvac.started_at is None + assert hvac.status is None + assert hvac.type is None + assert hvac.duration is None + assert hvac.command_id is None + assert hvac.options is None + + assert hvac.last_updated is None diff --git a/tests/legacy_test_lock_unlock.py b/tests/legacy_test_lock_unlock.py new file mode 100644 index 00000000..a2797fa7 --- /dev/null +++ b/tests/legacy_test_lock_unlock.py @@ -0,0 +1,107 @@ +"""pytest tests for mytoyota.client.MyT sending lock/unlock requests.""" +import asyncio +from datetime import datetime + +import pytest + +from mytoyota.exceptions import ToyotaActionNotSupportedError +from mytoyota.models.lock_unlock import ( + VehicleLockUnlockActionResponse, + VehicleLockUnlockStatusResponse, +) +from tests.test_myt import TestMyTHelper + + +class TestLockUnlock(TestMyTHelper): + """Pytest functions to test locking and unlocking.""" + + successful_lock_request_id = "d4f873d2-5da2-494f-a6d9-6e56d18d2ce9" + failed_lock_request_id = "14f873d2-5da2-494f-a6d9-6e56d18d2ce9" + pending_lock_request_id = "24f873d2-5da2-494f-a6d9-6e56d18d2ce9" + + def test_send_lock_request(self): + """Test sending the lock request.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + result = asyncio.get_event_loop().run_until_complete(myt.set_lock_vehicle(vehicle["vin"])) + assert isinstance(result, VehicleLockUnlockActionResponse) + assert result.raw_json == { + "id": self.pending_lock_request_id, + "status": "inprogress", + "type": "controlLock", + } + assert result.request_id == self.pending_lock_request_id + assert result.status == "inprogress" + assert result.type == "controlLock" + + def test_send_unlock_request(self): + """Test sending the unlock request.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + result = asyncio.get_event_loop().run_until_complete(myt.set_unlock_vehicle(vehicle["vin"])) + assert isinstance(result, VehicleLockUnlockActionResponse) + assert result.raw_json == { + "id": self.pending_lock_request_id, + "status": "inprogress", + "type": "controlLock", + } + + def test_get_successful_lock_status(self): + """Test getting the lock status.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + result = asyncio.get_event_loop().run_until_complete( + myt.get_lock_status(vehicle["vin"], self.successful_lock_request_id), + ) + assert isinstance(result, VehicleLockUnlockStatusResponse) + assert result.raw_json == { + "id": self.successful_lock_request_id, + "status": "completed", + "requestTimestamp": "2022-10-22T08:49:20.071Z", + "type": "controlLock", + } + assert result.request_id == self.successful_lock_request_id + assert result.status == "completed" + assert result.type == "controlLock" + assert result.request_timestamp == datetime(2022, 10, 22, 8, 49, 20, 71000) + assert not result.is_in_progress + assert not result.is_error + assert result.is_success + + def test_get_failed_lock_status(self): + """Test getting the lock status.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + result = asyncio.get_event_loop().run_until_complete( + myt.get_lock_status(vehicle["vin"], self.failed_lock_request_id), + ) + assert isinstance(result, VehicleLockUnlockStatusResponse) + assert result.raw_json == { + "id": self.failed_lock_request_id, + "status": "error", + "errorCode": "LU0004", + "requestTimestamp": "2022-10-22T08:49:20.071Z", + "type": "controlLock", + } + assert result.request_id == self.failed_lock_request_id + assert result.status == "error" + assert result.error_code == "LU0004" + assert result.type == "controlLock" + assert result.request_timestamp == datetime(2022, 10, 22, 8, 49, 20, 71000) + assert not result.is_in_progress + assert result.is_error + assert not result.is_success + + def test_set_lock_vehicle_unsupported(self): + """Test sending the lock request to a vehicle for which it is not supported.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 1111111) + with pytest.raises(ToyotaActionNotSupportedError): + asyncio.get_event_loop().run_until_complete(myt.set_lock_vehicle(vehicle["vin"])) + + def test_set_unlock_vehicle_unsupported(self): + """Test sending the lock request to a vehicle for which it is not supported.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 1111111) + with pytest.raises(ToyotaActionNotSupportedError): + asyncio.get_event_loop().run_until_complete(myt.set_unlock_vehicle(vehicle["vin"])) diff --git a/tests/legacy_test_myt.py b/tests/legacy_test_myt.py new file mode 100644 index 00000000..05b4c7e3 --- /dev/null +++ b/tests/legacy_test_myt.py @@ -0,0 +1,527 @@ +"""pytest tests for mytoyota.client.MyT.""" + +import asyncio +import datetime +import json +import os +import os.path +import re +from typing import Optional, Union + +import arrow +import pytest # pylint: disable=import-error + +from mytoyota.client import MyT +from mytoyota.exceptions import ( + ToyotaActionNotSupportedError, + ToyotaInternalError, + ToyotaInvalidUsernameError, + ToyotaRegionNotSupportedError, +) +from mytoyota.models.trip import DetailedTrip, Trip, TripEvent + + +class OfflineController: + """Provides a Controller class that can be used for testing.""" + + def __init__( # noqa: PLR0913 + self, + locale: str, + region: str, + username: str, + password: str, + brand: str, + uuid: Optional[str] = None, + ) -> None: + """Initialise offline controller class.""" + self._locale = locale + self._region = region + self._username = username + self._password = password + self._brand = brand + self._uuid = uuid + + @property + def uuid(self) -> str: + """Returns uuid.""" + return "_OfflineController_" + + async def first_login(self) -> None: + """Perform first login.""" + # This is no-operation + + def _load_from_file(self, filename: str): + """Load and return data structure from specified JSON filename.""" + with open(filename, encoding="UTF-8") as json_file: + return json.load(json_file) + + # Disables pylint warning about too many statements and branches when matching API paths + async def request( # noqa: PLR0912, PLR0913, PLR0915 + self, + method: str, + endpoint: str, + base_url: Optional[str] = None, + body: Optional[dict] = None, + params: Optional[dict] = None, + headers: Optional[dict] = None, + ) -> Union[dict, list, None]: + """Shared request method.""" + if method not in ("GET", "POST", "PUT", "DELETE"): + raise ToyotaInternalError("Invalid request method provided") # pragma: no cover + + _ = base_url + + data_files = os.path.join(os.path.curdir, "tests", "data") + + response = None + + match = re.match(r"/api/users/.*/vehicles/.*", endpoint) + if match: + # A new alias is set + # We should return a predefined dictionary + response = {"id": str(body["id"]), "alias": body["alias"]} + + match = re.match(r".*/vehicles\?.*services=uio", endpoint) + if match: + response = self._load_from_file(os.path.join(data_files, "vehicles.json")) + + match = re.match(r"/vehicle/user/.*/vehicle/([^?]+)\?.*services=fud,connected", endpoint) + if match: + vin = match.group(1) + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_connected_services.json")) + + match = re.match(r".*/vehicle/([^/]+)/addtionalInfo", endpoint) + if match: + vin = match.group(1) + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_odometer.json")) + + match = re.match(r".*/vehicles/([^/]+)/vehicleStatus", endpoint) + if match: + vin = match.group(1) + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_status.json")) + + match = re.match(r".*/vehicles/([^/]+)/remoteControl/status", endpoint) + if match: + vin = match.group(1) + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_status_legacy.json")) + + match = re.match(r"/v2/trips/summarize", endpoint) + if match: + # We should retrieve the driving statistics + vin = headers["vin"] + interval = params["calendarInterval"] + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_statistics_{interval}.json")) + + match = re.match(r"/api/user/.*/cms/trips/v2/history/vin/([^?]+)/.*", endpoint) + if match: + # We should retrieve the trips + vin = match.group(1) + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_trips.json")) + + match = re.match(r"/api/user/.*/cms/trips/v2/([^?]+)/events/vin/([^?]+)", endpoint) + if match: + # We should retrieve the trips + trip_id = match.group(1) + vin = match.group(2) + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_trip_{trip_id}.json")) + + match = re.match(r".*/vehicles/([^?]+)/lock", endpoint) + if match: + vin = match.group(1) + try: + response = self._load_from_file(os.path.join(data_files, f"vehicle_{vin}_lock_request.json")) + except FileNotFoundError as exc: + raise ToyotaActionNotSupportedError("Action is not supported") from exc + + match = re.match(r".*/vehicles/([^?]+)/lock/([^?]+)", endpoint) + if match: + vin = match.group(1) + request_id = match.group(2) + response = self._load_from_file( + os.path.join(data_files, f"vehicle_{vin}_lock_request_status_{request_id}.json"), + ) + return response + + +class TestMyTHelper: + """Helper class for the actual TestMyT pytest classes.""" + + def _create_offline_myt(self) -> MyT: + """Create a MyT instance that is using the OfflineController.""" + return MyT( + username="user@domain.com", + password="xxxxx", + locale="en-gb", + region="europe", + controller_class=OfflineController, + ) + + def _lookup_vehicle(self, myt: MyT, vehicle_id: int): + """Retrieve all the vehicles, and find the vehicle with the specified 'id'.""" + vehicles = asyncio.get_event_loop().run_until_complete(myt.get_vehicles()) + vehicle = [veh for veh in vehicles if veh["id"] == vehicle_id] + return vehicle[0] + + +class TestMyT(TestMyTHelper): + """pytest functions to test MyT.""" + + def test_myt(self): + """Test an error free initialisation of MyT.""" + myt = MyT( + username="user@domain.com", + password="xxxxx", + locale="en-gb", + region="europe", + ) + assert myt is not None + assert myt.api is not None + + @pytest.mark.parametrize( + "username", + [None, "", "_invalid_"], + ) + def test_myt_invalid_username(self, username): + """Test an invalid username in MyT.""" + with pytest.raises(ToyotaInvalidUsernameError): + _ = MyT(username=username, password="xxxxx", locale="en-gb", region="europe") + + @pytest.mark.parametrize( + "region", + [ + None, + "", + "antartica", + "mars", + ], + ) + def test_myt_unsupported_region(self, region): + """Test an invalid region in MyT.""" + with pytest.raises(ToyotaRegionNotSupportedError): + _ = MyT( + username="user@domain.com", + password="xxxxx", + locale="en-gb", + region=region, + ) + + def test_get_supported_regions(self): + """Test the supported regions.""" + regions = MyT.get_supported_regions() + assert regions is not None + assert len(regions) > 0 + assert "europe" in regions + + def test_login(self): + """Test the login.""" + myt = self._create_offline_myt() + asyncio.get_event_loop().run_until_complete(myt.login()) + + def test_get_uuid(self): + """Test the retrieval of an uuid.""" + myt = self._create_offline_myt() + uuid = myt.uuid + assert uuid + assert len(uuid) > 0 + + def test_set_alias(self): + """Test the set_alias.""" + myt = self._create_offline_myt() + result = asyncio.get_event_loop().run_until_complete(myt.set_alias(4444444, "pytest_vehicle")) + assert isinstance(result, (dict)) + assert result == {"id": "4444444", "alias": "pytest_vehicle"} + + def test_get_vehicles(self): + """Test the retrieval of the available vehicles.""" + myt = self._create_offline_myt() + vehicles = asyncio.get_event_loop().run_until_complete(myt.get_vehicles()) + assert vehicles + assert len(vehicles) > 0 + for veh in vehicles: + assert isinstance(veh, (dict)) + assert len(veh.keys()) > 0 + + def test_get_vehicles_json(self): + """Test the retrieval of the available vehicles in json format.""" + myt = self._create_offline_myt() + vehicles_json = asyncio.get_event_loop().run_until_complete(myt.get_vehicles_json()) + assert json.loads(vehicles_json) is not None + + def test_get_vehicle_status(self): + """Test the retrieval of the status of a vehicle.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the actual status of the vehicle + status = asyncio.get_event_loop().run_until_complete(myt.get_vehicle_status(vehicle)) + assert status is not None + + def test_get_trips_json(self): + """Test the retrieval of the trips of a vehicle.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the actual trips of the vehicle + trips = asyncio.get_event_loop().run_until_complete(myt.get_trips_json(vehicle["vin"])) + assert trips is not None + + def test_get_trip_json(self): + """Test the retrieval of a trip of a vehicle.""" + trip_id = "971B8221-299E-4899-BC73-AE2EFF604D28" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + trip_json = asyncio.get_event_loop().run_until_complete(myt.get_trip_json(vehicle["vin"], trip_id)) + assert trip_json is not None + + def test_get_trips(self): + """Test the retrieval of the trips of a vehicle.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + trips = asyncio.get_event_loop().run_until_complete(myt.get_trips(vehicle["vin"])) + assert trips is not None + for trip in trips: + assert isinstance(trip, Trip) + assert trip.raw_json.get("tripId") is not None + assert isinstance(trip.trip_id, str) + assert trip.raw_json.get("startAddress") is not None + assert isinstance(trip.start_address, str) + assert trip.raw_json.get("endAddress") is not None + assert isinstance(trip.end_address, str) + assert trip.raw_json.get("startTimeGmt") is not None + assert isinstance(trip.start_time_gmt, datetime.datetime) + assert trip.raw_json.get("endTimeGmt") is not None + assert isinstance(trip.end_time_gmt, datetime.datetime) + assert trip.raw_json.get("classificationType") is not None + assert isinstance(trip.classification_type, int) + + def test_get_trip(self): + """Test the retrieval of a trip of a vehicle.""" + trip_id = "971B8221-299E-4899-BC73-AE2EFF604D28" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the actual trip + trip = asyncio.get_event_loop().run_until_complete(myt.get_trip(vehicle["vin"], trip_id)) + assert trip is not None + assert isinstance(trip, DetailedTrip) + assert len(trip.trip_events) == 12 + for event in trip.trip_events: + assert isinstance(event, TripEvent) + assert event.raw_json.get("lat") is not None + assert isinstance(event.latitude, float) + assert event.raw_json.get("lon") is not None + assert isinstance(event.longitude, float) + assert event.raw_json.get("overspeed") is not None + assert isinstance(event.overspeed, bool) + assert event.raw_json.get("isEv") is not None + assert isinstance(event.is_ev, bool) + assert event.raw_json.get("highway") is not None + assert isinstance(event.highway, bool) + assert event.raw_json.get("mode") is not None + assert isinstance(event.mode, int) + assert trip.raw_json.get("statistics") is not None + assert isinstance(trip.statistics, dict) + + +class TestMyTStatistics(TestMyTHelper): + """pytest functions to test get_vehicle_statistics of MyT.""" + + def test_get_vehicle_statistics_invalid_interval_error(self): + """Test that retrieving the statistics of an unknown interval is not possible.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the actual status of the vehicle + stat = asyncio.get_event_loop().run_until_complete(myt.get_driving_statistics(vehicle["vin"], "century")) + assert stat is not None + assert "error_mesg" in stat[0] + + def test_get_vehicle_statistics_tomorrow_error(self): + """Test that retrieving the statistics of tomorrow is not possible.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + tomorrow = arrow.now().shift(days=1).format("YYYY-MM-DD") + # Retrieve the actual status of the vehicle + stat = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics(vehicle["vin"], "year", from_date=tomorrow), + ) + assert stat is not None + assert "error_mesg" in stat[0] + + def test_get_vehicle_statistics_isoweek_error(self): + """Test that retrieving statistics of long ago of an isoweek is not possible.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the actual status of the vehicle + stat = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics(vehicle["vin"], "isoweek", from_date="2010-01-01"), + ) + assert stat is not None + assert "error_mesg" in stat[0] + + def test_get_vehicle_statistics_year_error(self): + """Test that retrieving the previous year is not possible.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + previous_year = arrow.now().shift(years=-1).format("YYYY-MM-DD") + # Retrieve the actual status of the vehicle + stat = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics(vehicle["vin"], "year", from_date=previous_year), + ) + assert stat is not None + assert "error_mesg" in stat[0] + + @pytest.mark.parametrize( + "interval,unit", + [ + ("day", "metric"), + ("day", "imperial"), + ("day", "imperial_liters"), + ("week", "metric"), + ("week", "imperial"), + ("week", "imperial_liters"), + ("isoweek", "metric"), + ("isoweek", "imperial"), + ("isoweek", "imperial_liters"), + ("month", "metric"), + ("month", "imperial"), + ("month", "imperial_liters"), + # Retrieving the year statistics of today is possible + # as it will get the current year statistics + ], + ) + def test_get_vehicle_statistics_today_error(self, interval, unit): + """Test that retrieving the statistics of today is not possible.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + today = arrow.now().format("YYYY-MM-DD") + # Retrieve the actual status of the vehicle + stat = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics(vehicle["vin"], interval, unit=unit, from_date=today), + ) + assert stat is not None + assert "error_mesg" in stat[0] + + @pytest.mark.parametrize( + "interval,unit", + [ + ("day", "metric"), + ("day", "imperial"), + ("day", "imperial_liters"), + # ("week", "metric"), + # ("week", "imperial"), + # ("week", "imperial_liters"), + # ("isoweek", "metric"), + # ("isoweek", "imperial"), + # ("isoweek", "imperial_liters"), + ("month", "metric"), + ("month", "imperial"), + ("month", "imperial_liters"), + ("year", "metric"), + ("year", "imperial"), + ("year", "imperial_liters"), + ], + ) + def test_get_driving_statistics(self, interval, unit): + """Test the retrieval of the status of a vehicle.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the actual status of the vehicle + statistics = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics(vehicle["vin"], interval, unit=unit), + ) + assert statistics is not None + for data in statistics: + assert data["bucket"] is not None + # And the unit should be requested unit + assert data["bucket"]["unit"] == unit + # And the year should be recent or (short) future + assert 2018 <= int(data["bucket"]["year"]) <= 2100 + assert data["data"] is not None + + @pytest.mark.parametrize( + "unit", + [ + ("metric"), + ("imperial"), + ("imperial_liters"), + ], + ) + def test_get_driving_statistics_has_correct_day_of_year(self, unit): + """Test that the day-statistics contains the correct date for the day of the year.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the driving statistics of the vehicle + statistics = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics(vehicle["vin"], "day", unit=unit), + ) + assert statistics is not None + for day_data in statistics: + bucket = day_data["bucket"] + date = datetime.date.fromisoformat(bucket["date"]) + day_of_year = int(date.strftime("%j")) + assert bucket["dayOfYear"] == day_of_year + assert int(bucket["year"]) == date.year + + @pytest.mark.parametrize( + "interval,unit", + [ + ("day", "metric"), + ("day", "imperial"), + ("day", "imperial_liters"), + # ("week", "metric"), + # ("week", "imperial"), + # ("week", "imperial_liters"), + # ("isoweek", "metric"), + # ("isoweek", "imperial"), + # ("isoweek", "imperial_liters"), + ("month", "metric"), + ("month", "imperial"), + ("month", "imperial_liters"), + ("year", "metric"), + ("year", "imperial"), + ("year", "imperial_liters"), + ], + ) + def test_get_driving_statistics_contains_year_as_int(self, interval, unit): + """Test that the statistics contains the year as an integer.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the driving statistics of the vehicle + statistics = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics(vehicle["vin"], interval, unit=unit), + ) + assert statistics is not None + for day_data in statistics: + bucket = day_data["bucket"] + assert isinstance(bucket["year"], int) + + @pytest.mark.parametrize( + "interval", + [ + ("day"), + # ("week"), + # ("isoweek"), + ("month"), + ("year"), + ], + ) + def test_get_driving_statistics_json(self, interval): + """Test the retrieval of the statistics (in JSON format) of a vehicle.""" + myt = self._create_offline_myt() + vehicle = self._lookup_vehicle(myt, 4444444) + assert vehicle is not None + # Retrieve the actual status of the vehicle + statistics_json = asyncio.get_event_loop().run_until_complete( + myt.get_driving_statistics_json(vehicle["vin"], interval), + ) + assert json.loads(statistics_json) is not None diff --git a/tests/legacy_test_myt_online.py b/tests/legacy_test_myt_online.py new file mode 100644 index 00000000..8dd66e86 --- /dev/null +++ b/tests/legacy_test_myt_online.py @@ -0,0 +1,38 @@ +"""pytest tests for mytoyota.client.MyT using the online services.""" + +import asyncio + +import pytest # pylint: disable=import-error + +from mytoyota.client import MyT +from mytoyota.exceptions import ToyotaLoginError + + +class TestMyTOnline: + """pytest functions to test MyT using the online services.""" + + def _create_online_myt(self, uuid=None) -> MyT: + """Create a MyT instance that is using the OfflineController.""" + return MyT( + username="username@domain.com", + password="xxxxx", + locale="en-gb", + region="europe", + uuid=uuid, + ) + + def test_login(self): + """Test the login.""" + myt = self._create_online_myt() + with pytest.raises(ToyotaLoginError): + asyncio.run(myt.login()) + + @pytest.mark.parametrize( + "uuid", + ["uuid1", "uuid2", "something", None], + ) + def test_get_uuid(self, uuid): + """Test the retrieval of an uuid.""" + myt = self._create_online_myt(uuid) + actual_uuid = myt.uuid + assert actual_uuid == uuid diff --git a/tests/legacy_test_parking_location.py b/tests/legacy_test_parking_location.py new file mode 100644 index 00000000..530aa282 --- /dev/null +++ b/tests/legacy_test_parking_location.py @@ -0,0 +1,27 @@ +"""pytest tests for mytoyota.models.location.ParkingLocation.""" + +from mytoyota.models.parking_location import ( # pylint: disable=import-error + ParkingLocation, +) + + +class TestParkingLocation: + """pytest functions to test ParkingLocation.""" + + def _create_example_parking_location(self): + """Create an ParkingLocation with some predefined example data.""" + return ParkingLocation({"timestamp": 987654, "lat": 1.234, "lon": 5.678}) + + def test_parking_location(self): + """Test ParkingLocation.""" + location = self._create_example_parking_location() + assert location.latitude == 1.234 + assert location.longitude == 5.678 + assert location.timestamp == 987654 + + def test_parking_location_no_data(self): + """Test ParkingLocation with no initialization data.""" + location = ParkingLocation({}) + assert location.latitude == 0.0 + assert location.longitude == 0.0 + assert location.timestamp is None diff --git a/tests/legacy_test_statistics.py b/tests/legacy_test_statistics.py new file mode 100644 index 00000000..6fe42452 --- /dev/null +++ b/tests/legacy_test_statistics.py @@ -0,0 +1,20 @@ +"""pytest tests for mytoyota.statistics.Statistics.""" + +# A lot of the code in the mytoyota.statistics.Statistics class is already +# tested by the unittests of the MyT.get_driving_statistics function. +# These pytest tests are only to test the strange cases + +import pytest # pylint: disable=import-error + +from mytoyota.statistics import Statistics + + +class TestStatistics: + """pytest functions to test Statistics.""" + + def test_none_raw_statistics(self): + """Test the initialization when None raw_statistics is provided.""" + stat = Statistics(None, "day") + assert stat is not None + with pytest.raises(AttributeError): + assert stat.as_list() is None diff --git a/tests/legacy_test_utils.py b/tests/legacy_test_utils.py new file mode 100644 index 00000000..dd77998b --- /dev/null +++ b/tests/legacy_test_utils.py @@ -0,0 +1,74 @@ +"""pytest tests for mytoyota.utils.""" +# pylint: disable=import-error +import pytest + +from mytoyota.utils.conversions import ( + convert_to_liter_per_100_miles, + convert_to_miles, + convert_to_mpg, +) +from mytoyota.utils.formatters import format_odometer + + +class TestUtils: + """pytest functions to test functions in mytoyota.utils.""" + + @pytest.mark.parametrize( + "distance_km,distance_miles", + [ + (1, 0.6214), + (0, 0), + (1000, 621.3712), + ], + ) + def test_convert_to_miles(self, distance_km, distance_miles): + """Test conversion from km to miles.""" + assert convert_to_miles(distance_km) == distance_miles + + @pytest.mark.parametrize( + "liters_km,per_mile", + [ + (1, 1.6093), + (0, 0), + (1000, 1609.344), + ], + ) + def test_convert_to_liter_per_100_miles(self, liters_km, per_mile): + """Test conversion from l/100km to l/100miles.""" + assert convert_to_liter_per_100_miles(liters_km) == per_mile + + @pytest.mark.parametrize( + "liters_km,mpg", + [ + (0, 0), + (0.01, 28250.0), + (1, 282.5), + (12, 23.5417), + ], + ) + def test_convert_to_mpg(self, liters_km, mpg): + """Test conversion from liters/100km to miles per gallons.""" + assert convert_to_mpg(liters_km) == mpg + + def test_format_odometer(self): + """Test format odometer.""" + raw = [ + {"type": "mileage", "value": 3205, "unit": "km"}, + {"type": "Fuel", "value": 22}, + ] + + formatted = format_odometer(raw) + + assert isinstance(formatted, dict) + assert formatted == { + "mileage": 3205, + "mileage_unit": "km", + "Fuel": 22, + } + + def test_format_odometer_no_data(self): + """Test format odometer with no initialization data.""" + nothing = format_odometer([]) + + assert isinstance(nothing, dict) + assert not nothing diff --git a/tests/legacy_test_utils_logs.py b/tests/legacy_test_utils_logs.py new file mode 100644 index 00000000..92550974 --- /dev/null +++ b/tests/legacy_test_utils_logs.py @@ -0,0 +1,63 @@ +"""pytest tests for mytoyota.utils.logs.""" + +from mytoyota.utils.logs import censor_all, censor_string + + +class TestLogUtilities: + """pytest functions for testing logs.""" + + def test_censor(self): + """Testing sensitive info censoring.""" + string = censor_string("5957a713-f80f-483f-998c-97f956367048") + + assert string == "5***********************************" + + def test_censor_no_data(self): + """Testing sensitive info censoring.""" + string = censor_string("") + + assert string == "" + + def test_censor_string(self): + """Test censor_string.""" + vin = censor_string("JTDKGNEC00N999999") + + assert vin == "JTDKGNEC0********" + + def test_censor_string_no_data(self): + """Test censor_string.""" + vin = censor_string("") + + assert vin == "" + + def test_censor_all(self): + """Test censor_all.""" + dictionary = censor_all( + { + "vin": "JTDKGNEC00N999999", + "VIN": "JTDKGNEC00N999999", + "X-TME-TOKEN": "5957a713-f80f-483f-998c-97f956367048", + "uuid": "ba1ba6cb-b3c9-47d1-a657-c28a05cdd66e", + "id": 2199911, + "Cookie": "iPlanetDirectoryPro=5957a713-f80f-483f-998c-97f956367048", + "Today": "Tomorrow Toyota", + }, + ) + + assert isinstance(dictionary, dict) + assert dictionary == { + "vin": "JTDKGNEC0********", + "VIN": "JTDKGNEC0********", + "X-TME-TOKEN": "5***********************************", + "uuid": "b***********************************", + "id": "2******", + "Cookie": "iPlanetDirectoryPro=5***********************************", + "Today": "Tomorrow Toyota", + } + + def test_censor_all_no_data(self): + """Test censor_all with no data.""" + dictionary = censor_all({}) + + assert isinstance(dictionary, dict) + assert not dictionary diff --git a/tests/legacy_test_vehicle.py b/tests/legacy_test_vehicle.py new file mode 100644 index 00000000..740a918f --- /dev/null +++ b/tests/legacy_test_vehicle.py @@ -0,0 +1,210 @@ +"""pytest tests for mytoyota.models.vehicle.Vehicle.""" +import json +import os +import os.path + +import pytest + +from mytoyota.models.dashboard import Dashboard +from mytoyota.models.hvac import Hvac +from mytoyota.models.parking_location import ParkingLocation +from mytoyota.models.sensors import Sensors +from mytoyota.models.vehicle import Vehicle + + +class TestVehicle: + """pytest functions for Vehicle object.""" + + @staticmethod + def _load_from_file(filename: str): + """Load and return data structure from specified JSON filename.""" + with open(filename, encoding="UTF-8") as json_file: + return json.load(json_file) + + def test_vehicle_no_data(self): + """Test vehicle with no initialization data.""" + vehicle = Vehicle({}, {}) + + assert vehicle.vehicle_id is None + assert vehicle.vin is None + assert vehicle.alias == "My vehicle" + assert vehicle.hybrid is False + assert vehicle.fueltype == "Unknown" + assert vehicle.details is None + assert vehicle.is_connected_services_enabled is False + assert vehicle.dashboard is None + assert vehicle.sensors is None + assert vehicle.hvac is None + assert vehicle.parkinglocation is None + + def test_vehicle_init_no_status(self): + """Test vehicle initialization with no status.""" + data_files = os.path.join(os.path.curdir, "tests", "data") + + vehicle_fixtures = self._load_from_file(os.path.join(data_files, "vehicles.json")) + + for veh in vehicle_fixtures: + vehicle = Vehicle(vehicle_info=veh, connected_services={}) + + assert vehicle.is_connected_services_enabled is False + assert vehicle.dashboard is None + assert vehicle.sensors is None + assert vehicle.hvac is None + assert vehicle.parkinglocation is None + assert vehicle.sensors is None + + @pytest.mark.parametrize( + "vin,connected_services", + [ + (None, {}), + (None, {"connectedService": {"status": "ACTIVE"}}), + ("VINVIN123VIN", None), + ("VINVIN123VIN", {}), + # ("VINVIN123VIN", {"connectedService": None}), + ("VINVIN123VIN", {"connectedService": {}}), + ("VINVIN123VIN", {"connectedService": {"status": None}}), + ("VINVIN123VIN", {"connectedService": {"status": ""}}), + ("VINVIN123VIN", {"connectedService": {"status": "DISABLED"}}), + ("VINVIN123VIN", {"connectedService": {"error_status": "ACTIVE"}}), + ("VINVIN123VIN", {"error_connectedService": {"status": "ACTIVE"}}), + ], + ) + def test_vehicle_disabled_connected_services(self, vin, connected_services): + """Test the check if the connected services is disabled.""" + car = {"vin": vin} + vehicle = Vehicle(vehicle_info=car, connected_services=connected_services) + assert vehicle.is_connected_services_enabled is False + + def test_vehicle_init(self): + """Test vehicle initialization with connected services.""" + data_files = os.path.join(os.path.curdir, "tests", "data") + + vehicle_fixtures = self._load_from_file(os.path.join(data_files, "vehicles.json")) + + for veh in vehicle_fixtures: + vehicle = Vehicle( + vehicle_info=veh, + connected_services={ + "connectedService": { + "devices": [ + { + "brand": "TOYOTA", + "state": "ACTIVE", + "vin": veh.get("vin"), + }, + ], + }, + }, + ) + + assert vehicle.vin == veh.get("vin") + assert vehicle.alias == veh.get("alias", "My vehicle") + assert vehicle.vehicle_id == veh.get("id") + assert vehicle.hybrid == veh.get("hybrid", False) + assert isinstance(vehicle.fueltype, str) + assert isinstance(vehicle.details, dict) + + print(vehicle.vehicle_id) + + if vehicle.vin is None: + assert vehicle.is_connected_services_enabled is False + assert vehicle.dashboard is None + assert vehicle.sensors is None + assert vehicle.hvac is None + assert vehicle.parkinglocation is None + else: + assert vehicle.is_connected_services_enabled is True + assert vehicle.dashboard is None + assert vehicle.sensors is None + assert vehicle.hvac is None + assert vehicle.parkinglocation is None + + def test_vehicle_init_status(self): + """Test vehicle initialization with connected services with status.""" + data_files = os.path.join(os.path.curdir, "tests", "data") + + vehicle_fixtures = self._load_from_file(os.path.join(data_files, "vehicles.json")) + odometer_fixture = self._load_from_file(os.path.join(data_files, "vehicle_JTMW1234565432109_odometer.json")) + status_fixture = self._load_from_file(os.path.join(data_files, "vehicle_JTMW1234565432109_status.json")) + + vehicle = Vehicle( + vehicle_info=vehicle_fixtures[0], + connected_services={ + "connectedService": { + "devices": [ + { + "brand": "TOYOTA", + "state": "ACTIVE", + "vin": vehicle_fixtures[0].get("vin"), + }, + ], + }, + }, + odometer=odometer_fixture, + status=status_fixture, + ) + + assert vehicle.fueltype == status_fixture["energy"][0]["type"].capitalize() + assert isinstance(vehicle.parkinglocation, ParkingLocation) + assert isinstance(vehicle.sensors, Sensors) + assert vehicle.hvac is None + assert isinstance(vehicle.dashboard, Dashboard) + assert vehicle.dashboard.legacy is False + assert vehicle.dashboard.fuel_level == status_fixture["energy"][0]["level"] + assert vehicle.dashboard.is_metric is True + assert vehicle.dashboard.odometer == odometer_fixture[0]["value"] + assert vehicle.dashboard.fuel_range == status_fixture["energy"][0]["remainingRange"] + assert vehicle.dashboard.battery_level is None + assert vehicle.dashboard.battery_range is None + assert vehicle.dashboard.battery_range_with_aircon is None + assert vehicle.dashboard.charging_status is None + assert vehicle.dashboard.remaining_charge_time is None + + def test_vehicle_init_status_legacy(self): + """Test vehicle initialization with connected services with legacy status.""" + data_files = os.path.join(os.path.curdir, "tests", "data") + + vehicle_fixtures = self._load_from_file(os.path.join(data_files, "vehicles.json")) + odometer_fixture = self._load_from_file( + os.path.join(data_files, "vehicle_JTMW1234565432109_odometer_legacy.json"), + ) + status_fixture = self._load_from_file(os.path.join(data_files, "vehicle_JTMW1234565432109_status_legacy.json")) + + vehicle = Vehicle( + vehicle_info=vehicle_fixtures[0], + connected_services={ + "connectedService": { + "devices": [ + { + "brand": "TOYOTA", + "state": "ACTIVE", + "vin": vehicle_fixtures[0].get("vin"), + }, + ], + }, + }, + odometer=odometer_fixture, + status_legacy=status_fixture, + ) + + assert vehicle.fueltype == "Petrol" + assert vehicle.parkinglocation is None + assert vehicle.sensors is None + assert isinstance(vehicle.hvac, Hvac) + assert isinstance(vehicle.dashboard, Dashboard) + assert vehicle.dashboard.legacy is True + assert vehicle.dashboard.fuel_level == odometer_fixture[1]["value"] + assert vehicle.dashboard.is_metric is True + assert vehicle.dashboard.odometer == odometer_fixture[0]["value"] + assert vehicle.dashboard.fuel_range == status_fixture["VehicleInfo"]["ChargeInfo"]["GasolineTravelableDistance"] + assert vehicle.dashboard.battery_level == status_fixture["VehicleInfo"]["ChargeInfo"]["ChargeRemainingAmount"] + assert vehicle.dashboard.battery_range == status_fixture["VehicleInfo"]["ChargeInfo"]["EvDistanceInKm"] + assert ( + vehicle.dashboard.battery_range_with_aircon + == status_fixture["VehicleInfo"]["ChargeInfo"]["EvDistanceWithAirCoInKm"] + ) + assert vehicle.dashboard.charging_status == status_fixture["VehicleInfo"]["ChargeInfo"]["ChargingStatus"] + assert ( + vehicle.dashboard.remaining_charge_time + == status_fixture["VehicleInfo"]["ChargeInfo"]["RemainingChargeTime"] + ) diff --git a/tests/legcy_test_sensors.py b/tests/legcy_test_sensors.py new file mode 100644 index 00000000..91155ecd --- /dev/null +++ b/tests/legcy_test_sensors.py @@ -0,0 +1,225 @@ +"""pytest tests for mytoyota.models.sensors.""" +import json +import os +import os.path + +from mytoyota.models.sensors import ( + Door, + Doors, + Key, + Light, + Lights, + Sensors, + Window, + Windows, +) + + +class TestSensors: # pylint: disable=too-many-public-methods + """pytest functions to test Sensors.""" + + @staticmethod + def _load_from_file(filename: str): + """Load and return data structure from specified JSON filename.""" + with open(filename, encoding="UTF-8") as json_file: + return json.load(json_file) + + def test_hood(self): + """Test hood.""" + hood = Door({"warning": False, "closed": True}) + + assert hood.warning is False + assert hood.closed is True + assert hood.locked is None + + def test_hood_no_data(self): + """Test hood with no initialization data.""" + hood = Door({}) + + assert hood.warning is None + assert hood.closed is None + assert hood.locked is None + + @staticmethod + def _create_example_door(): + """Create a door with predefined data.""" + return Door({"warning": False, "closed": True, "locked": False}) + + def test_door(self): + """Test door.""" + door = self._create_example_door() + + assert door.warning is False + assert door.closed is True + assert door.locked is False + + def test_door_no_data(self): + """Test door with no initialization data.""" + door = Door({}) + + assert door.warning is None + assert door.closed is None + assert door.locked is None + + def test_doors(self): + """Test Doors.""" + doors = { + "warning": False, + "driverSeatDoor": {"warning": False, "closed": True, "locked": False}, + "passengerSeatDoor": {"warning": False, "closed": True, "locked": False}, + "rearRightSeatDoor": {"warning": False, "closed": True, "locked": False}, + "rearLeftSeatDoor": {"warning": False, "closed": True, "locked": False}, + "backDoor": {"warning": False, "closed": True, "locked": False}, + } + + doors = Doors(doors) + + assert doors.warning is False + assert isinstance(doors.driver_seat, Door) + assert isinstance(doors.passenger_seat, Door) + assert isinstance(doors.leftrear_seat, Door) + assert isinstance(doors.rightrear_seat, Door) + assert isinstance(doors.trunk, Door) + + def test_doors_no_data(self): + """Test Windows with no initialization data.""" + doors = Doors({}) + + assert doors.warning is None + assert isinstance(doors.driver_seat, Door) + assert isinstance(doors.passenger_seat, Door) + assert isinstance(doors.leftrear_seat, Door) + assert isinstance(doors.rightrear_seat, Door) + assert isinstance(doors.trunk, Door) + + @staticmethod + def _create_example_window(): + """Create a window with predefined data.""" + return Window({"warning": False, "state": "close"}) + + def test_window(self): + """Test window.""" + window = self._create_example_window() + + assert window.warning is False + assert window.state == "close" + + def test_window_no_data(self): + """Test window with no initialization data.""" + window = Window({}) + + assert window.warning is None + assert window.state is None + + def test_windows(self): + """Test Windows.""" + windows = { + "warning": False, + "driverSeatWindow": {"warning": False, "state": "close"}, + "passengerSeatWindow": {"warning": False, "state": "close"}, + "rearRightSeatWindow": {"warning": False, "state": "close"}, + "rearLeftSeatWindow": {"warning": False, "state": "close"}, + } + + windows = Windows(windows) + + assert windows.warning is False + assert isinstance(windows.driver_seat, Window) + assert isinstance(windows.passenger_seat, Window) + assert isinstance(windows.rightrear_seat, Window) + assert isinstance(windows.leftrear_seat, Window) + + def test_windows_no_data(self): + """Test Windows with no initialization data.""" + windows = Windows({}) + + assert windows.warning is None + assert isinstance(windows.driver_seat, Window) + assert isinstance(windows.passenger_seat, Window) + assert isinstance(windows.rightrear_seat, Window) + assert isinstance(windows.leftrear_seat, Window) + + @staticmethod + def _create_example_light(): + """Create a example light with predefined data.""" + return Light({"warning": False, "off": True}) + + def test_light(self): + """Test light.""" + light = self._create_example_light() + + assert light.warning is False + assert light.off is True + + def test_light_no_data(self): + """Test light with no initialization data.""" + light = Light({}) + + assert light.warning is None + assert light.off is None + + def test_lights(self): + """Test ligts.""" + lights = { + "warning": False, + "headLamp": {"warning": False, "off": True}, + "tailLamp": {"warning": False, "off": True}, + "hazardLamp": {"warning": False, "off": True}, + } + + lights = Lights(lights) + + assert lights.warning is False + assert isinstance(lights.headlights, Light) + assert isinstance(lights.taillights, Light) + assert isinstance(lights.hazardlights, Light) + + def test_lights_no_data(self): + """Test Lights with no initialization data.""" + lights = Lights({}) + + assert lights.warning is None + assert isinstance(lights.headlights, Light) + assert isinstance(lights.taillights, Light) + assert isinstance(lights.hazardlights, Light) + + def test_key(self): + """Test key.""" + key = Key({"warning": False, "inCar": True}) + + assert key.warning is False + assert key.in_car is True + + def test_key_no_data(self): + """Test key with no initialization data.""" + key = Key({}) + + assert key.warning is None + assert key.in_car is None + + def test_sensors(self): + """Test sensors.""" + data_files = os.path.join(os.path.curdir, "tests", "data") + fixture = self._load_from_file(os.path.join(data_files, "vehicle_JTMW1234565432109_status.json")) + sensors = Sensors(fixture.get("protectionState")) + + assert sensors.overallstatus == "OK" + assert sensors.last_updated == "2021-10-12T15:22:53Z" + assert isinstance(sensors.doors, Doors) + assert sensors.doors.driver_seat.warning is False + assert sensors.doors.driver_seat.closed is True + assert sensors.doors.driver_seat.locked is True + assert isinstance(sensors.hood, Door) + assert sensors.hood.warning is False + assert sensors.hood.closed is True + assert sensors.hood.locked is None + assert isinstance(sensors.lights, Lights) + assert sensors.lights.headlights.warning is False + assert sensors.lights.headlights.off is True + assert isinstance(sensors.windows, Windows) + assert sensors.windows.passenger_seat.warning is False + assert sensors.windows.passenger_seat.state == "close" + assert isinstance(sensors.key, Key) + + assert isinstance(sensors.raw_json, dict) + assert sensors.raw_json == fixture.get("protectionState") diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..057b8e18 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,30 @@ +"""Test mytoyota client.""" +import pytest + +from mytoyota.client import MyT +from mytoyota.exceptions import ToyotaInvalidUsernameError + +# Constants for tests +VALID_USERNAME = "user@example.com" +VALID_PASSWORD = "securepassword123" +INVALID_USERNAME = "userexample.com" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "username, password, expected_exception, test_id", + [ + (VALID_USERNAME, VALID_PASSWORD, None, "happy_path_valid_credentials"), + (INVALID_USERNAME, VALID_PASSWORD, ToyotaInvalidUsernameError, "error_invalid_username"), + ], +) +async def test_myt_init(username, password, expected_exception, test_id): # noqa: D103, ARG001 + # Arrange + if expected_exception: + with pytest.raises(expected_exception): + MyT(username, password) + else: + # Act + client = MyT(username, password) + # Assert + assert client._api is not None diff --git a/tests/test_utils/test_conversions.py b/tests/test_utils/test_conversions.py new file mode 100644 index 00000000..4918ce00 --- /dev/null +++ b/tests/test_utils/test_conversions.py @@ -0,0 +1,101 @@ +"""Test Conversion Utils.""" +import pytest + +from mytoyota.utils import conversions + + +# Test for convert_to_miles +@pytest.mark.parametrize( + "kilometers, expected_miles", + [ + pytest.param(1, 0.621, id="1km_to_miles"), + pytest.param(0, 0.0, id="0km_to_miles"), + pytest.param(10, 6.214, id="10km_to_miles"), + pytest.param(-1, -0.621, id="negative_km_to_miles"), + ], +) +def test_convert_to_miles(kilometers, expected_miles): # noqa: D103 + # Act + miles = conversions.convert_to_miles(kilometers) + + # Assert + assert round(miles, 3) == expected_miles + + +# Test for convert_to_km +@pytest.mark.parametrize( + "miles, expected_km", + [ + pytest.param(1, 1.609, id="1mile_to_km"), + pytest.param(0, 0.0, id="0mile_to_km"), + pytest.param(10, 16.093, id="10miles_to_km"), + pytest.param(-1, -1.609, id="negative_mile_to_km"), + ], +) +def test_convert_to_km(miles, expected_km): # noqa: D103 + # Act + km = conversions.convert_to_km(miles) + + # Assert + assert round(km, 3) == expected_km + + +# Test for convert_distance +@pytest.mark.parametrize( + "convert_to, convert_from, value, decimal_places, expected", + [ + pytest.param("km", "miles", 1, 3, 1.609, id="1mile_to_km"), + pytest.param("miles", "km", 1, 3, 0.621, id="1km_to_miles"), + pytest.param("km", "km", 1, 3, 1.000, id="1km_to_km"), + pytest.param("miles", "miles", 1, 3, 1.000, id="1mile_to_mile"), + pytest.param("km", "miles", 0, 3, 0.0, id="0mile_to_km"), + pytest.param("miles", "km", 0, 3, 0.0, id="0km_to_miles"), + pytest.param("km", "miles", 1, 0, 2, id="1mile_to_km_no_decimals"), + pytest.param("miles", "km", 1, 0, 1, id="1km_to_miles_no_decimals"), + pytest.param("km", "miles", -1, 3, -1.609, id="negative_mile_to_km"), + pytest.param("miles", "km", -1, 3, -0.621, id="negative_km_to_miles"), + ], +) +def test_convert_distance(convert_to, convert_from, value, decimal_places, expected): # noqa: D103 + # Act + result = conversions.convert_distance(convert_to, convert_from, value, decimal_places) + + # Assert + assert result == expected + + +# Test for convert_to_liter_per_100_miles +@pytest.mark.parametrize( + "liters, expected", + [ + pytest.param(1, 1.6093, id="1liter_to_100miles"), + pytest.param(0, 0.0, id="0liter_to_100miles"), + pytest.param(10, 16.0934, id="10liters_to_100miles"), + pytest.param(-1, -1.6093, id="negative_liter_to_100miles"), + ], +) +def test_convert_to_liter_per_100_miles(liters, expected): # noqa: D103 + # Act + result = conversions.convert_to_liter_per_100_miles(liters) + + # Assert + assert result == expected + + +# Test for convert_to_mpg +@pytest.mark.parametrize( + "liters_per_100_km, expected_mpg", + [ + pytest.param(1, 282.5000, id="1liter_to_mpg"), + pytest.param(0, 0.0, id="0liter_to_mpg"), + pytest.param(10, 28.2500, id="10liters_to_mpg"), + pytest.param(5.5, 51.3636, id="5.5liters_to_mpg"), + pytest.param(-1, 0.0, id="negative_liter_to_mpg"), + ], +) +def test_convert_to_mpg(liters_per_100_km, expected_mpg): # noqa: D103 + # Act + mpg = conversions.convert_to_mpg(liters_per_100_km) + + # Assert + assert mpg == expected_mpg diff --git a/tests/test_utils/test_formatter.py b/tests/test_utils/test_formatter.py new file mode 100644 index 00000000..9ff4428a --- /dev/null +++ b/tests/test_utils/test_formatter.py @@ -0,0 +1,72 @@ +"""Test Formatter Utils.""" +import pytest + +from mytoyota.utils.formatters import format_odometer + + +# Test cases for the happy path with various realistic test values +@pytest.mark.parametrize( + "test_input, expected", + [ + (pytest.param([{"type": "mileage", "value": 12345}], {"mileage": 12345}, id="single_instrument_no_unit")), + ( + pytest.param( + [{"type": "mileage", "value": 12345, "unit": "km"}], + {"mileage": 12345, "mileage_unit": "km"}, + id="single_instrument_with_unit", + ) + ), + ( + pytest.param( + [{"type": "mileage", "value": 12345, "unit": "km"}, {"type": "fuel", "value": 50, "unit": "liters"}], + {"mileage": 12345, "mileage_unit": "km", "fuel": 50, "fuel_unit": "liters"}, + id="multiple_instruments_with_units", + ) + ), + ], +) +def test_format_odometer_happy_path(test_input, expected): # noqa: D103 + # Act + result = format_odometer(test_input) + + # Assert + assert result == expected + + +# Test cases for edge cases +@pytest.mark.parametrize( + "test_input, expected", + [ + (pytest.param([], {}, id="empty_list")), + (pytest.param([{"type": "mileage", "value": 0}], {"mileage": 0}, id="zero_value")), + ( + pytest.param( + [{"type": "mileage", "value": 12345}, {"type": "mileage", "value": 67890}], + {"mileage": 67890}, + id="duplicate_type_last_wins", + ) + ), + ], +) +def test_format_odometer_edge_cases(test_input, expected): # noqa: D103 + # Act + result = format_odometer(test_input) + + # Assert + assert result == expected + + +# Test cases for error cases +@pytest.mark.parametrize( + "test_input, expected_exception", + [ + (pytest.param([{"value": 12345}], KeyError, id="missing_type_key")), + (pytest.param([{"type": "mileage"}], KeyError, id="missing_value_key")), + (pytest.param("not_a_list", TypeError, id="non_list_input")), + (pytest.param([12345], TypeError, id="non_dict_in_list")), + ], +) +def test_format_odometer_error_cases(test_input, expected_exception): # noqa: D103 + # Act / Assert + with pytest.raises(expected_exception): + format_odometer(test_input) diff --git a/tests/test_utils/test_locale.py b/tests/test_utils/test_locale.py new file mode 100644 index 00000000..a2ade638 --- /dev/null +++ b/tests/test_utils/test_locale.py @@ -0,0 +1,57 @@ +"""Test Locale Utils.""" +import pytest + +from mytoyota.utils.locale import is_valid_locale + + +# Parametrized test for happy path with various realistic test values +@pytest.mark.parametrize( + "locale, expected", + [ + pytest.param("en-GB", True, id="id_valid_english_gb"), + pytest.param("en-US", True, id="id_valid_english_us"), + pytest.param("fr-FR", True, id="id_valid_french"), + pytest.param("de-DE", True, id="id_valid_german"), + ], +) +def test_is_valid_locale_happy_path(locale, expected): # noqa: D103 + # Act + result = is_valid_locale(locale) + + # Assert + assert result == expected + + +# Parametrized test for edge cases +@pytest.mark.parametrize( + "locale, expected", + [ + pytest.param("", False, id="id_empty_string"), + pytest.param("en-GB-oed", True, id="id_valid_english_oxford"), + pytest.param("i-klingon", True, id="id_valid_klingon"), # Grandfathered tag + pytest.param("x-private", True, id="id_valid_private_use"), # Private use tag + ], +) +def test_is_valid_locale_edge_cases(locale, expected): # noqa: D103 + # Act + result = is_valid_locale(locale) + + # Assert + assert result == expected + + +# Parametrized test for error cases +@pytest.mark.parametrize( + "locale, expected", + [ + pytest.param("en-US-12345", False, id="id_invalid_subtag_length"), + pytest.param("123-en", False, id="id_invalid_language_digits"), + pytest.param("en@currency=USD", False, id="id_invalid_locale_with_currency"), + ], +) +def test_is_valid_locale_error_cases(locale, expected): # noqa: D103 + # Act + result = is_valid_locale(locale) + + # Assert + assert result == expected diff --git a/tests/test_utils/test_logs.py b/tests/test_utils/test_logs.py new file mode 100644 index 00000000..d237d1a2 --- /dev/null +++ b/tests/test_utils/test_logs.py @@ -0,0 +1,89 @@ +"""Test Logs Utils.""" +import pytest + +from mytoyota.utils.logs import censor_all, censor_string, censor_value + + +# Test censor_value function +@pytest.mark.parametrize( + "test_id, value, key, to_censor, expected", + [ + ("happy-1", "SensitiveData", "authorization", {"authorization"}, "Se***********"), + ("happy-2", 123.456, "latitude", {"latitude"}, 123), + ( + "happy-3", + {"key1": "value1", "password": "12345"}, + "password", + {"password"}, + {"key1": "value1", "password": "12***"}, + ), + ("happy-4", ["SensitiveData1", "SensitiveData2"], "emails", {"emails"}, ["Se************", "Se************"]), + ("edge-1", "", "empty", {"empty"}, ""), + ("edge-2", "AB", "short", {"short"}, "AB"), + ("edge-3", {"key": "value"}, "key", set(), {"key": "value"}), + ("error-1", None, "none", {"none"}, None), + ("error-2", 123, "int", {"int"}, 123), + ], +) +def test_censor_value(test_id, value, key, to_censor, expected): # noqa: D103, ARG001 + # Act + result = censor_value(value, key, to_censor) + + # Assert + assert result == expected + + +# Test censor_all function +@pytest.mark.parametrize( + "test_id, dictionary, to_censor, expected", + [ + ( + "happy-1", + {"username": "user123", "password": "secret"}, + {"password"}, + {"username": "user123", "password": "se****"}, + ), + ( + "happy-2", + {"latitude": 123.456, "longitude": -123.456}, + {"latitude", "longitude"}, + {"latitude": 123, "longitude": -123}, + ), + ( + "edge-1", + {"key": "value"}, + set(), + {"key": "value"}, + ), + ( + "error-1", + {"username": "user123", "password": None}, + {"password"}, + {"username": "user123", "password": None}, + ), + ], +) +def test_censor_all(test_id, dictionary, to_censor, expected): # noqa: D103, ARG001 + # Act + result = censor_all(dictionary, to_censor) + + # Assert + assert result == expected + + +# Test censor_string function +@pytest.mark.parametrize( + "test_id, string, expected", + [ + ("happy-1", "SensitiveData", "Se***********"), + ("edge-1", "", ""), + ("edge-2", "AB", "AB"), + ("edge-3", "A", "A"), + ], +) +def test_censor_string(test_id, string, expected): # noqa: D103, ARG001 + # Act + result = censor_string(string) + + # Assert + assert result == expected