From 1b3a80fb6f86b721e815e0877820d0a865d2ca09 Mon Sep 17 00:00:00 2001 From: Jordan Shaw Date: Sun, 26 Jul 2026 19:36:41 -0400 Subject: [PATCH] Structure missing-token authentication errors --- CHANGELOG.md | 5 +++++ datanet/__init__.py | 2 +- datanet/client.py | 14 +++++++++----- pyproject.toml | 2 +- tests/test_client.py | 27 +++++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 151fb6a..5c833fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.1.2 - 2026-07-26 + +- Raised a structured `DataNetError` when an authentication response succeeds + without returning a usable JWT. + ## 0.1.1 - 2026-07-17 - Added `get_presence(channel)` for authoritative occupancy and member lookups. diff --git a/datanet/__init__.py b/datanet/__init__.py index 0467df5..3408720 100644 --- a/datanet/__init__.py +++ b/datanet/__init__.py @@ -32,4 +32,4 @@ "build_art_dmx_packet", "build_dmx_frame", ] -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/datanet/client.py b/datanet/client.py index ab3ef4a..668925d 100644 --- a/datanet/client.py +++ b/datanet/client.py @@ -953,11 +953,15 @@ async def _fetch_jwt(self) -> str: code=code, status=resp.status, ) - payload = await resp.json() - token = payload.get("token") - if not token: - raise RuntimeError( - f"DataNet auth failed: missing token in response {payload!r}" + body = await resp.json() + token = body.get("token") if isinstance(body, dict) else None + if not isinstance(token, str) or not token.strip(): + detail = body.get("error") if isinstance(body, dict) else None + raise DataNetError( + f"DataNet auth failed ({resp.status}): " + f"{detail or 'response missing token'}", + code="authentication_failed", + status=resp.status, ) logger.debug("DataNet: JWT acquired.") return token diff --git a/pyproject.toml b/pyproject.toml index fcdae70..2b01395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "datanet-sdk" -version = "0.1.1" +version = "0.1.2" description = "DataNet Python SDK — async realtime pub/sub client for the DataNet platform" readme = "README.md" license = { text = "MIT" } diff --git a/tests/test_client.py b/tests/test_client.py index 4c65f9e..8819463 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,6 +8,7 @@ AnyMessage, BinaryMessageMeta, DataNet, + DataNetError, MessageMeta, binary_to_base64, build_art_dmx_packet, @@ -63,6 +64,17 @@ def post(self, url, **kwargs): return FakePostResponse() +class FakeMissingTokenResponse(FakePostResponse): + async def json(self): + return {"error": "invalid apiKey"} + + +class FakeMissingTokenSession(FakeClientSession): + def post(self, url, **kwargs): + self.calls.append((url, kwargs)) + return FakeMissingTokenResponse() + + class FakePresenceResponse: status = 200 @@ -393,6 +405,21 @@ async def test_auth_payload_includes_device_metadata(self): }, ) + async def test_auth_response_without_token_raises_structured_error(self): + client = DataNet("ak_invalid") + + with patch.object( + client_module.aiohttp, + "ClientSession", + FakeMissingTokenSession, + ): + with self.assertRaises(DataNetError) as raised: + await client._fetch_jwt() + + self.assertEqual(raised.exception.code, "authentication_failed") + self.assertEqual(raised.exception.status, 200) + self.assertIn("invalid apiKey", str(raised.exception)) + async def test_heartbeat_sends_hb_envelope(self): client = DataNet("ak_test") ws = FakeWebSocket()