matterbridge-roborock-vacuum-plugin-regions 1.1.1-jb.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.github/workflows/build.yml +56 -0
  2. package/.github/workflows/coverage.yml +59 -0
  3. package/.github/workflows/publish.yml +37 -0
  4. package/.tarignore +5 -0
  5. package/CHANGELOG.md +62 -0
  6. package/LICENSE +202 -0
  7. package/README.md +135 -0
  8. package/README_CLEANMODE.md +29 -0
  9. package/README_DEV.md +75 -0
  10. package/README_REPORT_ISSUE.md +34 -0
  11. package/README_SUPPORTED.md +67 -0
  12. package/dist/behaviorFactory.js +26 -0
  13. package/dist/behaviors/BehaviorDeviceGeneric.js +22 -0
  14. package/dist/behaviors/roborock.vacuum/default/default.js +183 -0
  15. package/dist/behaviors/roborock.vacuum/default/initalData.js +143 -0
  16. package/dist/behaviors/roborock.vacuum/default/runtimes.js +21 -0
  17. package/dist/behaviors/roborock.vacuum/smart/initalData.js +18 -0
  18. package/dist/behaviors/roborock.vacuum/smart/runtimes.js +11 -0
  19. package/dist/behaviors/roborock.vacuum/smart/smart.js +119 -0
  20. package/dist/clientManager.js +17 -0
  21. package/dist/helper.js +76 -0
  22. package/dist/index.js +4 -0
  23. package/dist/initialData/getBatteryStatus.js +24 -0
  24. package/dist/initialData/getOperationalStates.js +82 -0
  25. package/dist/initialData/getSupportedAreas.js +120 -0
  26. package/dist/initialData/getSupportedCleanModes.js +17 -0
  27. package/dist/initialData/getSupportedRunModes.js +11 -0
  28. package/dist/initialData/getSupportedScenes.js +26 -0
  29. package/dist/initialData/index.js +6 -0
  30. package/dist/model/CloudMessageModel.js +1 -0
  31. package/dist/model/DockingStationStatus.js +26 -0
  32. package/dist/model/ExperimentalFeatureSetting.js +30 -0
  33. package/dist/model/RoomMap.js +19 -0
  34. package/dist/model/roomIndexMap.js +21 -0
  35. package/dist/notifyMessageTypes.js +9 -0
  36. package/dist/platform.js +312 -0
  37. package/dist/platformRunner.js +90 -0
  38. package/dist/roborockCommunication/RESTAPI/roborockAuthenticateApi.js +213 -0
  39. package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +95 -0
  40. package/dist/roborockCommunication/Zenum/additionalPropCode.js +4 -0
  41. package/dist/roborockCommunication/Zenum/authenticateResponseCode.js +7 -0
  42. package/dist/roborockCommunication/Zenum/dockType.js +4 -0
  43. package/dist/roborockCommunication/Zenum/operationStatusCode.js +44 -0
  44. package/dist/roborockCommunication/Zenum/vacuumAndDockErrorCode.js +68 -0
  45. package/dist/roborockCommunication/Zmodel/apiResponse.js +1 -0
  46. package/dist/roborockCommunication/Zmodel/authenticateFlowState.js +1 -0
  47. package/dist/roborockCommunication/Zmodel/authenticateResponse.js +1 -0
  48. package/dist/roborockCommunication/Zmodel/baseURL.js +1 -0
  49. package/dist/roborockCommunication/Zmodel/batteryMessage.js +1 -0
  50. package/dist/roborockCommunication/Zmodel/device.js +1 -0
  51. package/dist/roborockCommunication/Zmodel/deviceModel.js +28 -0
  52. package/dist/roborockCommunication/Zmodel/deviceSchema.js +1 -0
  53. package/dist/roborockCommunication/Zmodel/deviceStatus.js +22 -0
  54. package/dist/roborockCommunication/Zmodel/dockInfo.js +6 -0
  55. package/dist/roborockCommunication/Zmodel/home.js +1 -0
  56. package/dist/roborockCommunication/Zmodel/homeInfo.js +1 -0
  57. package/dist/roborockCommunication/Zmodel/map.js +1 -0
  58. package/dist/roborockCommunication/Zmodel/mapInfo.js +29 -0
  59. package/dist/roborockCommunication/Zmodel/messageResult.js +7 -0
  60. package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
  61. package/dist/roborockCommunication/Zmodel/networkInfo.js +1 -0
  62. package/dist/roborockCommunication/Zmodel/product.js +1 -0
  63. package/dist/roborockCommunication/Zmodel/room.js +1 -0
  64. package/dist/roborockCommunication/Zmodel/roomInfo.js +22 -0
  65. package/dist/roborockCommunication/Zmodel/scene.js +16 -0
  66. package/dist/roborockCommunication/Zmodel/userData.js +1 -0
  67. package/dist/roborockCommunication/Zmodel/vacuumError.js +27 -0
  68. package/dist/roborockCommunication/broadcast/abstractClient.js +55 -0
  69. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +174 -0
  70. package/dist/roborockCommunication/broadcast/client/LocalNetworkUDPClient.js +129 -0
  71. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +139 -0
  72. package/dist/roborockCommunication/broadcast/client.js +1 -0
  73. package/dist/roborockCommunication/broadcast/clientRouter.js +82 -0
  74. package/dist/roborockCommunication/broadcast/listener/abstractConnectionListener.js +1 -0
  75. package/dist/roborockCommunication/broadcast/listener/abstractMessageHandler.js +1 -0
  76. package/dist/roborockCommunication/broadcast/listener/abstractMessageListener.js +1 -0
  77. package/dist/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.js +26 -0
  78. package/dist/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.js +11 -0
  79. package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +43 -0
  80. package/dist/roborockCommunication/broadcast/listener/implementation/generalSyncMessageListener.js +28 -0
  81. package/dist/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.js +27 -0
  82. package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +33 -0
  83. package/dist/roborockCommunication/broadcast/listener/index.js +1 -0
  84. package/dist/roborockCommunication/broadcast/messageProcessor.js +148 -0
  85. package/dist/roborockCommunication/broadcast/model/contentMessage.js +1 -0
  86. package/dist/roborockCommunication/broadcast/model/dps.js +1 -0
  87. package/dist/roborockCommunication/broadcast/model/headerMessage.js +1 -0
  88. package/dist/roborockCommunication/broadcast/model/messageContext.js +37 -0
  89. package/dist/roborockCommunication/broadcast/model/protocol.js +28 -0
  90. package/dist/roborockCommunication/broadcast/model/requestMessage.js +38 -0
  91. package/dist/roborockCommunication/broadcast/model/responseMessage.js +14 -0
  92. package/dist/roborockCommunication/helper/chunkBuffer.js +17 -0
  93. package/dist/roborockCommunication/helper/cryptoHelper.js +23 -0
  94. package/dist/roborockCommunication/helper/messageDeserializer.js +98 -0
  95. package/dist/roborockCommunication/helper/messageSerializer.js +84 -0
  96. package/dist/roborockCommunication/helper/nameDecoder.js +66 -0
  97. package/dist/roborockCommunication/helper/sequence.js +16 -0
  98. package/dist/roborockCommunication/index.js +13 -0
  99. package/dist/roborockService.js +494 -0
  100. package/dist/runtimes/handleCloudMessage.js +110 -0
  101. package/dist/runtimes/handleHomeDataMessage.js +57 -0
  102. package/dist/runtimes/handleLocalMessage.js +169 -0
  103. package/dist/rvc.js +51 -0
  104. package/dist/settings.js +1 -0
  105. package/dist/share/function.js +93 -0
  106. package/dist/share/runtimeHelper.js +17 -0
  107. package/dist/tests/testData/mockData.js +359 -0
  108. package/eslint.config.js +80 -0
  109. package/jest.config.js +22 -0
  110. package/jest.setup.js +2 -0
  111. package/logo.png +0 -0
  112. package/matterbridge-roborock-vacuum-plugin.config.json +46 -0
  113. package/matterbridge-roborock-vacuum-plugin.schema.json +293 -0
  114. package/misc/status.md +119 -0
  115. package/package.json +111 -0
  116. package/prettier.config.js +49 -0
  117. package/screenshot/IMG_1.PNG +0 -0
  118. package/screenshot/IMG_2.PNG +0 -0
  119. package/screenshot/IMG_3.PNG +0 -0
  120. package/screenshot/IMG_4.PNG +0 -0
  121. package/screenshot/IMG_5.PNG +0 -0
  122. package/screenshot/IMG_6.PNG +0 -0
  123. package/screenshot/IMG_7.PNG +0 -0
  124. package/src/behaviorFactory.ts +41 -0
  125. package/src/behaviors/BehaviorDeviceGeneric.ts +31 -0
  126. package/src/behaviors/roborock.vacuum/default/default.ts +238 -0
  127. package/src/behaviors/roborock.vacuum/default/initalData.ts +152 -0
  128. package/src/behaviors/roborock.vacuum/default/runtimes.ts +23 -0
  129. package/src/behaviors/roborock.vacuum/smart/initalData.ts +20 -0
  130. package/src/behaviors/roborock.vacuum/smart/runtimes.ts +15 -0
  131. package/src/behaviors/roborock.vacuum/smart/smart.ts +159 -0
  132. package/src/clientManager.ts +23 -0
  133. package/src/helper.ts +97 -0
  134. package/src/index.ts +16 -0
  135. package/src/initialData/getBatteryStatus.ts +26 -0
  136. package/src/initialData/getOperationalStates.ts +94 -0
  137. package/src/initialData/getSupportedAreas.ts +162 -0
  138. package/src/initialData/getSupportedCleanModes.ts +22 -0
  139. package/src/initialData/getSupportedRunModes.ts +14 -0
  140. package/src/initialData/getSupportedScenes.ts +32 -0
  141. package/src/initialData/index.ts +6 -0
  142. package/src/model/CloudMessageModel.ts +11 -0
  143. package/src/model/DockingStationStatus.ts +41 -0
  144. package/src/model/ExperimentalFeatureSetting.ts +77 -0
  145. package/src/model/RoomMap.ts +38 -0
  146. package/src/model/roomIndexMap.ts +26 -0
  147. package/src/notifyMessageTypes.ts +8 -0
  148. package/src/platform.ts +424 -0
  149. package/src/platformRunner.ts +103 -0
  150. package/src/roborockCommunication/RESTAPI/roborockAuthenticateApi.ts +302 -0
  151. package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +107 -0
  152. package/src/roborockCommunication/Zenum/additionalPropCode.ts +3 -0
  153. package/src/roborockCommunication/Zenum/authenticateResponseCode.ts +6 -0
  154. package/src/roborockCommunication/Zenum/dockType.ts +3 -0
  155. package/src/roborockCommunication/Zenum/operationStatusCode.ts +43 -0
  156. package/src/roborockCommunication/Zenum/vacuumAndDockErrorCode.ts +68 -0
  157. package/src/roborockCommunication/Zmodel/apiResponse.ts +3 -0
  158. package/src/roborockCommunication/Zmodel/authenticateFlowState.ts +6 -0
  159. package/src/roborockCommunication/Zmodel/authenticateResponse.ts +5 -0
  160. package/src/roborockCommunication/Zmodel/baseURL.ts +5 -0
  161. package/src/roborockCommunication/Zmodel/batteryMessage.ts +16 -0
  162. package/src/roborockCommunication/Zmodel/device.ts +50 -0
  163. package/src/roborockCommunication/Zmodel/deviceModel.ts +27 -0
  164. package/src/roborockCommunication/Zmodel/deviceSchema.ts +8 -0
  165. package/src/roborockCommunication/Zmodel/deviceStatus.ts +30 -0
  166. package/src/roborockCommunication/Zmodel/dockInfo.ts +9 -0
  167. package/src/roborockCommunication/Zmodel/home.ts +13 -0
  168. package/src/roborockCommunication/Zmodel/homeInfo.ts +5 -0
  169. package/src/roborockCommunication/Zmodel/map.ts +20 -0
  170. package/src/roborockCommunication/Zmodel/mapInfo.ts +54 -0
  171. package/src/roborockCommunication/Zmodel/messageResult.ts +75 -0
  172. package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
  173. package/src/roborockCommunication/Zmodel/networkInfo.ts +7 -0
  174. package/src/roborockCommunication/Zmodel/product.ts +9 -0
  175. package/src/roborockCommunication/Zmodel/room.ts +4 -0
  176. package/src/roborockCommunication/Zmodel/roomInfo.ts +30 -0
  177. package/src/roborockCommunication/Zmodel/scene.ts +44 -0
  178. package/src/roborockCommunication/Zmodel/userData.ts +26 -0
  179. package/src/roborockCommunication/Zmodel/vacuumError.ts +35 -0
  180. package/src/roborockCommunication/broadcast/abstractClient.ts +80 -0
  181. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +218 -0
  182. package/src/roborockCommunication/broadcast/client/LocalNetworkUDPClient.ts +157 -0
  183. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +174 -0
  184. package/src/roborockCommunication/broadcast/client.ts +19 -0
  185. package/src/roborockCommunication/broadcast/clientRouter.ts +104 -0
  186. package/src/roborockCommunication/broadcast/listener/abstractConnectionListener.ts +6 -0
  187. package/src/roborockCommunication/broadcast/listener/abstractMessageHandler.ts +11 -0
  188. package/src/roborockCommunication/broadcast/listener/abstractMessageListener.ts +5 -0
  189. package/src/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.ts +33 -0
  190. package/src/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.ts +16 -0
  191. package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +57 -0
  192. package/src/roborockCommunication/broadcast/listener/implementation/generalSyncMessageListener.ts +38 -0
  193. package/src/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.ts +37 -0
  194. package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +50 -0
  195. package/src/roborockCommunication/broadcast/listener/index.ts +3 -0
  196. package/src/roborockCommunication/broadcast/messageProcessor.ts +184 -0
  197. package/src/roborockCommunication/broadcast/model/contentMessage.ts +5 -0
  198. package/src/roborockCommunication/broadcast/model/dps.ts +19 -0
  199. package/src/roborockCommunication/broadcast/model/headerMessage.ts +7 -0
  200. package/src/roborockCommunication/broadcast/model/messageContext.ts +53 -0
  201. package/src/roborockCommunication/broadcast/model/protocol.ts +28 -0
  202. package/src/roborockCommunication/broadcast/model/requestMessage.ts +51 -0
  203. package/src/roborockCommunication/broadcast/model/responseMessage.ts +19 -0
  204. package/src/roborockCommunication/helper/chunkBuffer.ts +18 -0
  205. package/src/roborockCommunication/helper/cryptoHelper.ts +30 -0
  206. package/src/roborockCommunication/helper/messageDeserializer.ts +119 -0
  207. package/src/roborockCommunication/helper/messageSerializer.ts +101 -0
  208. package/src/roborockCommunication/helper/nameDecoder.ts +78 -0
  209. package/src/roborockCommunication/helper/sequence.ts +18 -0
  210. package/src/roborockCommunication/index.ts +25 -0
  211. package/src/roborockService.ts +657 -0
  212. package/src/runtimes/handleCloudMessage.ts +134 -0
  213. package/src/runtimes/handleHomeDataMessage.ts +67 -0
  214. package/src/runtimes/handleLocalMessage.ts +209 -0
  215. package/src/rvc.ts +97 -0
  216. package/src/settings.ts +1 -0
  217. package/src/share/function.ts +103 -0
  218. package/src/share/runtimeHelper.ts +23 -0
  219. package/src/tests/behaviors/roborock.vacuum/default/default.test.ts +134 -0
  220. package/src/tests/behaviors/roborock.vacuum/smart/runtimes.test.ts +64 -0
  221. package/src/tests/behaviors/roborock.vacuum/smart/smart.test.ts +215 -0
  222. package/src/tests/helper.test.ts +162 -0
  223. package/src/tests/initialData/getSupportedAreas.test.ts +181 -0
  224. package/src/tests/model/DockingStationStatus.test.ts +39 -0
  225. package/src/tests/platformRunner.test.ts +188 -0
  226. package/src/tests/platformRunner2.test.ts +228 -0
  227. package/src/tests/platformRunner3.test.ts +46 -0
  228. package/src/tests/roborockCommunication/RESTAPI/roborockAuthenticateApi.test.ts +144 -0
  229. package/src/tests/roborockCommunication/RESTAPI/roborockIoTApi.test.ts +106 -0
  230. package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +189 -0
  231. package/src/tests/roborockCommunication/broadcast/client/MQTTClient.test.ts +208 -0
  232. package/src/tests/roborockCommunication/broadcast/clientRouter.test.ts +168 -0
  233. package/src/tests/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.test.ts +59 -0
  234. package/src/tests/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.test.ts +46 -0
  235. package/src/tests/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.test.ts +71 -0
  236. package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +86 -0
  237. package/src/tests/roborockCommunication/broadcast/messageProcessor.test.ts +126 -0
  238. package/src/tests/roborockService.setSelectedAreas.test.ts +61 -0
  239. package/src/tests/roborockService.test.ts +517 -0
  240. package/src/tests/roborockService2.test.ts +69 -0
  241. package/src/tests/roborockService3.test.ts +133 -0
  242. package/src/tests/roborockService4.test.ts +76 -0
  243. package/src/tests/roborockService5.test.ts +79 -0
  244. package/src/tests/runtimes/handleCloudMessage.test.ts +200 -0
  245. package/src/tests/runtimes/handleHomeDataMessage.test.ts +54 -0
  246. package/src/tests/runtimes/handleLocalMessage.test.ts +227 -0
  247. package/src/tests/testData/mockData.ts +370 -0
  248. package/src/tests/testData/mockHomeData-a187.json +286 -0
  249. package/tsconfig.jest.json +21 -0
  250. package/tsconfig.json +37 -0
  251. package/tsconfig.production.json +19 -0
  252. package/tslint.json +9 -0
  253. package/web-for-testing/README.md +47 -0
  254. package/web-for-testing/nodemon.json +7 -0
  255. package/web-for-testing/package-lock.json +6600 -0
  256. package/web-for-testing/package.json +36 -0
  257. package/web-for-testing/src/app.ts +194 -0
  258. package/web-for-testing/tsconfig-ext.json +19 -0
  259. package/web-for-testing/tsconfig.json +23 -0
  260. package/web-for-testing/views/index.ejs +172 -0
  261. package/web-for-testing/watch.mjs +93 -0
@@ -0,0 +1,144 @@
1
+ import { RoborockAuthenticateApi } from '../../../roborockCommunication/RESTAPI/roborockAuthenticateApi';
2
+
3
+ describe('RoborockAuthenticateApi', () => {
4
+ let mockLogger: any;
5
+ let mockAxiosFactory: any;
6
+ let mockAxiosInstance: any;
7
+ let api: any;
8
+
9
+ beforeEach(() => {
10
+ mockLogger = { info: jest.fn(), error: jest.fn() };
11
+ mockAxiosInstance = {
12
+ post: jest.fn(),
13
+ get: jest.fn(),
14
+ interceptors: {
15
+ request: {
16
+ use: jest.fn(),
17
+ },
18
+ response: {
19
+ use: jest.fn(),
20
+ },
21
+ },
22
+ };
23
+ mockAxiosFactory = {
24
+ create: jest.fn(() => mockAxiosInstance),
25
+ };
26
+ api = new RoborockAuthenticateApi(mockLogger, mockAxiosFactory);
27
+ });
28
+
29
+ it('should initialize deviceId, logger, axiosFactory', () => {
30
+ expect(api.logger).toBe(mockLogger);
31
+ expect(api.axiosFactory).toBe(mockAxiosFactory);
32
+ expect(typeof api['deviceId']).toBe('string');
33
+ });
34
+
35
+ it('loginWithUserData should call loginWithAuthToken and return userData', async () => {
36
+ const spy = jest.spyOn(api as any, 'loginWithAuthToken');
37
+ const userData = { token: 'abc', other: 'data' };
38
+ const result = await api.loginWithUserData('user', userData);
39
+ expect(spy).toHaveBeenCalledWith('user', 'abc');
40
+ expect(result).toBe(userData);
41
+ });
42
+
43
+ it('loginWithPassword should call auth and return userData', async () => {
44
+ const userData = { token: 'tok', other: 'data' };
45
+ const response = { data: { data: userData } };
46
+ jest.spyOn(api as any, 'getAPIFor').mockResolvedValue(mockAxiosInstance);
47
+ mockAxiosInstance.post.mockResolvedValue(response);
48
+ jest.spyOn(api as any, 'auth').mockReturnValue(userData);
49
+
50
+ const result = await api.loginWithPassword('user', 'pass');
51
+ expect(result).toBe(userData);
52
+ expect(mockAxiosInstance.post).toHaveBeenCalled();
53
+ expect(api['getAPIFor']).toHaveBeenCalledWith('user');
54
+ expect(api['auth']).toHaveBeenCalledWith('user', response.data);
55
+ });
56
+
57
+ it('loginWithPassword should throw error if token missing', async () => {
58
+ const response = { data: { data: null, msg: 'fail', code: 401 } };
59
+ jest.spyOn(api as any, 'getAPIFor').mockResolvedValue(mockAxiosInstance);
60
+ mockAxiosInstance.post.mockResolvedValue(response);
61
+ jest.spyOn(api as any, 'auth').mockImplementation(() => {
62
+ throw new Error('Authentication failed: fail code: 401');
63
+ });
64
+
65
+ await expect(api.loginWithPassword('user', 'pass')).rejects.toThrow('Authentication failed: fail code: 401');
66
+ });
67
+
68
+ it('getHomeDetails should return undefined if username/authToken missing', async () => {
69
+ api['username'] = undefined;
70
+ api['authToken'] = undefined;
71
+ const result = await api.getHomeDetails();
72
+ expect(result).toBeUndefined();
73
+ });
74
+
75
+ it('getHomeDetails should throw error if response.data missing', async () => {
76
+ api['username'] = 'user';
77
+ api['authToken'] = 'tok';
78
+ jest.spyOn(api as any, 'getAPIFor').mockResolvedValue(mockAxiosInstance);
79
+ mockAxiosInstance.get.mockResolvedValue({ data: { data: null } });
80
+
81
+ await expect(api.getHomeDetails()).rejects.toThrow('Failed to retrieve the home details');
82
+ });
83
+
84
+ it('getHomeDetails should return HomeInfo if present', async () => {
85
+ api['username'] = 'user';
86
+ api['authToken'] = 'tok';
87
+ const homeInfo = { home: 'info' };
88
+ jest.spyOn(api as any, 'getAPIFor').mockResolvedValue(mockAxiosInstance);
89
+ mockAxiosInstance.get.mockResolvedValue({ data: { data: homeInfo } });
90
+
91
+ const result = await api.getHomeDetails();
92
+ expect(result).toBe(homeInfo);
93
+ });
94
+
95
+ it('getBaseUrl should throw error if response.data missing', async () => {
96
+ jest.spyOn(api as any, 'apiForUser').mockResolvedValue(mockAxiosInstance);
97
+ mockAxiosInstance.post.mockResolvedValue({ data: { data: null, msg: 'fail' } });
98
+
99
+ await expect(api['getBaseUrl']('user')).rejects.toThrow('Failed to retrieve base URL: fail');
100
+ });
101
+
102
+ it('getBaseUrl should return url if present', async () => {
103
+ jest.spyOn(api as any, 'apiForUser').mockResolvedValue(mockAxiosInstance);
104
+ mockAxiosInstance.post.mockResolvedValue({ data: { data: { url: 'http://base.url' } } });
105
+
106
+ const result = await api['getBaseUrl']('user');
107
+ expect(result).toBe('http://base.url');
108
+ });
109
+
110
+ it('apiForUser should create AxiosInstance with correct headers', async () => {
111
+ const username = 'user';
112
+ const baseUrl = 'http://base.url';
113
+ const spy = jest.spyOn(mockAxiosFactory, 'create');
114
+ await api['apiForUser'](username, baseUrl);
115
+ expect(spy).toHaveBeenCalledWith(
116
+ expect.objectContaining({
117
+ baseURL: baseUrl,
118
+ headers: expect.objectContaining({
119
+ header_clientid: expect.any(String),
120
+ Authorization: undefined,
121
+ }),
122
+ }),
123
+ );
124
+ });
125
+
126
+ it('auth should call loginWithAuthToken and return userData', () => {
127
+ const spy = jest.spyOn(api as any, 'loginWithAuthToken');
128
+ const response = { data: { token: 'tok', other: 'data' }, msg: '', code: 0 };
129
+ const result = api['auth']('user', response);
130
+ expect(spy).toHaveBeenCalledWith('user', 'tok');
131
+ expect(result).toBe(response.data);
132
+ });
133
+
134
+ it('auth should throw error if token missing', () => {
135
+ const response = { data: null, msg: 'fail', code: 401 };
136
+ expect(() => api['auth']('user', response)).toThrow('Authentication failed: fail code: 401');
137
+ });
138
+
139
+ it('loginWithAuthToken should set username and authToken', () => {
140
+ api['loginWithAuthToken']('user', 'tok');
141
+ expect(api['username']).toBe('user');
142
+ expect(api['authToken']).toBe('tok');
143
+ });
144
+ });
@@ -0,0 +1,106 @@
1
+ import axios from 'axios';
2
+ import { RoborockIoTApi } from '../../../roborockCommunication/RESTAPI/roborockIoTApi';
3
+
4
+ describe('RoborockIoTApi', () => {
5
+ let mockLogger: any;
6
+ let mockAxiosInstance: any;
7
+ let mockUserData: any;
8
+ let api: RoborockIoTApi;
9
+
10
+ beforeEach(() => {
11
+ mockLogger = { error: jest.fn() };
12
+ mockAxiosInstance = {
13
+ get: jest.fn(),
14
+ post: jest.fn(),
15
+ interceptors: { request: { use: jest.fn() } },
16
+ getUri: jest.fn((config) => config.url),
17
+ };
18
+ mockUserData = {
19
+ rriot: {
20
+ r: { a: 'http://base.url' },
21
+ u: 'uid',
22
+ s: 'sid',
23
+ h: 'hkey',
24
+ },
25
+ };
26
+ jest.spyOn(axios, 'create').mockReturnValue(mockAxiosInstance);
27
+ api = new RoborockIoTApi(mockUserData, mockLogger);
28
+ });
29
+
30
+ afterEach(() => {
31
+ jest.restoreAllMocks();
32
+ });
33
+
34
+ it('should initialize logger and api', () => {
35
+ expect(api.logger).toBe(mockLogger);
36
+ expect(api['api']).toBe(mockAxiosInstance);
37
+ });
38
+
39
+ it('getHome should return home if result exists', async () => {
40
+ const home = { id: 1 };
41
+ mockAxiosInstance.get.mockResolvedValue({ data: { result: home } });
42
+ const result = await api.getHome(1);
43
+ expect(result).toBe(home);
44
+ });
45
+
46
+ it('getHome should log error and return undefined if result missing', async () => {
47
+ mockAxiosInstance.get.mockResolvedValue({ data: {} });
48
+ const result = await api.getHome(1);
49
+ expect(result).toBeUndefined();
50
+ expect(mockLogger.error).toHaveBeenCalledWith('Failed to retrieve the home data');
51
+ });
52
+
53
+ it('getHomev2 should return home if result exists', async () => {
54
+ const home = { id: 2 };
55
+ mockAxiosInstance.get.mockResolvedValue({ data: { result: home } });
56
+ const result = await api.getHomev2(2);
57
+ expect(result).toBe(home);
58
+ });
59
+
60
+ it('getHomev3 should return home if result exists', async () => {
61
+ const home = { id: 3 };
62
+ mockAxiosInstance.get.mockResolvedValue({ data: { result: home } });
63
+ const result = await api.getHomev3(3);
64
+ expect(result).toBe(home);
65
+ });
66
+
67
+ it('getScenes should return scenes if result exists', async () => {
68
+ const scenes = [{ id: 1 }, { id: 2 }];
69
+ mockAxiosInstance.get.mockResolvedValue({ data: { result: scenes } });
70
+ const result = await api.getScenes(1);
71
+ expect(result).toBe(scenes);
72
+ });
73
+
74
+ it('getScenes should log error and return undefined if result missing', async () => {
75
+ mockAxiosInstance.get.mockResolvedValue({ data: {} });
76
+ const result = await api.getScenes(1);
77
+ expect(result).toBeUndefined();
78
+ expect(mockLogger.error).toHaveBeenCalledWith('Failed to retrieve scene');
79
+ });
80
+
81
+ it('startScene should return result if present', async () => {
82
+ mockAxiosInstance.post.mockResolvedValue({ data: { result: 'started' } });
83
+ const result = await api.startScene(1);
84
+ expect(result).toBe('started');
85
+ });
86
+
87
+ it('startScene should log error and return undefined if result missing', async () => {
88
+ mockAxiosInstance.post.mockResolvedValue({ data: {} });
89
+ const result = await api.startScene(1);
90
+ expect(result).toBeUndefined();
91
+ expect(mockLogger.error).toHaveBeenCalledWith('Failed to execute scene');
92
+ });
93
+
94
+ it('getCustom should return result if present', async () => {
95
+ mockAxiosInstance.get.mockResolvedValue({ data: { result: 'custom' } });
96
+ const result = await api.getCustom('/custom/url');
97
+ expect(result).toBe('custom');
98
+ });
99
+
100
+ it('getCustom should log error and return undefined if result missing', async () => {
101
+ mockAxiosInstance.get.mockResolvedValue({ data: {} });
102
+ const result = await api.getCustom('/custom/url');
103
+ expect(result).toBeUndefined();
104
+ expect(mockLogger.error).toHaveBeenCalledWith('Failed to execute scene');
105
+ });
106
+ });
@@ -0,0 +1,189 @@
1
+ import { LocalNetworkClient } from '../../../../roborockCommunication/broadcast/client/LocalNetworkClient';
2
+ import { Protocol } from '../../../../roborockCommunication/broadcast/model/protocol';
3
+
4
+ const Sket = jest.fn();
5
+
6
+ jest.mock('node:net', () => {
7
+ const actual = jest.requireActual('node:net');
8
+ return {
9
+ ...actual,
10
+ Socket: Sket, // We'll set the implementation in beforeEach
11
+ };
12
+ });
13
+
14
+ describe('LocalNetworkClient', () => {
15
+ let client: LocalNetworkClient;
16
+ let mockLogger: any;
17
+ let mockContext: any;
18
+ let mockSocket: any;
19
+ const duid = 'duid1';
20
+ const ip = '127.0.0.1';
21
+
22
+ beforeEach(() => {
23
+ mockLogger = {
24
+ debug: jest.fn(),
25
+ error: jest.fn(),
26
+ notice: jest.fn(),
27
+ info: jest.fn(),
28
+ };
29
+ mockContext = {};
30
+ mockSocket = {
31
+ on: jest.fn(),
32
+ connect: jest.fn(),
33
+ destroy: jest.fn(),
34
+ write: jest.fn(),
35
+ address: jest.fn().mockReturnValue({ address: '127.0.0.1', port: 58867 }),
36
+ };
37
+ // Set the Socket mock implementation
38
+ Sket.mockImplementation(() => mockSocket);
39
+
40
+ client = new LocalNetworkClient(mockLogger, mockContext, duid, ip);
41
+ // Patch serializer/deserializer for send/onMessage
42
+ (client as any).serializer = { serialize: jest.fn().mockReturnValue({ buffer: Buffer.from([1, 2, 3]), messageId: 123 }) };
43
+ (client as any).deserializer = { deserialize: jest.fn().mockReturnValue('deserialized') };
44
+ (client as any).messageListeners = { onMessage: jest.fn() };
45
+ (client as any).connectionListeners = {
46
+ onConnected: jest.fn(),
47
+ onDisconnected: jest.fn(),
48
+ onError: jest.fn(),
49
+ };
50
+ });
51
+
52
+ afterEach(() => {
53
+ jest.clearAllMocks();
54
+ jest.useRealTimers();
55
+ // Clear keepConnectionAliveInterval to prevent open handle leaks
56
+ if (client['keepConnectionAliveInterval']) {
57
+ clearInterval(client['keepConnectionAliveInterval']);
58
+ client['keepConnectionAliveInterval'] = undefined;
59
+ }
60
+ });
61
+
62
+ it('should initialize fields in constructor', () => {
63
+ expect(client.duid).toBe(duid);
64
+ expect(client.ip).toBe(ip);
65
+ expect((client as any).messageIdSeq).toBeDefined();
66
+ });
67
+
68
+ it('connect() should create socket, set handlers, and call connect', () => {
69
+ client.connect();
70
+ expect(client['socket']).not.toBeUndefined();
71
+ });
72
+
73
+ it('disconnect() should destroy socket and clear pingInterval', async () => {
74
+ client['socket'] = mockSocket;
75
+ client['pingInterval'] = setInterval(() => {
76
+ jest.fn();
77
+ }, 1000);
78
+ await client.disconnect();
79
+ expect(mockSocket.destroy).toHaveBeenCalled();
80
+ expect(client['socket']).toBeUndefined();
81
+ });
82
+
83
+ it('disconnect() should do nothing if socket is undefined', async () => {
84
+ client['socket'] = undefined;
85
+ await expect(client.disconnect()).resolves.toBeUndefined();
86
+ });
87
+
88
+ it('send() should log error if socket is not connected', async () => {
89
+ client['socket'] = undefined;
90
+ client['connected'] = false;
91
+ await client.send(duid, { toLocalRequest: jest.fn(), secure: false } as any);
92
+ expect(mockLogger.error).toHaveBeenCalled();
93
+ expect(mockSocket.write).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it('send() should serialize and write if connected', async () => {
97
+ client['socket'] = mockSocket;
98
+ client['connected'] = true;
99
+ const req = { toLocalRequest: jest.fn().mockReturnValue({}), secure: false };
100
+ await client.send(duid, req as any);
101
+ expect(client['serializer'].serialize).toHaveBeenCalled();
102
+ expect(mockSocket.write).toHaveBeenCalledWith(expect.any(Buffer));
103
+ expect(mockLogger.debug).toHaveBeenCalled();
104
+ });
105
+
106
+ it('onConnect() should set connected, log, send hello, set ping, call onConnected', async () => {
107
+ client['socket'] = mockSocket;
108
+ jest.useFakeTimers();
109
+ const sendHelloSpy = jest.spyOn(client as any, 'sendHelloMessage').mockResolvedValue(undefined);
110
+ await (client as any).onConnect();
111
+ expect(client['connected']).toBe(true);
112
+ expect(mockLogger.debug).toHaveBeenCalled();
113
+ expect(sendHelloSpy).toHaveBeenCalled();
114
+ expect(client['pingInterval']).toBeDefined();
115
+ expect(client['connectionListeners'].onConnected).toHaveBeenCalled();
116
+ });
117
+
118
+ it('onDisconnect() should log, set connected false, destroy socket, clear ping, call onDisconnected', async () => {
119
+ client['socket'] = mockSocket;
120
+ client['pingInterval'] = setInterval(() => {
121
+ jest.fn();
122
+ }, 1000);
123
+ await (client as any).onDisconnect();
124
+ expect(mockLogger.info).toHaveBeenCalled();
125
+ expect(client['connected']).toBe(false);
126
+ expect(mockSocket.destroy).toHaveBeenCalled();
127
+ expect(client['socket']).toBeUndefined();
128
+ expect(client['connectionListeners'].onDisconnected).toHaveBeenCalled();
129
+ });
130
+
131
+ it('onError() should log, set connected false, destroy socket, call onError', async () => {
132
+ client['socket'] = mockSocket;
133
+ await (client as any).onError(new Error('fail'));
134
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining(' [LocalNetworkClient]: Socket error for'));
135
+ expect(client['connectionListeners'].onError).toHaveBeenCalledWith('duid1', expect.stringContaining('fail'));
136
+ });
137
+
138
+ it('onMessage() should log debug if message is empty', async () => {
139
+ client['socket'] = mockSocket;
140
+ await (client as any).onMessage(Buffer.alloc(0));
141
+ expect(mockLogger.debug).toHaveBeenCalledWith('LocalNetworkClient received empty message from socket.');
142
+ });
143
+
144
+ it('onMessage() should process complete message and call onMessage', async () => {
145
+ client['socket'] = mockSocket;
146
+ // Compose a buffer with a single segment of length 3 (after 4 bytes)
147
+ const payload = Buffer.from([0, 0, 0, 3, 10, 20, 30]);
148
+ (client as any).isMessageComplete = jest.fn().mockReturnValue(true);
149
+ (client as any).buffer = {
150
+ append: jest.fn(),
151
+ get: jest.fn().mockReturnValue(payload),
152
+ reset: jest.fn(),
153
+ };
154
+ await (client as any).onMessage(payload);
155
+ expect(client['deserializer'].deserialize).toHaveBeenCalled();
156
+ expect(client['messageListeners'].onMessage).toHaveBeenCalledWith('deserialized');
157
+ });
158
+
159
+ it('isMessageComplete() should return true for complete buffer', () => {
160
+ // 4 bytes length + 3 bytes payload
161
+ const buf = Buffer.from([0, 0, 0, 3, 1, 2, 3]);
162
+ expect((client as any).isMessageComplete(buf)).toBe(true);
163
+ });
164
+
165
+ it('isMessageComplete() should return false for incomplete buffer', () => {
166
+ // 4 bytes length + only 2 bytes payload
167
+ const buf = Buffer.from([0, 0, 0, 3, 1, 2]);
168
+ expect((client as any).isMessageComplete(buf)).toBe(false);
169
+ });
170
+
171
+ it('wrapWithLengthData() should prepend length', () => {
172
+ const data = Buffer.from([1, 2, 3]);
173
+ const result = (client as any).wrapWithLengthData(data);
174
+ expect(result.readUInt32BE(0)).toBe(3);
175
+ expect(result.slice(4).equals(data)).toBe(true);
176
+ });
177
+
178
+ it('sendHelloMessage() should call send with hello_request', async () => {
179
+ const sendSpy = jest.spyOn(client, 'send').mockResolvedValue(undefined);
180
+ await (client as any).sendHelloMessage();
181
+ expect(sendSpy).toHaveBeenCalledWith(duid, expect.objectContaining({ protocol: Protocol.hello_request }));
182
+ });
183
+
184
+ it('sendPingRequest() should call send with ping_request', async () => {
185
+ const sendSpy = jest.spyOn(client, 'send').mockResolvedValue(undefined);
186
+ await (client as any).sendPingRequest();
187
+ expect(sendSpy).toHaveBeenCalledWith(duid, expect.objectContaining({ protocol: Protocol.ping_request }));
188
+ });
189
+ });
@@ -0,0 +1,208 @@
1
+ import { MQTTClient } from '../../../../roborockCommunication/broadcast/client/MQTTClient';
2
+
3
+ const mockConnect = jest.fn();
4
+ jest.mock('mqtt', () => {
5
+ const actual = jest.requireActual('mqtt');
6
+ return {
7
+ ...actual,
8
+ connect: mockConnect,
9
+ };
10
+ });
11
+
12
+ describe('MQTTClient', () => {
13
+ let logger: any;
14
+ let context: any;
15
+ let userdata: any;
16
+ let client: any;
17
+ let serializer: any;
18
+ let deserializer: any;
19
+ let connectionListeners: any;
20
+ let messageListeners: any;
21
+
22
+ beforeEach(() => {
23
+ logger = { error: jest.fn(), debug: jest.fn(), notice: jest.fn(), info: jest.fn() };
24
+ context = {};
25
+ userdata = {
26
+ rriot: {
27
+ u: 'user',
28
+ k: 'key',
29
+ s: 'secret',
30
+ r: { m: 'mqtt://broker' },
31
+ },
32
+ };
33
+ serializer = { serialize: jest.fn(() => ({ buffer: Buffer.from('msg') })) };
34
+ deserializer = { deserialize: jest.fn(() => 'deserialized') };
35
+ connectionListeners = {
36
+ onConnected: jest.fn().mockResolvedValue(undefined),
37
+ onDisconnected: jest.fn().mockResolvedValue(undefined),
38
+ onError: jest.fn().mockResolvedValue(undefined),
39
+ onReconnect: jest.fn().mockResolvedValue(undefined),
40
+ };
41
+ messageListeners = { onMessage: jest.fn().mockResolvedValue(undefined) };
42
+
43
+ // Mock mqtt client instance
44
+ client = {
45
+ on: jest.fn(),
46
+ end: jest.fn(),
47
+ publish: jest.fn(),
48
+ subscribe: jest.fn(),
49
+ };
50
+ mockConnect.mockReturnValue(client);
51
+ });
52
+
53
+ function createMQTTClient() {
54
+ // Pass dependencies via constructor if possible, or use a factory/mockable subclass for testing
55
+ class TestMQTTClient extends MQTTClient {
56
+ constructor() {
57
+ super(logger, context, userdata);
58
+ (this as any).serializer = serializer;
59
+ (this as any).deserializer = deserializer;
60
+ (this as any).connectionListeners = connectionListeners;
61
+ (this as any).messageListeners = messageListeners;
62
+ }
63
+ }
64
+ return new TestMQTTClient();
65
+ }
66
+
67
+ afterEach(() => {
68
+ jest.clearAllMocks();
69
+ });
70
+
71
+ it('should generate username and password in constructor', () => {
72
+ const mqttClient = createMQTTClient();
73
+ expect(mqttClient['mqttUsername']).toBe('c6d6afb9');
74
+ expect(mqttClient['mqttPassword']).toBe('938f62d6603bde9c');
75
+ });
76
+
77
+ it('should not connect if already connected', () => {
78
+ const mqttClient = createMQTTClient();
79
+ mqttClient['mqttClient'] = client;
80
+ mqttClient.connect();
81
+ expect(mockConnect).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('should disconnect if connected', async () => {
85
+ const mqttClient = createMQTTClient();
86
+ mqttClient['mqttClient'] = client;
87
+ mqttClient['connected'] = true;
88
+ await mqttClient.disconnect();
89
+ expect(client.end).toHaveBeenCalled();
90
+ });
91
+
92
+ it('should not disconnect if not connected', async () => {
93
+ const mqttClient = createMQTTClient();
94
+ mqttClient['mqttClient'] = undefined;
95
+ mqttClient['connected'] = false;
96
+ await mqttClient.disconnect();
97
+ expect(client.end).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('should log error if disconnect throws', async () => {
101
+ const mqttClient = createMQTTClient();
102
+ mqttClient['mqttClient'] = {
103
+ end: jest.fn(() => {
104
+ throw new Error('fail');
105
+ }),
106
+ } as any;
107
+ mqttClient['connected'] = true;
108
+ await mqttClient.disconnect();
109
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('MQTT client failed to disconnect'));
110
+ });
111
+
112
+ it('should publish message if connected', async () => {
113
+ const mqttClient = createMQTTClient();
114
+ mqttClient['mqttClient'] = client;
115
+ mqttClient['connected'] = true;
116
+ const request = { toMqttRequest: jest.fn(() => 'req') };
117
+ await mqttClient.send('duid1', request as any);
118
+ expect(serializer.serialize).toHaveBeenCalledWith('duid1', 'req');
119
+ expect(client.publish).toHaveBeenCalledWith('rr/m/i/user/c6d6afb9/duid1', Buffer.from('msg'), { qos: 1 });
120
+ });
121
+
122
+ it('should log error if send called when not connected', async () => {
123
+ const mqttClient = createMQTTClient();
124
+ mqttClient['mqttClient'] = undefined;
125
+ mqttClient['connected'] = false;
126
+ const request = { toMqttRequest: jest.fn() };
127
+ await mqttClient.send('duid1', request as any);
128
+ expect(logger.error).toHaveBeenCalled();
129
+ expect(client.publish).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('onConnect should set connected, call onConnected, and subscribeToQueue', async () => {
133
+ const mqttClient = createMQTTClient();
134
+ mqttClient['mqttClient'] = client;
135
+ mqttClient['subscribeToQueue'] = jest.fn();
136
+ await mqttClient['onConnect']({} as any);
137
+ expect(mqttClient['connected']).toBe(true);
138
+ expect(connectionListeners.onConnected).toHaveBeenCalled();
139
+ expect(mqttClient['subscribeToQueue']).toHaveBeenCalled();
140
+ });
141
+
142
+ it('subscribeToQueue should call client.subscribe with correct topic', () => {
143
+ const mqttClient = createMQTTClient();
144
+ mqttClient['mqttClient'] = client;
145
+ mqttClient['connected'] = true;
146
+ mqttClient['subscribeToQueue']();
147
+ expect(client.subscribe).toHaveBeenCalledWith('rr/m/o/user/c6d6afb9/#', expect.any(Function));
148
+ });
149
+
150
+ it('onSubscribe should log error and call onDisconnected if error', async () => {
151
+ const mqttClient = createMQTTClient();
152
+ await mqttClient['onSubscribe'](new Error('fail'), undefined);
153
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('failed to subscribe'));
154
+ expect(mqttClient['connected']).toBe(false);
155
+ expect(connectionListeners.onDisconnected).toHaveBeenCalled();
156
+ });
157
+
158
+ it('onSubscribe should do nothing if no error', async () => {
159
+ const mqttClient = createMQTTClient();
160
+ await mqttClient['onSubscribe'](null, undefined);
161
+ expect(logger.error).not.toHaveBeenCalled();
162
+ expect(connectionListeners.onDisconnected).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it('onDisconnect should call onDisconnected', async () => {
166
+ const mqttClient = createMQTTClient();
167
+ await mqttClient['onDisconnect']();
168
+ expect(connectionListeners.onDisconnected).toHaveBeenCalled();
169
+ });
170
+
171
+ it('onError should log error, set connected false, and call onError', async () => {
172
+ const mqttClient = createMQTTClient();
173
+ mqttClient['connected'] = true;
174
+ await mqttClient['onError'](new Error('fail'));
175
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('MQTT connection error'));
176
+ expect(mqttClient['connected']).toBe(false);
177
+ expect(connectionListeners.onError).toHaveBeenCalledWith('mqtt-c6d6afb9', expect.stringContaining('fail'));
178
+ });
179
+
180
+ it('onReconnect should call subscribeToQueue', () => {
181
+ const mqttClient = createMQTTClient();
182
+ mqttClient['subscribeToQueue'] = jest.fn();
183
+ mqttClient['onReconnect']();
184
+ expect(mqttClient['subscribeToQueue']).toHaveBeenCalled();
185
+ });
186
+
187
+ it('onMessage should call deserializer and messageListeners.onMessage if message', async () => {
188
+ const mqttClient = createMQTTClient();
189
+ await mqttClient['onMessage']('rr/m/o/user/c6d6afb9/duid1', Buffer.from('msg'));
190
+ expect(deserializer.deserialize).toHaveBeenCalledWith('duid1', Buffer.from('msg'));
191
+ expect(messageListeners.onMessage).toHaveBeenCalledWith('deserialized');
192
+ });
193
+
194
+ it('onMessage should log notice if message is falsy', async () => {
195
+ const mqttClient = createMQTTClient();
196
+ await mqttClient['onMessage']('topic', null as any);
197
+ expect(logger.notice).toHaveBeenCalledWith(expect.stringContaining('received empty message'));
198
+ });
199
+
200
+ it('onMessage should log error if deserializer throws', async () => {
201
+ const mqttClient = createMQTTClient();
202
+ deserializer.deserialize.mockImplementation(() => {
203
+ throw new Error('fail');
204
+ });
205
+ await mqttClient['onMessage']('topic/duid', Buffer.from('msg'));
206
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('unable to process message'));
207
+ });
208
+ });