Skip to content

Commit 45f9ef6

Browse files
committed
mqtt: api: add more unit-tests
1 parent 9346bc4 commit 45f9ef6

File tree

16 files changed

+492
-26
lines changed

16 files changed

+492
-26
lines changed

src/enapter/mqtt/api/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212

1313
class Client:
1414

15-
def __init__(self, config: Config, task_group: asyncio.TaskGroup | None) -> None:
15+
def __init__(
16+
self, config: Config, task_group: asyncio.TaskGroup | None = None
17+
) -> None:
1618
self._config = config
1719
self._client = self._new_client(task_group=task_group)
1820

src/enapter/mqtt/api/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def from_env(
4646
return cls(
4747
host=env[prefix + "HOST"],
4848
port=int(env[prefix + "PORT"]),
49-
user=env.get(prefix + "USER", default=None),
50-
password=env.get(prefix + "PASSWORD", default=None),
49+
user=env.get(prefix + "USER", None),
50+
password=env.get(prefix + "PASSWORD", None),
5151
tls_config=TLSConfig.from_env(env, namespace=namespace),
5252
)
5353

src/enapter/mqtt/api/device/command_request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def from_dto(cls, dto: dict[str, Any]) -> Self:
1818
return cls(
1919
id=dto["id"],
2020
name=dto["name"],
21-
arguments=dto.get("arguments", {}),
21+
arguments=dto["arguments"] if dto.get("arguments") is not None else {},
2222
)
2323

2424
def to_dto(self) -> dict[str, Any]:

src/enapter/mqtt/api/device/message.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ class Message(abc.ABC):
88
@classmethod
99
@abc.abstractmethod
1010
def from_dto(cls, dto: dict[str, Any]) -> Self:
11-
pass
11+
pass # pragma: no cover
1212

1313
@abc.abstractmethod
1414
def to_dto(self) -> dict[str, Any]:
15-
pass
15+
pass # pragma: no cover
1616

1717
@classmethod
1818
def from_json(cls, data: str | bytes) -> Self:

src/enapter/mqtt/api/device/properties.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class Properties(Message):
99

1010
timestamp: int
11-
values: dict[str, Any] = dataclasses.field(default_factory=dict)
11+
values: dict[str, Any]
1212

1313
def __post_init__(self) -> None:
1414
if "timestamp" in self.values:

src/enapter/mqtt/api/device/telemetry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
class Telemetry(Message):
99

1010
timestamp: int
11-
alerts: list[str] | None = None
12-
values: dict[str, Any] = dataclasses.field(default_factory=dict)
11+
alerts: list[str] | None
12+
values: dict[str, Any]
1313

1414
def __post_init__(self) -> None:
1515
if "timestamp" in self.values:

tests/fake_data_generator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ def hardware_id(self) -> str:
2020
def channel_id(self) -> str:
2121
return self._fake.word()
2222

23+
def uuid(self) -> str:
24+
return self._fake.uuid4()
25+
26+
def method_name(self) -> str:
27+
if self._fake.boolean():
28+
return self._fake.word()
29+
else:
30+
return self._fake.word() + "_" + self._fake.word()
31+
2332
def _new_faker(self, seed) -> faker.Faker:
2433
fake = faker.Faker()
2534
fake.seed_instance(seed)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import enapter
2+
3+
4+
def test_device_channel() -> None:
5+
client = enapter.mqtt.api.Client(
6+
enapter.mqtt.api.Config(
7+
host="mqtt.example.com",
8+
port=8883,
9+
user="testuser",
10+
password="testpass",
11+
tls_config=None,
12+
)
13+
)
14+
channel = client.device_channel("hardware123", "channelABC")
15+
assert channel.hardware_id == "hardware123"
16+
assert channel.channel_id == "channelABC"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import pytest
2+
3+
import enapter
4+
5+
6+
def test_plaintext_config_from_env() -> None:
7+
env = {
8+
"ENAPTER_MQTT_API_HOST": "mqtt.example.com",
9+
"ENAPTER_MQTT_API_PORT": "8883",
10+
"ENAPTER_MQTT_API_USER": "testuser",
11+
"ENAPTER_MQTT_API_PASSWORD": "testpass",
12+
}
13+
config = enapter.mqtt.api.Config.from_env(env)
14+
assert config.host == "mqtt.example.com"
15+
assert config.port == 8883
16+
assert config.user == "testuser"
17+
assert config.password == "testpass"
18+
assert config.tls is None
19+
20+
21+
def test_tls_config_from_env() -> None:
22+
env = {
23+
"ENAPTER_MQTT_API_HOST": "mqtt.example.com",
24+
"ENAPTER_MQTT_API_PORT": "8883",
25+
"ENAPTER_MQTT_API_TLS_SECRET_KEY": "my_secret_key",
26+
"ENAPTER_MQTT_API_TLS_CERT": "my_cert",
27+
"ENAPTER_MQTT_API_TLS_CA_CERT": "my_ca_cert",
28+
}
29+
config = enapter.mqtt.api.Config.from_env(env)
30+
assert config.host == "mqtt.example.com"
31+
assert config.port == 8883
32+
assert config.user is None
33+
assert config.password is None
34+
assert config.tls is not None
35+
assert config.tls.secret_key == "my_secret_key"
36+
assert config.tls.cert == "my_cert"
37+
assert config.tls.ca_cert == "my_ca_cert"
38+
39+
40+
def test_tls_config_secret_key_missing() -> None:
41+
env = {
42+
"ENAPTER_MQTT_API_TLS_CERT": "my_cert",
43+
"ENAPTER_MQTT_API_TLS_CA_CERT": "my_ca_cert",
44+
}
45+
with pytest.raises(KeyError):
46+
enapter.mqtt.api.TLSConfig.from_env(env)
47+
48+
49+
def test_tls_config_cert_missing() -> None:
50+
env = {
51+
"ENAPTER_MQTT_API_TLS_SECRET_KEY": "my_secret_key",
52+
"ENAPTER_MQTT_API_TLS_CA_CERT": "my_ca_cert",
53+
}
54+
with pytest.raises(KeyError):
55+
enapter.mqtt.api.TLSConfig.from_env(env)
56+
57+
58+
def test_tls_config_ca_cert_missing() -> None:
59+
env = {
60+
"ENAPTER_MQTT_API_TLS_SECRET_KEY": "my_secret_key",
61+
"ENAPTER_MQTT_API_TLS_CERT": "my_cert",
62+
}
63+
with pytest.raises(KeyError):
64+
enapter.mqtt.api.TLSConfig.from_env(env)
65+
66+
67+
def test_tls_config_replace_newlines() -> None:
68+
env = {
69+
"ENAPTER_MQTT_API_TLS_SECRET_KEY": "line1\\nline2",
70+
"ENAPTER_MQTT_API_TLS_CERT": "line1\\nline2",
71+
"ENAPTER_MQTT_API_TLS_CA_CERT": "line1\\nline2",
72+
}
73+
tls_config = enapter.mqtt.api.TLSConfig.from_env(env)
74+
assert tls_config is not None
75+
assert tls_config.secret_key == "line1\nline2"
76+
assert tls_config.cert == "line1\nline2"
77+
assert tls_config.ca_cert == "line1\nline2"
Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,110 @@
11
from unittest import mock
22

33
import enapter
4-
import tests
54

65

7-
async def test_publish_telemetry(fake: tests.FakeDataGenerator) -> None:
8-
hardware_id = fake.hardware_id()
9-
channel_id = fake.channel_id()
10-
timestamp = fake.timestamp()
6+
async def test_subscribe_to_command_requests() -> None:
7+
@enapter.async_.generator
8+
async def subscribe(topic: str):
9+
assert (
10+
topic
11+
== "v1/to/6BAA9455E3E70682C2094CAC629F6FBED82C07CD/main/v1/command/requests"
12+
)
13+
yield enapter.mqtt.Message(
14+
topic=topic,
15+
payload='{"id": "bbe17a10-3107-47cb-b0ec-99648debade6", "name": "my_command", "arguments": {"foo": "bar"}}',
16+
qos=0,
17+
retain=False,
18+
mid=1,
19+
properties=None,
20+
)
21+
22+
mock_client = mock.AsyncMock()
23+
mock_client.subscribe = subscribe
24+
channel = enapter.mqtt.api.device.Channel(
25+
client=mock_client,
26+
hardware_id="6BAA9455E3E70682C2094CAC629F6FBED82C07CD",
27+
channel_id="main",
28+
)
29+
async with channel.subscribe_to_command_requests() as requests:
30+
request = await requests.__anext__()
31+
assert request == enapter.mqtt.api.device.CommandRequest(
32+
id="bbe17a10-3107-47cb-b0ec-99648debade6",
33+
name="my_command",
34+
arguments={"foo": "bar"},
35+
)
36+
37+
38+
async def test_publish_command_response() -> None:
39+
mock_client = mock.AsyncMock()
40+
channel = enapter.mqtt.api.device.Channel(
41+
client=mock_client,
42+
hardware_id="6BAA9455E3E70682C2094CAC629F6FBED82C07CD",
43+
channel_id="main",
44+
)
45+
await channel.publish_command_response(
46+
enapter.mqtt.api.device.CommandResponse(
47+
id="bbe17a10-3107-47cb-b0ec-99648debade6",
48+
state=enapter.mqtt.api.device.CommandState.COMPLETED,
49+
payload={"foo": "bar"},
50+
)
51+
)
52+
mock_client.publish.assert_called_once_with(
53+
"v1/from/6BAA9455E3E70682C2094CAC629F6FBED82C07CD/main/v1/command/responses",
54+
'{"id": "bbe17a10-3107-47cb-b0ec-99648debade6", "state": "completed", "payload": {"foo": "bar"}}',
55+
)
56+
57+
58+
async def test_publish_telemetry() -> None:
1159
mock_client = mock.AsyncMock()
1260
channel = enapter.mqtt.api.device.Channel(
13-
client=mock_client, hardware_id=hardware_id, channel_id=channel_id
61+
client=mock_client,
62+
hardware_id="6BAA9455E3E70682C2094CAC629F6FBED82C07CD",
63+
channel_id="main",
1464
)
1565
await channel.publish_telemetry(
16-
enapter.mqtt.api.device.Telemetry(timestamp=timestamp)
66+
enapter.mqtt.api.device.Telemetry(
67+
timestamp=1234567890, alerts=["aaa"], values={"foo": "bar"}
68+
)
1769
)
1870
mock_client.publish.assert_called_once_with(
19-
f"v1/from/{hardware_id}/{channel_id}/v1/telemetry",
20-
'{"timestamp": ' + str(timestamp) + ', "alerts": null}',
71+
"v1/from/6BAA9455E3E70682C2094CAC629F6FBED82C07CD/main/v1/telemetry",
72+
'{"timestamp": 1234567890, "alerts": ["aaa"], "foo": "bar"}',
2173
)
2274

2375

24-
async def test_publish_properties(fake: tests.FakeDataGenerator) -> None:
25-
hardware_id = fake.hardware_id()
26-
channel_id = fake.channel_id()
27-
timestamp = fake.timestamp()
76+
async def test_publish_properties() -> None:
2877
mock_client = mock.AsyncMock()
2978
channel = enapter.mqtt.api.device.Channel(
30-
client=mock_client, hardware_id=hardware_id, channel_id=channel_id
79+
client=mock_client,
80+
hardware_id="6BAA9455E3E70682C2094CAC629F6FBED82C07CD",
81+
channel_id="main",
3182
)
3283
await channel.publish_properties(
33-
enapter.mqtt.api.device.Properties(timestamp=timestamp)
84+
enapter.mqtt.api.device.Properties(timestamp=1234567890, values={"foo": "bar"})
85+
)
86+
mock_client.publish.assert_called_once_with(
87+
"v1/from/6BAA9455E3E70682C2094CAC629F6FBED82C07CD/main/v1/register",
88+
'{"timestamp": 1234567890, "foo": "bar"}',
89+
)
90+
91+
92+
async def test_publish_log() -> None:
93+
mock_client = mock.AsyncMock()
94+
channel = enapter.mqtt.api.device.Channel(
95+
client=mock_client,
96+
hardware_id="6BAA9455E3E70682C2094CAC629F6FBED82C07CD",
97+
channel_id="main",
98+
)
99+
await channel.publish_log(
100+
enapter.mqtt.api.device.Log(
101+
timestamp=1234567890,
102+
severity=enapter.mqtt.api.device.LogSeverity.INFO,
103+
message="Test log",
104+
persist=False,
105+
)
34106
)
35107
mock_client.publish.assert_called_once_with(
36-
f"v1/from/{hardware_id}/{channel_id}/v1/register",
37-
'{"timestamp": ' + str(timestamp) + "}",
108+
"v1/from/6BAA9455E3E70682C2094CAC629F6FBED82C07CD/main/v3/logs",
109+
'{"timestamp": 1234567890, "message": "Test log", "severity": "info", "persist": false}',
38110
)

0 commit comments

Comments
 (0)