Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/hatch_rest_api/util_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ async def get_rest_devices(
client_id=f"hatch_rest_api/{safe_email}/{str(uuid4())}",
on_connection_interrupted=on_connection_interrupted,
on_connection_resumed=on_connection_resumed,
# Opt out of the AWS IoT SDK metrics that awsiot otherwise appends
# to the CONNECT packet username. They report AWS SDK/platform
# details to AWS and are of no use to us, but building them makes
# awscrt introspect private ClientTlsContext internals
# (tls_ctx._certificate_source). On installs where awscrt's modules
# are not all from the same version, that attribute is missing and
# the connection blows up before it is ever attempted:
# AttributeError: 'ClientTlsContext' object
# has no attribute '_certificate_source'
# Disabling metrics skips that code path entirely.
enable_metrics_collection=False,
),
)
try:
Expand Down
121 changes: 121 additions & 0 deletions tests/test_util_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import asyncio
import unittest
from unittest.mock import MagicMock, patch

from hatch_rest_api import util_bootstrap


class FakeHatch:
"""Stands in for the Hatch REST client, returning canned payloads."""

def __init__(self, *args, **kwargs):
self.api_session = MagicMock()

async def login(self, **kwargs):
return "auth-token"

async def iot_devices(self, **kwargs):
return [
{
"product": "restMini",
"name": "Nursery",
"thingName": "thing-1",
"macAddress": "AA:BB:CC:DD:EE:FF",
}
]

async def token(self, **kwargs):
return {
"region": "us-east-1",
"identityId": "identity-1",
"token": "aws-token",
"endpoint": "https://example-ats.iot.us-east-1.amazonaws.com",
}


class FakeAwsHttp:
def __init__(self, *args, **kwargs):
pass

async def aws_credentials(self, **kwargs):
return {
"Credentials": {
"AccessKeyId": "key",
"SecretKey": "secret",
"SessionToken": "session",
"Expiration": 1780000000,
}
}


class FakeShadowClient:
"""IotShadowClient stub.

subscribe_* returns the (future, topic) pair the real client returns;
publish_* returns a future whose .result() is a MagicMock.
"""

def __getattr__(self, name):
if name.startswith("subscribe_"):
return lambda *args, **kwargs: (MagicMock(), MagicMock())
return lambda *args, **kwargs: MagicMock()


class GetRestDevicesMetricsTest(unittest.TestCase):
"""Regression guard for dahlb/ha_hatch#323.

awsiot defaults ``enable_metrics_collection`` to True, which makes awscrt
build an AWS IoT SDK metrics string by reading private ClientTlsContext
internals. On installs whose awscrt modules are not all the same version
that read raises::

AttributeError: 'ClientTlsContext' object has no attribute
'_certificate_source'

which fails setup before the MQTT connection is even attempted. We must
keep passing enable_metrics_collection=False so that path is never entered.
"""

def _run_bootstrap(self):
builder = MagicMock(return_value=MagicMock())

with (
patch.object(util_bootstrap, "Hatch", FakeHatch),
patch.object(util_bootstrap, "Contentful", MagicMock()),
patch.object(util_bootstrap, "AwsHttp", FakeAwsHttp),
patch.object(util_bootstrap, "AwsCredentialsProvider", MagicMock()),
patch.object(util_bootstrap, "io", MagicMock()),
patch.object(
util_bootstrap, "IotShadowClient", lambda *a, **kw: FakeShadowClient()
),
patch.object(
util_bootstrap, "websockets_with_default_aws_signing", builder
),
):
asyncio.run(
util_bootstrap.get_rest_devices(
email="user@example.com", password="hunter2"
)
)

return builder

def test_metrics_collection_disabled(self):
builder = self._run_bootstrap()

builder.assert_called_once()
self.assertIs(builder.call_args.kwargs["enable_metrics_collection"], False)

def test_devices_still_created(self):
builder = self._run_bootstrap()

# Sanity check that disabling metrics did not disturb the rest of the
# bootstrap: the connection is still built and devices still returned.
self.assertEqual(
builder.call_args.kwargs["endpoint"],
"example-ats.iot.us-east-1.amazonaws.com",
)


if __name__ == "__main__":
unittest.main()