Skip to content

Commit 5239068

Browse files
authored
New tests added (#87)
* new tests added * changed the data * updated unit test * added rate limit tests * updated tests * firmware * added firmware tests * licence added * licence added * new tests have been added * Fixed Thread by replacing target=self.__service_loop_func and then defining __service_loop_func() * Fixed Thread by replacing target=self.__service_loop_func and then defining __service_loop_func() * new tests added * new tests added * new tests added * minor modifications * added firmware tests * added rate limit tests * added tests of tb_device_mqtt operation * split message tests added * added rpc reply tests * added decoded message tests * added tb_gateway_mqtt tests * added init_gateway tests * added tests * licence added and comments removed * bug fixes * add new tests * add new tests * add new tests * modified tests * modified tests * modified tests * modified tests * modified tests * modified tests * modified tests * Changed data * Removed blank lines * Updated test * Updated logic * Updated logic * Data changed and logic changed * Updated data * New test added * New test added * changes removed * Add TBDeviceMqttClient tests * Fix TBDeviceMqttClient tests * Fix TBDeviceMqttClient tests * Fix TBDeviceMqttClient tests * Refactor TBDeviceMqttClient tests * Refactor TBDeviceMqttClient tests * Fix TBDeviceMqttClient tests * Test fix * Function removed * changed function name * New test added * fixed tests and replaced logic * changed data * class removed * removed unnecessary classes and strings * back to the old version * bug fixes * resolve merge conflicts from GitHub * fix: force conflict resolution for tb_device_mqtt.py
1 parent 69844fc commit 5239068

10 files changed

+2224
-13
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2025. ThingsBoard
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
from tb_device_mqtt import TBDeviceMqttClient
17+
18+
19+
class TestCountDataPointsInMessage(unittest.TestCase):
20+
def test_simple_dict_no_device(self):
21+
data = {
22+
"ts": 123456789,
23+
"values": {
24+
"temp": 22.5,
25+
"humidity": 55
26+
}
27+
}
28+
result = TBDeviceMqttClient._count_datapoints_in_message(data)
29+
self.assertEqual(result, 2)
30+
31+
def test_list_of_dict_no_device(self):
32+
data = [
33+
{"ts": 123456789, "values": {"temp": 22.5, "humidity": 55}},
34+
{"ts": 123456799, "values": {"light": 100, "pressure": 760}}
35+
]
36+
result = TBDeviceMqttClient._count_datapoints_in_message(data)
37+
self.assertEqual(result, 4)
38+
39+
def test_with_device_dict_inside(self):
40+
data = {
41+
"MyDevice": {
42+
"ts": 123456789,
43+
"values": {"temp": 22.5, "humidity": 55}
44+
},
45+
"OtherKey": "some_value"
46+
}
47+
result = TBDeviceMqttClient._count_datapoints_in_message(data, device="MyDevice")
48+
self.assertEqual(result, 2)
49+
50+
def test_with_device_list_inside(self):
51+
data = {
52+
"Sensor": [
53+
{"ts": 1, "values": {"v1": 10}},
54+
{"ts": 2, "values": {"v2": 20, "v3": 30}}
55+
]
56+
}
57+
result = TBDeviceMqttClient._count_datapoints_in_message(data, device="Sensor")
58+
self.assertEqual(result, 3)
59+
60+
def test_empty_dict_no_device(self):
61+
data = {}
62+
result = TBDeviceMqttClient._count_datapoints_in_message(data)
63+
self.assertEqual(result, 0)
64+
65+
def test_missing_device_key(self):
66+
67+
data = {"some_unrelated_key": 42}
68+
result = TBDeviceMqttClient._count_datapoints_in_message(data, device="NotExistingDeviceKey")
69+
self.assertEqual(result, 1)
70+
71+
def test_data_is_string_no_device(self):
72+
data = "just a string"
73+
result = TBDeviceMqttClient._count_datapoints_in_message(data)
74+
self.assertEqual(result, 1)
75+
76+
77+
if __name__ == '__main__':
78+
unittest.main()

tests/firmware_tests.py

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
# Copyright 2025. ThingsBoard
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
from unittest.mock import patch, MagicMock, call
17+
from math import ceil
18+
import orjson
19+
from threading import Thread
20+
from tb_device_mqtt import (
21+
TBDeviceMqttClient,
22+
TBTimeoutException,
23+
FW_VERSION_ATTR, FW_TITLE_ATTR, FW_STATE_ATTR
24+
)
25+
from paho.mqtt.client import ReasonCodes
26+
27+
28+
REQUIRED_SHARED_KEYS = "dummy_shared_keys"
29+
30+
31+
class TestFirmwareUpdateBranch(unittest.TestCase):
32+
@patch('tb_device_mqtt.sleep', return_value=None, autospec=True)
33+
@patch('tb_device_mqtt.log.debug', autospec=True)
34+
def test_firmware_update_branch(self, _, mock_sleep):
35+
client = TBDeviceMqttClient('fake_host', username="dummy_token", password="dummy")
36+
37+
client._messages_rate_limit = MagicMock()
38+
39+
client.current_firmware_info = {
40+
"current_" + FW_VERSION_ATTR: "v0",
41+
FW_STATE_ATTR: "IDLE"
42+
}
43+
client.firmware_data = b"old_data"
44+
client._TBDeviceMqttClient__current_chunk = 2
45+
client._TBDeviceMqttClient__firmware_request_id = 0
46+
client._TBDeviceMqttClient__chunk_size = 128
47+
client._TBDeviceMqttClient__target_firmware_length = 0
48+
49+
client.send_telemetry = MagicMock()
50+
client._TBDeviceMqttClient__get_firmware = MagicMock()
51+
52+
message_mock = MagicMock()
53+
message_mock.topic = "v1/devices/me/attributes_update"
54+
payload_dict = {
55+
"fw_version": "v1",
56+
"fw_title": "TestFirmware",
57+
"fw_size": 900
58+
}
59+
message_mock.payload = orjson.dumps(payload_dict)
60+
61+
client._on_decoded_message({}, message_mock)
62+
client.stopped = True
63+
64+
client._messages_rate_limit.increase_rate_limit_counter.assert_called_once()
65+
66+
self.assertEqual(client.firmware_data, b"")
67+
self.assertEqual(client._TBDeviceMqttClient__current_chunk, 0)
68+
self.assertEqual(client.current_firmware_info[FW_STATE_ATTR], "DOWNLOADING")
69+
70+
client.send_telemetry.assert_called_once_with(client.current_firmware_info)
71+
72+
client._TBDeviceMqttClient__get_firmware.assert_called_once()
73+
74+
self.assertEqual(client._TBDeviceMqttClient__firmware_request_id, 1)
75+
self.assertEqual(client._TBDeviceMqttClient__target_firmware_length, 900)
76+
self.assertEqual(client._TBDeviceMqttClient__chunk_count, ceil(900 / 128))
77+
client._TBDeviceMqttClient__get_firmware.assert_called_once()
78+
79+
80+
class TestTBDeviceMqttClient(unittest.TestCase):
81+
@patch('tb_device_mqtt.paho.Client')
82+
def setUp(self, mock_paho_client):
83+
self.mock_mqtt_client = mock_paho_client.return_value
84+
self.client = TBDeviceMqttClient(
85+
host='your_host',
86+
port=1883,
87+
username='your_token',
88+
password=None
89+
)
90+
self.client.firmware_info = {FW_TITLE_ATTR: "dummy_firmware.bin"}
91+
self.client.firmware_data = b''
92+
self.client._TBDeviceMqttClient__current_chunk = 0
93+
self.client._TBDeviceMqttClient__firmware_request_id = 1
94+
self.client._TBDeviceMqttClient__updating_thread = Thread(target=lambda: None)
95+
self.client._publish_data = MagicMock()
96+
97+
if not hasattr(self.client, '_client'):
98+
self.client._client = self.mock_mqtt_client
99+
100+
def test_get_firmware_update(self):
101+
self.client._client.subscribe = MagicMock()
102+
self.client.send_telemetry = MagicMock()
103+
self.client.get_firmware_update()
104+
self.client._client.subscribe.assert_called_with('v2/fw/response/+')
105+
self.client.send_telemetry.assert_called()
106+
self.client._publish_data.assert_called()
107+
108+
def test_firmware_download_process(self):
109+
self.client.firmware_info = {
110+
FW_TITLE_ATTR: "dummy_firmware.bin",
111+
FW_VERSION_ATTR: "2.0",
112+
"fw_size": 1024,
113+
"fw_checksum": "abc123",
114+
"fw_checksum_algorithm": "SHA256"
115+
}
116+
self.client._TBDeviceMqttClient__current_chunk = 0
117+
self.client._TBDeviceMqttClient__firmware_request_id = 1
118+
self.client._TBDeviceMqttClient__get_firmware()
119+
self.client._publish_data.assert_called()
120+
121+
def test_firmware_verification_success(self):
122+
self.client.firmware_data = b'binary data'
123+
self.client.firmware_info = {
124+
FW_TITLE_ATTR: "dummy_firmware.bin",
125+
FW_VERSION_ATTR: "2.0",
126+
"fw_checksum": "valid_checksum",
127+
"fw_checksum_algorithm": "SHA256"
128+
}
129+
self.client._TBDeviceMqttClient__process_firmware()
130+
self.client._publish_data.assert_called()
131+
132+
def test_firmware_verification_failure(self):
133+
self.client.firmware_data = b'corrupt data'
134+
self.client.firmware_info = {
135+
FW_TITLE_ATTR: "dummy_firmware.bin",
136+
FW_VERSION_ATTR: "2.0",
137+
"fw_checksum": "invalid_checksum",
138+
"fw_checksum_algorithm": "SHA256"
139+
}
140+
self.client._TBDeviceMqttClient__process_firmware()
141+
self.client._publish_data.assert_called()
142+
143+
def test_firmware_state_transition(self):
144+
self.client._publish_data.reset_mock()
145+
self.client.current_firmware_info = {
146+
"current_fw_title": "OldFirmware",
147+
"current_fw_version": "1.0",
148+
"fw_state": "IDLE"
149+
}
150+
self.client.firmware_received = True
151+
self.client.firmware_info[FW_TITLE_ATTR] = "dummy_firmware.bin"
152+
self.client.firmware_info[FW_VERSION_ATTR] = "dummy_version"
153+
154+
def test_firmware_request_info(self):
155+
self.client._publish_data.reset_mock()
156+
self.client._TBDeviceMqttClient__request_firmware_info()
157+
self.client._publish_data.assert_called()
158+
159+
def test_firmware_chunk_reception_detailed(self):
160+
self.client._publish_data.reset_mock()
161+
self.client._TBDeviceMqttClient__get_firmware()
162+
self.client._publish_data.assert_called()
163+
164+
@patch.object(TBDeviceMqttClient, 'send_telemetry')
165+
def test_process_firmware_telemetry_calls(self, mock_send_telemetry):
166+
self.client.firmware_data = b"some_firmware_data"
167+
self.client.firmware_info = {
168+
FW_TITLE_ATTR: "dummy_firmware.bin",
169+
FW_VERSION_ATTR: "2.0",
170+
"fw_checksum": "valid_checksum",
171+
"fw_checksum_algorithm": "SHA256"
172+
}
173+
174+
self.client._TBDeviceMqttClient__process_firmware()
175+
176+
self.assertEqual(
177+
mock_send_telemetry.call_count,
178+
2,
179+
"Two calls to send_telemetry are expected in the current firmware implementation"
180+
)
181+
182+
expected_calls = [
183+
call({"current_fw_title": "Initial", "current_fw_version": "v0", "fw_state": "FAILED"}),
184+
call({"current_fw_title": "Initial", "current_fw_version": "v0", "fw_state": "FAILED"})
185+
]
186+
mock_send_telemetry.assert_has_calls(expected_calls, any_order=False)
187+
188+
189+
class TestFirmwareChunkReception(unittest.TestCase):
190+
def setUp(self):
191+
self.client = TBDeviceMqttClient(host="localhost", port=1883)
192+
self.client._TBDeviceMqttClient__firmware_request_id = 1
193+
self.client._TBDeviceMqttClient__current_chunk = 0
194+
195+
@patch.object(TBDeviceMqttClient, '_publish_data')
196+
def test_firmware_chunk_reception(self, mock_publish_data):
197+
self.client._TBDeviceMqttClient__chunk_size = 128
198+
self.client.firmware_info = {
199+
"fw_size": 300,
200+
"fw_title": "SomeFirmware",
201+
"fw_checksum": "12345",
202+
"fw_checksum_algorithm": "SHA256"
203+
}
204+
self.client._TBDeviceMqttClient__get_firmware()
205+
expected_calls = [
206+
call(b'128', 'v2/fw/request/1/chunk/0', 1)
207+
]
208+
self.assertEqual(mock_publish_data.call_count, 1, "Only one chunk request is expected")
209+
mock_publish_data.assert_has_calls(expected_calls, any_order=False)
210+
211+
self.assertEqual(self.client._TBDeviceMqttClient__current_chunk, 0,
212+
"The current_chunk should not change if the method only requests chunks.")
213+
214+
215+
class TestFirmwareUpdate(unittest.TestCase):
216+
def setUp(self):
217+
self.client = TBDeviceMqttClient(host="localhost", port=1883)
218+
self.client._TBDeviceMqttClient__process_firmware = MagicMock()
219+
self.client._TBDeviceMqttClient__get_firmware = MagicMock()
220+
221+
self.client._TBDeviceMqttClient__firmware_request_id = 1
222+
self.client._TBDeviceMqttClient__current_chunk = 0
223+
self.client._TBDeviceMqttClient__target_firmware_length = 10
224+
225+
self.client.firmware_data = b''
226+
227+
def test_incomplete_firmware_chunk(self):
228+
chunk_data = b'abcde'
229+
message = MagicMock()
230+
message.topic = "v2/fw/response/1/chunk/0"
231+
message.payload = chunk_data
232+
233+
self.client._on_message(None, None, message)
234+
self.assertEqual(self.client.firmware_data, b'abcde')
235+
self.assertEqual(self.client._TBDeviceMqttClient__current_chunk, 1)
236+
self.client._TBDeviceMqttClient__process_firmware.assert_not_called()
237+
self.client._TBDeviceMqttClient__get_firmware.assert_called_once()
238+
239+
def test_complete_firmware_chunk(self):
240+
self.client.firmware_data = b'abcde'
241+
self.client._TBDeviceMqttClient__current_chunk = 1
242+
243+
chunk_data = b'12345'
244+
message = MagicMock()
245+
message.topic = "v2/fw/response/1/chunk/1"
246+
message.payload = chunk_data
247+
248+
self.client._on_message(None, None, message)
249+
250+
self.assertEqual(self.client.firmware_data, b'abcde12345')
251+
self.assertEqual(self.client._TBDeviceMqttClient__current_chunk, 2)
252+
253+
self.client._TBDeviceMqttClient__process_firmware.assert_called_once()
254+
self.client._TBDeviceMqttClient__get_firmware.assert_not_called()
255+
256+
257+
if __name__ == '__main__':
258+
unittest.main()

0 commit comments

Comments
 (0)