]> git.basschouten.com Git - openhab-addons.git/blob
4bfde88740be8326f3e439d3d81a85d7104ec0f0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.boschshc.internal.devices.bridge;
14
15 import static org.junit.jupiter.api.Assertions.assertEquals;
16 import static org.junit.jupiter.api.Assertions.assertNotNull;
17 import static org.junit.jupiter.api.Assertions.assertSame;
18 import static org.junit.jupiter.api.Assertions.assertThrows;
19 import static org.junit.jupiter.api.Assertions.assertTrue;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.ArgumentMatchers.anyString;
22 import static org.mockito.ArgumentMatchers.argThat;
23 import static org.mockito.ArgumentMatchers.contains;
24 import static org.mockito.ArgumentMatchers.eq;
25 import static org.mockito.ArgumentMatchers.same;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.verify;
28 import static org.mockito.Mockito.verifyNoMoreInteractions;
29 import static org.mockito.Mockito.when;
30
31 import java.io.IOException;
32 import java.nio.file.Files;
33 import java.nio.file.Path;
34 import java.nio.file.Paths;
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.UUID;
40 import java.util.concurrent.ExecutionException;
41 import java.util.concurrent.TimeoutException;
42 import java.util.function.BiFunction;
43
44 import org.eclipse.jdt.annotation.NonNullByDefault;
45 import org.eclipse.jdt.annotation.Nullable;
46 import org.eclipse.jetty.client.api.ContentResponse;
47 import org.eclipse.jetty.client.api.Request;
48 import org.eclipse.jetty.http.HttpMethod;
49 import org.junit.jupiter.api.AfterEach;
50 import org.junit.jupiter.api.BeforeAll;
51 import org.junit.jupiter.api.BeforeEach;
52 import org.junit.jupiter.api.Test;
53 import org.mockito.ArgumentCaptor;
54 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
55 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
56 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
57 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
58 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
59 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
60 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
61 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
62 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest;
63 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
64 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
65 import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState;
66 import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.ArmActionRequest;
67 import org.openhab.binding.boschshc.internal.services.intrusion.dto.AlarmState;
68 import org.openhab.binding.boschshc.internal.services.intrusion.dto.ArmingState;
69 import org.openhab.binding.boschshc.internal.services.intrusion.dto.IntrusionDetectionSystemState;
70 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
71 import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
72 import org.openhab.core.config.core.Configuration;
73 import org.openhab.core.thing.Bridge;
74 import org.openhab.core.thing.Thing;
75 import org.openhab.core.thing.ThingStatus;
76 import org.openhab.core.thing.ThingStatusDetail;
77 import org.openhab.core.thing.binding.ThingHandlerCallback;
78 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
79
80 import com.google.gson.JsonElement;
81 import com.google.gson.JsonParser;
82
83 /**
84  * Unit tests for the {@link BridgeHandler}.
85  *
86  * @author David Pace - Initial contribution
87  *
88  */
89 @NonNullByDefault
90 class BridgeHandlerTest {
91
92     private @NonNullByDefault({}) BridgeHandler fixture;
93
94     private @NonNullByDefault({}) BoschHttpClient httpClient;
95
96     private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
97
98     /**
99      * A mocked bridge instance
100      */
101     private @NonNullByDefault({}) Bridge thing;
102
103     @BeforeAll
104     static void beforeAll() throws IOException {
105         Path mavenTargetFolder = Paths.get("target");
106         assertTrue(Files.exists(mavenTargetFolder), "Maven target folder does not exist.");
107         System.setProperty("openhab.userdata", mavenTargetFolder.toFile().getAbsolutePath());
108         Path etc = mavenTargetFolder.resolve("etc");
109         if (!Files.exists(etc)) {
110             Files.createDirectory(etc);
111         }
112     }
113
114     @BeforeEach
115     void beforeEach() throws Exception {
116         Bridge bridge = mock(Bridge.class);
117         fixture = new BridgeHandler(bridge);
118
119         thingHandlerCallback = mock(ThingHandlerCallback.class);
120         fixture.setCallback(thingHandlerCallback);
121
122         Configuration bridgeConfiguration = new Configuration();
123         Map<@Nullable String, @Nullable Object> properties = new HashMap<>();
124         properties.put("ipAddress", "localhost");
125         properties.put("password", "test");
126         bridgeConfiguration.setProperties(properties);
127
128         thing = mock(Bridge.class);
129         when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
130         // this calls initialize() as well
131         fixture.thingUpdated(thing);
132
133         // shut down the real HTTP client
134         if (fixture.httpClient != null) {
135             fixture.httpClient.stop();
136         }
137
138         // use a mocked HTTP client
139         httpClient = mock(BoschHttpClient.class);
140         fixture.httpClient = httpClient;
141     }
142
143     @Test
144     void postAction() throws InterruptedException, TimeoutException, ExecutionException {
145         String endpoint = "/intrusion/actions/arm";
146         String url = "https://127.0.0.1:8444/smarthome/intrusion/actions/arm";
147         when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(url);
148         Request mockRequest = mock(Request.class);
149         when(httpClient.createRequest(anyString(), any(), any())).thenReturn(mockRequest);
150         ArmActionRequest request = new ArmActionRequest();
151         request.profileId = "0";
152
153         fixture.postAction(endpoint, request);
154         verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), same(request));
155         verify(mockRequest).send();
156     }
157
158     @Test
159     void initialAccessHttpClientOffline() {
160         fixture.initialAccess(httpClient);
161     }
162
163     @Test
164     void initialAccessHttpClientOnline() throws InterruptedException {
165         when(httpClient.isOnline()).thenReturn(true);
166         fixture.initialAccess(httpClient);
167     }
168
169     @Test
170     void initialAccessAccessPossible()
171             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
172         when(httpClient.isOnline()).thenReturn(true);
173         when(httpClient.isAccessPossible()).thenReturn(true);
174         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
175         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
176
177         // mock a request and response to obtain rooms
178         Request roomsRequest = mock(Request.class);
179         ContentResponse roomsResponse = mock(ContentResponse.class);
180         when(roomsResponse.getStatus()).thenReturn(200);
181         when(roomsResponse.getContentAsString()).thenReturn(
182                 "[{\"@type\":\"room\",\"id\":\"hz_1\",\"iconId\":\"icon_room_bedroom\",\"name\":\"Bedroom\"}]");
183         when(roomsRequest.send()).thenReturn(roomsResponse);
184         when(httpClient.createRequest(contains("/rooms"), same(HttpMethod.GET))).thenReturn(roomsRequest);
185
186         // mock a request and response to obtain devices
187         Request devicesRequest = mock(Request.class);
188         ContentResponse devicesResponse = mock(ContentResponse.class);
189         when(devicesResponse.getStatus()).thenReturn(200);
190         when(devicesResponse.getContentAsString()).thenReturn("""
191                 [{"@type":"device",
192                  "rootDeviceId":"64-da-a0-02-14-9b",
193                  "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
194                  "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
195                  "manufacturer":"BOSCH",
196                  "roomId":"hz_3",
197                  "deviceModel":"PSM",
198                  "serial":"3014F711A00004953859F31B",
199                  "profile":"GENERIC",
200                  "name":"Coffee Machine",
201                  "status":"AVAILABLE",
202                  "childDeviceIds":[]
203                  }]\
204                 """);
205         when(devicesRequest.send()).thenReturn(devicesResponse);
206         when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
207
208         SubscribeResult subscribeResult = new SubscribeResult();
209         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
210
211         Request longPollRequest = mock(Request.class);
212         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
213                 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
214
215         fixture.initialAccess(httpClient);
216         verify(thingHandlerCallback).statusUpdated(any(),
217                 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
218     }
219
220     @Test
221     void getState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
222         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
223         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
224         Request request = mock(Request.class);
225         when(request.header(anyString(), anyString())).thenReturn(request);
226         ContentResponse response = mock(ContentResponse.class);
227         when(response.getStatus()).thenReturn(200);
228         when(response.getContentAsString()).thenReturn("""
229                 {
230                      "@type": "systemState",
231                      "systemAvailability": {
232                          "@type": "systemAvailabilityState",
233                          "available": true,
234                          "deleted": false
235                      },
236                      "armingState": {
237                          "@type": "armingState",
238                          "state": "SYSTEM_DISARMED",
239                          "deleted": false
240                      },
241                      "alarmState": {
242                          "@type": "alarmState",
243                          "value": "ALARM_OFF",
244                          "incidents": [],
245                          "deleted": false
246                      },
247                      "activeConfigurationProfile": {
248                          "@type": "activeConfigurationProfile",
249                          "deleted": false
250                      },
251                      "securityGapState": {
252                          "@type": "securityGapState",
253                          "securityGaps": [],
254                          "deleted": false
255                      },
256                      "deleted": false
257                  }\
258                 """);
259         when(request.send()).thenReturn(response);
260         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
261
262         IntrusionDetectionSystemState state = fixture.getState("intrusion/states/system",
263                 IntrusionDetectionSystemState.class);
264         assertNotNull(state);
265         assertTrue(state.systemAvailability.available);
266         assertSame(AlarmState.ALARM_OFF, state.alarmState.value);
267         assertSame(ArmingState.SYSTEM_DISARMED, state.armingState.state);
268     }
269
270     @Test
271     void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
272         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
273         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
274         when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod();
275         when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
276
277         Request request = mock(Request.class);
278         when(request.header(anyString(), anyString())).thenReturn(request);
279         ContentResponse response = mock(ContentResponse.class);
280         when(response.getStatus()).thenReturn(200);
281         when(response.getContentAsString())
282                 .thenReturn("{\n" + "   \"@type\": \"shutterContactState\",\n" + "   \"value\": \"OPEN\"\n" + " }");
283         when(request.send()).thenReturn(response);
284         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
285
286         ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
287                 "ShutterContact", ShutterContactServiceState.class);
288         assertNotNull(state);
289         assertSame(ShutterContactState.OPEN, state.value);
290     }
291
292     @Test
293     void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
294         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
295         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
296
297         Request request = mock(Request.class);
298         when(request.header(anyString(), anyString())).thenReturn(request);
299         ContentResponse response = mock(ContentResponse.class);
300         when(response.getStatus()).thenReturn(200);
301         when(request.send()).thenReturn(response);
302         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
303         when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
304                 .thenReturn(DeviceTest.createTestDevice());
305
306         String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
307         Device device = fixture.getDeviceInfo(deviceId);
308         assertEquals(deviceId, device.id);
309     }
310
311     @Test
312     void getDeviceInfoErrorCases()
313             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
314         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
315         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
316
317         Request request = mock(Request.class);
318         when(request.header(anyString(), anyString())).thenReturn(request);
319         ContentResponse response = mock(ContentResponse.class);
320         when(response.getStatus()).thenReturn(200);
321         when(request.send()).thenReturn(response);
322         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
323
324         @SuppressWarnings("unchecked")
325         ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
326                 .forClass(BiFunction.class);
327
328         when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
329                 .thenReturn(DeviceTest.createTestDevice());
330
331         String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
332         fixture.getDeviceInfo(deviceId);
333
334         BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
335         Exception e = errorResponseHandler.apply(500,
336                 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
337         assertEquals(
338                 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
339                 e.getMessage());
340
341         e = errorResponseHandler.apply(404,
342                 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
343         assertNotNull(e);
344
345         e = errorResponseHandler.apply(500, "");
346         assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
347                 e.getMessage());
348     }
349
350     @Test
351     void getServiceData() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
352         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
353         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
354         when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
355
356         Request request = mock(Request.class);
357         when(request.header(anyString(), anyString())).thenReturn(request);
358         ContentResponse response = mock(ContentResponse.class);
359         when(response.getStatus()).thenReturn(200);
360         when(response.getContentAsString()).thenReturn("""
361                 {
362                     "@type":"DeviceServiceData",
363                     "path":"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel",
364                     "id":"BatteryLevel",
365                     "deviceId":"hdm:ZigBee:000d6f0004b93361",
366                     "faults":{\s
367                         "entries":[
368                           {
369                             "type":"LOW_BATTERY",
370                             "category":"WARNING"
371                           }
372                         ]
373                     }
374                 }\
375                 """);
376         when(request.send()).thenReturn(response);
377         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
378
379         DeviceServiceData serviceData = fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel");
380         assertNotNull(serviceData);
381         Faults faults = serviceData.faults;
382         assertNotNull(faults);
383         assertEquals("LOW_BATTERY", faults.entries.get(0).type);
384     }
385
386     @Test
387     void getServiceDataError() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
388         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
389         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
390         when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
391
392         Request request = mock(Request.class);
393         when(request.header(anyString(), anyString())).thenReturn(request);
394         ContentResponse response = mock(ContentResponse.class);
395         when(response.getStatus()).thenReturn(500);
396         when(response.getContentAsString()).thenReturn(
397                 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
398         when(request.send()).thenReturn(response);
399         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
400         when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
401                 .thenReturn(DeviceTest.createTestDevice());
402
403         BoschSHCException e = assertThrows(BoschSHCException.class,
404                 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
405         assertEquals(
406                 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
407                 e.getMessage());
408     }
409
410     @Test
411     void getServiceDataErrorNoRestExceptionResponse()
412             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
413         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
414         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
415         when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
416
417         Request request = mock(Request.class);
418         when(request.header(anyString(), anyString())).thenReturn(request);
419         ContentResponse response = mock(ContentResponse.class);
420         when(response.getStatus()).thenReturn(500);
421         when(response.getContentAsString()).thenReturn("");
422         when(request.send()).thenReturn(response);
423         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
424
425         BoschSHCException e = assertThrows(BoschSHCException.class,
426                 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
427         assertEquals(
428                 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
429                 e.getMessage());
430     }
431
432     @Test
433     void putState() throws InterruptedException, TimeoutException, ExecutionException {
434         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
435         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
436         when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
437         when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod();
438
439         Request request = mock(Request.class);
440         when(request.header(anyString(), anyString())).thenReturn(request);
441         ContentResponse response = mock(ContentResponse.class);
442
443         when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
444                 .thenReturn(request);
445         when(request.send()).thenReturn(response);
446
447         BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
448         binarySwitchState.on = true;
449         fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
450     }
451
452     @Test
453     void getUserStateInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
454         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
455         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
456         String stateId = UUID.randomUUID().toString();
457
458         Request request = mock(Request.class);
459         when(request.header(anyString(), anyString())).thenReturn(request);
460         ContentResponse response = mock(ContentResponse.class);
461         when(response.getStatus()).thenReturn(200);
462         when(request.send()).thenReturn(response);
463         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
464         when(httpClient.sendRequest(same(request), same(UserDefinedState.class), any(), any()))
465                 .thenReturn(UserDefinedStateTest.createTestState(stateId));
466
467         UserDefinedState userState = fixture.getUserStateInfo(stateId);
468         assertEquals(stateId, userState.getId());
469     }
470
471     @Test
472     void getUserStates() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
473         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
474         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
475         String stateId = UUID.randomUUID().toString();
476
477         Request request = mock(Request.class);
478         when(request.header(anyString(), anyString())).thenReturn(request);
479         ContentResponse response = mock(ContentResponse.class);
480         when(response.getStatus()).thenReturn(200);
481         when(request.send()).thenReturn(response);
482         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
483         when(response.getContentAsString()).thenReturn(
484                 GsonUtils.DEFAULT_GSON_INSTANCE.toJson(List.of(UserDefinedStateTest.createTestState(stateId))));
485
486         List<UserDefinedState> userStates = fixture.getUserStates();
487         assertEquals(1, userStates.size());
488     }
489
490     @Test
491     void getUserStatesReturnsEmptyListIfRequestNotSuccessful()
492             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
493         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
494         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
495
496         Request request = mock(Request.class);
497         when(request.header(anyString(), anyString())).thenReturn(request);
498         ContentResponse response = mock(ContentResponse.class);
499         when(response.getStatus()).thenReturn(401);
500         when(request.send()).thenReturn(response);
501         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
502
503         List<UserDefinedState> userStates = fixture.getUserStates();
504         assertTrue(userStates.isEmpty());
505     }
506
507     @Test
508     void getUserStatesReturnsEmptyListIfExceptionHappened()
509             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
510         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
511         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
512
513         Request request = mock(Request.class);
514         when(request.header(anyString(), anyString())).thenReturn(request);
515         ContentResponse response = mock(ContentResponse.class);
516         when(response.getStatus()).thenReturn(401);
517         when(request.send()).thenThrow(new TimeoutException("text exception"));
518         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
519
520         List<UserDefinedState> userStates = fixture.getUserStates();
521         assertTrue(userStates.isEmpty());
522     }
523
524     @AfterEach
525     void afterEach() throws Exception {
526         fixture.dispose();
527     }
528
529     @Test
530     void handleLongPollResultNoDeviceId() {
531         List<Thing> things = new ArrayList<Thing>();
532         when(thing.getThings()).thenReturn(things);
533
534         Thing thing = mock(Thing.class);
535         things.add(thing);
536
537         BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
538         when(thing.getHandler()).thenReturn(thingHandler);
539
540         String json = """
541                 {
542                   "result": [{
543                     "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
544                     "@type": "DeviceServiceData",
545                     "id": "PowerSwitch",
546                     "state": {
547                        "@type": "powerSwitchState",
548                        "switchState": "ON"
549                     },
550                     "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
551                   }],
552                   "jsonrpc": "2.0"
553                 }
554                 """;
555         LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
556         assertNotNull(longPollResult);
557
558         fixture.handleLongPollResult(longPollResult);
559
560         verify(thingHandler).getBoschID();
561         verifyNoMoreInteractions(thingHandler);
562     }
563
564     @Test
565     void handleLongPollResult() {
566         List<Thing> things = new ArrayList<Thing>();
567         when(thing.getThings()).thenReturn(things);
568
569         Thing thing = mock(Thing.class);
570         things.add(thing);
571
572         BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
573         when(thing.getHandler()).thenReturn(thingHandler);
574
575         when(thingHandler.getBoschID()).thenReturn("hdm:HomeMaticIP:3014F711A0001916D859A8A9");
576
577         String json = """
578                 {
579                   "result": [{
580                     "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
581                     "@type": "DeviceServiceData",
582                     "id": "PowerSwitch",
583                     "state": {
584                        "@type": "powerSwitchState",
585                        "switchState": "ON"
586                     },
587                     "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
588                   }],
589                   "jsonrpc": "2.0"
590                 }
591                 """;
592         LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
593         assertNotNull(longPollResult);
594
595         fixture.handleLongPollResult(longPollResult);
596
597         verify(thingHandler).getBoschID();
598
599         JsonElement expectedState = JsonParser.parseString("""
600                 {
601                     "@type": "powerSwitchState",
602                     "switchState": "ON"
603                 }
604                 """);
605
606         verify(thingHandler).processUpdate("PowerSwitch", expectedState);
607     }
608
609     @Test
610     void handleLongPollResultHandleChildUpdate() {
611         List<Thing> things = new ArrayList<Thing>();
612         when(thing.getThings()).thenReturn(things);
613
614         Thing thing = mock(Thing.class);
615         things.add(thing);
616
617         BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
618         when(thing.getHandler()).thenReturn(thingHandler);
619
620         when(thingHandler.getBoschID()).thenReturn("hdm:ZigBee:70ac08fffefead2d");
621
622         String json = """
623                 {
624                   "result": [{
625                     "path": "/devices/hdm:ZigBee:70ac08fffefead2d#3/services/PowerSwitch",
626                     "@type": "DeviceServiceData",
627                     "id": "PowerSwitch",
628                     "state": {
629                        "@type": "powerSwitchState",
630                        "switchState": "ON"
631                     },
632                     "deviceId": "hdm:ZigBee:70ac08fffefead2d#3"
633                   }],
634                   "jsonrpc": "2.0"
635                 }
636                 """;
637         LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
638         assertNotNull(longPollResult);
639
640         fixture.handleLongPollResult(longPollResult);
641
642         verify(thingHandler).getBoschID();
643
644         JsonElement expectedState = JsonParser.parseString("""
645                 {
646                     "@type": "powerSwitchState",
647                     "switchState": "ON"
648                 }
649                 """);
650
651         verify(thingHandler).processChildUpdate("hdm:ZigBee:70ac08fffefead2d#3", "PowerSwitch", expectedState);
652     }
653 }