diff --git a/tests/common.py b/tests/common.py index 9a04a05..9dc3cc5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -2,43 +2,55 @@ from __future__ import annotations import json -import os from dataclasses import dataclass +from pathlib import Path from typing import Any from aiohttp import WSMsgType class WSMessage: - def __init__(self, type: WSMsgType, json: dict | None = None) -> None: - self.type = type + """WSMessage.""" + + def __init__(self, messagetype: WSMsgType, json: dict | None = None) -> None: + """Initialize.""" + self.type = messagetype self._json = json - def json(self): + def json(self) -> dict | None: + """json.""" return self._json -def load_response(filename): +def load_response(filename: str) -> dict[str, Any]: """Load a response.""" filename = f"{filename}.json" if "." not in filename else filename - path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), + path = Path( + Path.resolve(Path(__file__)).parent, "responses", filename.lower().replace("/", "_"), ) - with open(path, encoding="utf-8") as fptr: + with path.open(encoding="utf-8") as fptr: return json.loads(fptr.read()) class WSMessageHandler: - messages = [] + """WSMessageHandler.""" + + def __init__(self) -> None: + """Initialize.""" + self.messages = [] - def add(self, msg: WSMessage): + def add(self, msg: WSMessage) -> None: + """Add.""" self.messages.append(msg) - def get(self): + def get(self) -> WSMessage: + """Get.""" return ( - self.messages.pop(0) if self.messages else WSMessage(type=WSMsgType.CLOSED) + self.messages.pop(0) + if self.messages + else WSMessage(messagetype=WSMsgType.CLOSED) ) @@ -56,16 +68,16 @@ class MockResponse: mock_status: int = 200 @property - def status(self): + def status(self) -> int: """status.""" return self.mock_status @property - def reason(self): - """Return the reason""" + def reason(self) -> str: + """Return the reason.""" return "unknown" - async def json(self, **_): + async def json(self, **_: Any) -> Any: """json.""" if self.mock_raises is not None: raise self.mock_raises # pylint: disable=raising-bad-type @@ -77,10 +89,10 @@ async def json(self, **_): return self.mock_data return load_response(self.mock_endpoint) - def release(self): + def release(self) -> None: """release.""" - def clear(self): + def clear(self) -> None: """clear.""" self.mock_data = None self.mock_endpoint = "" @@ -88,24 +100,27 @@ def clear(self): self.mock_raises = None self.mock_status = 200 - async def wait_for_close(self): - pass + async def wait_for_close(self) -> None: + """wait_for_close.""" class MockedRequests: """Mock request class.""" - _calls = [] + def __init__(self) -> None: + """Initialize.""" + self._calls = [] - def add(self, url: str): + def add(self, url: str) -> None: """add.""" self._calls.append(url) - def clear(self): + def clear(self) -> None: """clear.""" self._calls.clear() def __repr__(self) -> str: + """repr.""" return f"" @property diff --git a/tests/conftest.py b/tests/conftest.py index ca1310d..b433f96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ """Test fixtures and configuration.""" import logging +from typing import Any, AsyncGenerator import aiohttp import pytest @@ -15,39 +16,43 @@ @pytest.fixture -def mock_requests(): +def mock_requests() -> MockedRequests: """Return a new mock request instanse.""" return MockedRequests() @pytest.fixture -def mock_response(): +def mock_response() -> MockResponse: """Return a new mock response instanse.""" return MockResponse() @pytest.fixture -def mock_ws_messages(): +def mock_ws_messages() -> WSMessageHandler: """Return a new mock ws instanse.""" return WSMessageHandler() @pytest_asyncio.fixture -async def client_session(mock_response, mock_requests, mock_ws_messages): +async def client_session( + mock_response: MockResponse, + mock_requests: MockedRequests, + mock_ws_messages: WSMessageHandler, +) -> AsyncGenerator[aiohttp.ClientSession, None]: """Mock our the request part of the client session.""" class MockedWSContext: @property - def closed(self): + def closed(self) -> bool: return len(mock_ws_messages.messages) == 0 - async def receive(self): + async def receive(self) -> Any: return mock_ws_messages.get() - async def _mocked_ws_connect(*args, **kwargs): + async def _mocked_ws_connect(*_: Any, **__: Any) -> Any: return MockedWSContext() - async def _mocked_request(*args, **kwargs): + async def _mocked_request(*args: Any, **kwargs: Any) -> Any: if len(args) > 2: mock_response.mock_endpoint = args[2].split("/api/")[-1] mock_requests.add({"method": args[1], "url": args[2], **kwargs}) @@ -58,18 +63,20 @@ async def _mocked_request(*args, **kwargs): async with aiohttp.ClientSession() as session: mock_requests.clear() - session._request = _mocked_request # pylint: disable=protected-access - session._ws_connect = _mocked_ws_connect + session._request = _mocked_request # noqa: SLF001 + session._ws_connect = _mocked_ws_connect # noqa: SLF001 yield session @pytest_asyncio.fixture -async def api_client(client_session): +async def api_client( + client_session: AsyncGenerator[aiohttp.ClientSession, None], +) -> AsyncGenerator[ApiClient, None]: """Fixture to provide a API Client.""" yield ApiClient( host="127.0.0.1", port=1337, username="test", - password="test", + password="test", # noqa: S106 client_session=client_session, ) diff --git a/tests/ruff.toml b/tests/ruff.toml new file mode 100644 index 0000000..202a56b --- /dev/null +++ b/tests/ruff.toml @@ -0,0 +1,6 @@ +# This extend our general Ruff rules specifically for tests +extend = "../pyproject.toml" + +lint.extend-ignore = [ + "S101", # Use of assert detected. +] diff --git a/tests/test_api_calls.py b/tests/test_api_calls.py index aef82f6..1f66461 100644 --- a/tests/test_api_calls.py +++ b/tests/test_api_calls.py @@ -5,14 +5,14 @@ @pytest.mark.asyncio -async def test_server(api_client: ApiClient): +async def test_server(api_client: ApiClient) -> None: """Test /server endpoint.""" response = await api_client.get_server() assert response["id"] == 0 @pytest.mark.asyncio -async def test_devices(api_client: ApiClient): +async def test_devices(api_client: ApiClient) -> None: """Test /devices endpoint.""" response = await api_client.get_devices() assert isinstance(response, list) @@ -20,7 +20,7 @@ async def test_devices(api_client: ApiClient): @pytest.mark.asyncio -async def test_geofences(api_client: ApiClient): +async def test_geofences(api_client: ApiClient) -> None: """Test /geofences endpoint.""" response = await api_client.get_geofences() assert isinstance(response, list) @@ -28,7 +28,7 @@ async def test_geofences(api_client: ApiClient): @pytest.mark.asyncio -async def test_positions(api_client: ApiClient): +async def test_positions(api_client: ApiClient) -> None: """Test /positions endpoint.""" response = await api_client.get_positions() assert isinstance(response, list) @@ -36,7 +36,7 @@ async def test_positions(api_client: ApiClient): @pytest.mark.asyncio -async def test_reports_events(api_client: ApiClient): +async def test_reports_events(api_client: ApiClient) -> None: """Test /reports/events endpoint.""" response = await api_client.get_reports_events() assert isinstance(response, list) diff --git a/tests/test_base_api.py b/tests/test_base_api.py index 873498b..07e2e3a 100644 --- a/tests/test_base_api.py +++ b/tests/test_base_api.py @@ -15,7 +15,7 @@ @pytest.mark.asyncio -async def test_base_api(api_client: ApiClient): +async def test_base_api(api_client: ApiClient) -> None: """Test base API.""" response = await api_client.get_server() assert response["bingKey"] == "string" @@ -24,7 +24,7 @@ async def test_base_api(api_client: ApiClient): @pytest.mark.asyncio async def test_base_api_unauthenticated( api_client: ApiClient, mock_response: MockResponse -): +) -> None: """Test unauthenticated base API.""" mock_response.mock_status = 401 with pytest.raises(TraccarAuthenticationException): @@ -32,7 +32,9 @@ async def test_base_api_unauthenticated( @pytest.mark.asyncio -async def test_base_api_issue(api_client: ApiClient, mock_response: MockResponse): +async def test_base_api_issue( + api_client: ApiClient, mock_response: MockResponse +) -> None: """Test API issue.""" mock_response.mock_status = 500 with pytest.raises(TraccarResponseException): @@ -40,7 +42,9 @@ async def test_base_api_issue(api_client: ApiClient, mock_response: MockResponse @pytest.mark.asyncio -async def test_base_api_timeout(api_client: ApiClient, mock_response: MockResponse): +async def test_base_api_timeout( + api_client: ApiClient, mock_response: MockResponse +) -> None: """Test API issue.""" mock_response.mock_raises = asyncio.TimeoutError with pytest.raises(TraccarConnectionException): diff --git a/tests/test_client.py b/tests/test_client.py index 860a16b..f8b7012 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,7 +6,7 @@ @pytest.mark.asyncio -async def test_client_init(client_session: ClientSession): +async def test_client_init(client_session: ClientSession) -> None: """Test client init.""" client_params = { "host": "127.0.0.1", @@ -16,12 +16,12 @@ async def test_client_init(client_session: ClientSession): "client_session": client_session, } - assert ApiClient(**client_params)._base_url == "http://127.0.0.1:8080/api" + assert ApiClient(**client_params)._base_url == "http://127.0.0.1:8080/api" # noqa: SLF001 assert ( - ApiClient(**{**client_params, "port": None})._base_url + ApiClient(**{**client_params, "port": None})._base_url # noqa: SLF001 == "http://127.0.0.1:8082/api" ) assert ( - ApiClient(**{**client_params, "ssl": True})._base_url + ApiClient(**{**client_params, "ssl": True})._base_url # noqa: SLF001 == "https://127.0.0.1:8080/api" ) diff --git a/tests/test_subscription.py b/tests/test_subscription.py index 7046fea..506d04f 100644 --- a/tests/test_subscription.py +++ b/tests/test_subscription.py @@ -1,5 +1,8 @@ """Test subscription.""" +from __future__ import annotations + import asyncio +from typing import Any, NoReturn from unittest.mock import patch import aiohttp @@ -12,43 +15,41 @@ TraccarConnectionException, TraccarException, ) -from tests.common import WSMessage +from tests.common import WSMessage, WSMessageHandler @pytest.mark.parametrize( "messages", - ( + [ [ - WSMessage(type=WSMsgType.TEXT, json={}), - WSMessage(type=WSMsgType.TEXT, json=None), + WSMessage(messagetype=WSMsgType.TEXT, json={}), + WSMessage(messagetype=WSMsgType.TEXT, json=None), ], [ - WSMessage(type=WSMsgType.TEXT, json={"devices": []}), + WSMessage(messagetype=WSMsgType.TEXT, json={"devices": []}), ], [ - WSMessage(type=WSMsgType.TEXT, json={"positions": []}), + WSMessage(messagetype=WSMsgType.TEXT, json={"positions": []}), ], [ - WSMessage(type=WSMsgType.TEXT, json={"events": []}), + WSMessage(messagetype=WSMsgType.TEXT, json={"events": []}), ], [ - WSMessage(type=WSMsgType.TEXT, json={"devices": []}), - WSMessage(type=WSMsgType.TEXT, json={"positions": []}), - WSMessage(type=WSMsgType.TEXT, json={"events": []}), + WSMessage(messagetype=WSMsgType.TEXT, json={"devices": []}), + WSMessage(messagetype=WSMsgType.TEXT, json={"positions": []}), + WSMessage(messagetype=WSMsgType.TEXT, json={"events": []}), ], [ - WSMessage( - type=WSMsgType.TEXT, json={"events": [], "devices": [], "events": []} - ), + WSMessage(messagetype=WSMsgType.TEXT, json={"events": [], "devices": []}), ], - ), + ], ) @pytest.mark.asyncio async def test_subscription_text_message( api_client: ApiClient, messages: list[WSMessage], - mock_ws_messages, -): + mock_ws_messages: WSMessageHandler, +) -> None: """Test subscription text message.""" _handled = [] _expected_handled = [] @@ -64,7 +65,7 @@ async def test_subscription_text_message( } ) - async def _handler(data): + async def _handler(data: Any) -> None: _handled.append(data) await api_client.subscribe(_handler) @@ -73,23 +74,23 @@ async def _handler(data): @pytest.mark.parametrize( "message", - ( - WSMessage(type=WSMsgType.CLOSE), - WSMessage(type=WSMsgType.CLOSED), - WSMessage(type=WSMsgType.ERROR), - ), + [ + WSMessage(messagetype=WSMsgType.CLOSE), + WSMessage(messagetype=WSMsgType.CLOSED), + WSMessage(messagetype=WSMsgType.ERROR), + ], ) @pytest.mark.asyncio async def test_subscription_stopping_message( api_client: ApiClient, message: WSMessage, - mock_ws_messages, -): + mock_ws_messages: WSMessageHandler, +) -> None: """Test subscription stopping message.""" _handled = [] mock_ws_messages.add(message) - async def _handler(data): + async def _handler(data: Any) -> None: _handled.append(data) with pytest.raises( @@ -102,26 +103,26 @@ async def _handler(data): @pytest.mark.parametrize( "message", - ( - WSMessage(type=WSMsgType.CONTINUATION), - WSMessage(type=WSMsgType.BINARY), - WSMessage(type=WSMsgType.PING), - WSMessage(type=WSMsgType.PONG), - WSMessage(type=WSMsgType.CLOSING), - ), + [ + WSMessage(messagetype=WSMsgType.CONTINUATION), + WSMessage(messagetype=WSMsgType.BINARY), + WSMessage(messagetype=WSMsgType.PING), + WSMessage(messagetype=WSMsgType.PONG), + WSMessage(messagetype=WSMsgType.CLOSING), + ], ) @pytest.mark.asyncio async def test_subscription_unknown_type( api_client: ApiClient, message: WSMessage, - mock_ws_messages, + mock_ws_messages: WSMessageHandler, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test subscription unknown type.""" _handled = [] mock_ws_messages.add(message) - async def _handler(data): + async def _handler(data: Any) -> None: _handled.append(data) assert f"Unexpected message type {message.type.name}" not in caplog.text @@ -135,13 +136,13 @@ async def _handler(data): @pytest.mark.asyncio async def test_subscription_bad_handler( api_client: ApiClient, - mock_ws_messages, + mock_ws_messages: WSMessageHandler, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test subscription unknown type.""" - mock_ws_messages.add(WSMessage(type=WSMsgType.TEXT, json={"devices": []})) + mock_ws_messages.add(WSMessage(messagetype=WSMsgType.TEXT, json={"devices": []})) - async def _handler(data): + async def _handler(_: Any) -> NoReturn: raise ValueError("Bad handler") await api_client.subscribe(_handler) @@ -151,7 +152,7 @@ async def _handler(data): @pytest.mark.parametrize( ("side_effect", "raises", "with_message"), - ( + [ ( KeyError("boom"), TraccarException, @@ -172,7 +173,7 @@ async def _handler(data): TraccarConnectionException, "", ), - ), + ], ) @pytest.mark.asyncio async def test_subscription_exceptions( @@ -180,7 +181,7 @@ async def test_subscription_exceptions( side_effect: Exception, raises: Exception, with_message: str, -): +) -> None: """Test subscription exceptions.""" assert api_client.subscription_status == SubscriptionStatus.DISCONNECTED with patch( @@ -195,7 +196,7 @@ async def test_subscription_exceptions( @pytest.mark.asyncio -async def test_subscription_cancelation(api_client: ApiClient): +async def test_subscription_cancelation(api_client: ApiClient) -> None: """Test subscription exceptions.""" assert api_client.subscription_status == SubscriptionStatus.DISCONNECTED with patch(