2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschshc.internal.devices.bridge;
15 import static org.hamcrest.MatcherAssert.assertThat;
16 import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
17 import static org.junit.jupiter.api.Assertions.assertEquals;
18 import static org.junit.jupiter.api.Assertions.assertFalse;
19 import static org.junit.jupiter.api.Assertions.assertNotNull;
20 import static org.junit.jupiter.api.Assertions.assertSame;
21 import static org.junit.jupiter.api.Assertions.assertThrows;
22 import static org.junit.jupiter.api.Assertions.assertTrue;
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.ArgumentMatchers.anyString;
25 import static org.mockito.ArgumentMatchers.argThat;
26 import static org.mockito.ArgumentMatchers.contains;
27 import static org.mockito.ArgumentMatchers.eq;
28 import static org.mockito.ArgumentMatchers.isNull;
29 import static org.mockito.ArgumentMatchers.same;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.verify;
32 import static org.mockito.Mockito.verifyNoInteractions;
33 import static org.mockito.Mockito.verifyNoMoreInteractions;
34 import static org.mockito.Mockito.when;
36 import java.io.IOException;
37 import java.nio.file.Files;
38 import java.nio.file.Path;
39 import java.nio.file.Paths;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.List;
44 import java.util.UUID;
45 import java.util.concurrent.ExecutionException;
46 import java.util.concurrent.TimeoutException;
47 import java.util.function.BiFunction;
49 import org.eclipse.jdt.annotation.NonNullByDefault;
50 import org.eclipse.jdt.annotation.Nullable;
51 import org.eclipse.jetty.client.api.ContentResponse;
52 import org.eclipse.jetty.client.api.Request;
53 import org.eclipse.jetty.http.HttpMethod;
54 import org.junit.jupiter.api.AfterEach;
55 import org.junit.jupiter.api.BeforeAll;
56 import org.junit.jupiter.api.BeforeEach;
57 import org.junit.jupiter.api.Test;
58 import org.junit.jupiter.params.ParameterizedTest;
59 import org.junit.jupiter.params.provider.MethodSource;
60 import org.mockito.ArgumentCaptor;
61 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
62 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
63 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
64 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
65 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
66 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
67 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
68 import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
69 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
70 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
71 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
72 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
73 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest;
74 import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
75 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
76 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
77 import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState;
78 import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.ArmActionRequest;
79 import org.openhab.binding.boschshc.internal.services.intrusion.dto.AlarmState;
80 import org.openhab.binding.boschshc.internal.services.intrusion.dto.ArmingState;
81 import org.openhab.binding.boschshc.internal.services.intrusion.dto.IntrusionDetectionSystemState;
82 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
83 import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
84 import org.openhab.core.config.core.Configuration;
85 import org.openhab.core.library.types.OnOffType;
86 import org.openhab.core.library.types.StringType;
87 import org.openhab.core.thing.Bridge;
88 import org.openhab.core.thing.Channel;
89 import org.openhab.core.thing.ChannelUID;
90 import org.openhab.core.thing.Thing;
91 import org.openhab.core.thing.ThingStatus;
92 import org.openhab.core.thing.ThingStatusDetail;
93 import org.openhab.core.thing.ThingStatusInfo;
94 import org.openhab.core.thing.binding.ThingHandlerCallback;
95 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
97 import com.google.gson.JsonElement;
98 import com.google.gson.JsonParser;
99 import com.google.gson.JsonPrimitive;
102 * Unit tests for the {@link BridgeHandler}.
104 * @author David Pace - Initial contribution
108 class BridgeHandlerTest {
110 private @NonNullByDefault({}) BridgeHandler fixture;
112 private @NonNullByDefault({}) BoschHttpClient httpClient;
113 private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
114 private @NonNullByDefault({}) Bridge thing;
115 private @NonNullByDefault({}) Configuration bridgeConfiguration;
118 static void beforeAll() throws IOException {
119 Path mavenTargetFolder = Paths.get("target");
120 assertTrue(Files.exists(mavenTargetFolder), "Maven target folder does not exist.");
121 System.setProperty("openhab.userdata", mavenTargetFolder.toFile().getAbsolutePath());
122 Path etc = mavenTargetFolder.resolve("etc");
123 if (!Files.exists(etc)) {
124 Files.createDirectory(etc);
129 void beforeEach() throws Exception {
130 Bridge bridge = mock(Bridge.class);
131 fixture = new BridgeHandler(bridge);
133 thingHandlerCallback = mock(ThingHandlerCallback.class);
134 fixture.setCallback(thingHandlerCallback);
136 bridgeConfiguration = new Configuration();
137 Map<@Nullable String, @Nullable Object> properties = new HashMap<>();
138 properties.put("ipAddress", "localhost");
139 properties.put("password", "test");
140 bridgeConfiguration.setProperties(properties);
142 thing = mock(Bridge.class);
143 when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
144 // this calls initialize() as well
145 fixture.thingUpdated(thing);
147 // shut down the real HTTP client
148 if (fixture.httpClient != null) {
149 fixture.httpClient.stop();
152 // use a mocked HTTP client
153 httpClient = mock(BoschHttpClient.class);
154 fixture.httpClient = httpClient;
158 void postAction() throws InterruptedException, TimeoutException, ExecutionException {
159 String endpoint = "/intrusion/actions/arm";
160 String url = "https://127.0.0.1:8444/smarthome/intrusion/actions/arm";
161 when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(url);
162 Request mockRequest = mock(Request.class);
163 when(httpClient.createRequest(anyString(), any(), any())).thenReturn(mockRequest);
164 ArmActionRequest request = new ArmActionRequest();
165 request.profileId = "0";
167 fixture.postAction(endpoint, request);
168 verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), same(request));
169 verify(mockRequest).send();
173 void postActionWithoutRequestBody() throws InterruptedException, TimeoutException, ExecutionException {
174 String endpoint = "/intrusion/actions/disarm";
175 String url = "https://127.0.0.1:8444/smarthome/intrusion/actions/disarm";
176 when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(url);
177 Request mockRequest = mock(Request.class);
178 when(httpClient.createRequest(anyString(), any(), any())).thenReturn(mockRequest);
180 fixture.postAction(endpoint);
181 verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), isNull());
182 verify(mockRequest).send();
186 void initialAccessHttpClientOffline() {
187 fixture.initialAccess(httpClient);
191 void initialAccessHttpClientOnline() throws InterruptedException {
192 when(httpClient.isOnline()).thenReturn(true);
193 fixture.initialAccess(httpClient);
197 void initialAccessAccessPossible()
198 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
199 when(httpClient.isOnline()).thenReturn(true);
200 when(httpClient.isAccessPossible()).thenReturn(true);
201 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
202 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
204 // mock a request and response to obtain rooms
205 Request roomsRequest = mock(Request.class);
206 ContentResponse roomsResponse = mock(ContentResponse.class);
207 when(roomsResponse.getStatus()).thenReturn(200);
208 when(roomsResponse.getContentAsString()).thenReturn(
209 "[{\"@type\":\"room\",\"id\":\"hz_1\",\"iconId\":\"icon_room_bedroom\",\"name\":\"Bedroom\"}]");
210 when(roomsRequest.send()).thenReturn(roomsResponse);
211 when(httpClient.createRequest(contains("/rooms"), same(HttpMethod.GET))).thenReturn(roomsRequest);
213 // mock a request and response to obtain devices
214 Request devicesRequest = mock(Request.class);
215 ContentResponse devicesResponse = mock(ContentResponse.class);
216 when(devicesResponse.getStatus()).thenReturn(200);
217 when(devicesResponse.getContentAsString()).thenReturn("""
219 "rootDeviceId":"64-da-a0-02-14-9b",
220 "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
221 "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
222 "manufacturer":"BOSCH",
225 "serial":"3014F711A00004953859F31B",
227 "name":"Coffee Machine",
228 "status":"AVAILABLE",
232 when(devicesRequest.send()).thenReturn(devicesResponse);
233 when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
235 SubscribeResult subscribeResult = new SubscribeResult();
236 when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
238 Request longPollRequest = mock(Request.class);
239 when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
240 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
242 ThingDiscoveryService thingDiscoveryListener = mock(ThingDiscoveryService.class);
243 fixture.registerDiscoveryListener(thingDiscoveryListener);
245 fixture.initialAccess(httpClient);
247 verify(thingHandlerCallback).statusUpdated(any(),
248 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
249 verify(thingDiscoveryListener).doScan();
253 void initialAccessNoBridgeAccess() throws InterruptedException, TimeoutException, ExecutionException {
254 when(httpClient.isOnline()).thenReturn(true);
255 when(httpClient.isAccessPossible()).thenReturn(true);
256 Request request = mock(Request.class);
257 when(httpClient.createRequest(any(), same(HttpMethod.GET))).thenReturn(request);
258 ContentResponse response = mock(ContentResponse.class);
259 when(request.send()).thenReturn(response);
260 when(response.getStatus()).thenReturn(400);
262 fixture.initialAccess(httpClient);
264 verify(thingHandlerCallback).statusUpdated(same(thing),
265 argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
266 && status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
270 void getState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
271 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
272 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
273 Request request = mock(Request.class);
274 when(request.header(anyString(), anyString())).thenReturn(request);
275 ContentResponse response = mock(ContentResponse.class);
276 when(response.getStatus()).thenReturn(200);
277 when(response.getContentAsString()).thenReturn("""
279 "@type": "systemState",
280 "systemAvailability": {
281 "@type": "systemAvailabilityState",
286 "@type": "armingState",
287 "state": "SYSTEM_DISARMED",
291 "@type": "alarmState",
292 "value": "ALARM_OFF",
296 "activeConfigurationProfile": {
297 "@type": "activeConfigurationProfile",
300 "securityGapState": {
301 "@type": "securityGapState",
308 when(request.send()).thenReturn(response);
309 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
311 IntrusionDetectionSystemState state = fixture.getState("intrusion/states/system",
312 IntrusionDetectionSystemState.class);
313 assertNotNull(state);
314 assertTrue(state.systemAvailability.available);
315 assertSame(AlarmState.ALARM_OFF, state.alarmState.value);
316 assertSame(ArmingState.SYSTEM_DISARMED, state.armingState.state);
320 void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
321 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
322 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
323 when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod();
324 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
326 Request request = mock(Request.class);
327 when(request.header(anyString(), anyString())).thenReturn(request);
328 ContentResponse response = mock(ContentResponse.class);
329 when(response.getStatus()).thenReturn(200);
330 when(response.getContentAsString())
331 .thenReturn("{\n" + " \"@type\": \"shutterContactState\",\n" + " \"value\": \"OPEN\"\n" + " }");
332 when(request.send()).thenReturn(response);
333 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
335 ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
336 "ShutterContact", ShutterContactServiceState.class);
337 assertNotNull(state);
338 assertSame(ShutterContactState.OPEN, state.value);
342 void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
343 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
344 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
346 Request request = mock(Request.class);
347 when(request.header(anyString(), anyString())).thenReturn(request);
348 ContentResponse response = mock(ContentResponse.class);
349 when(response.getStatus()).thenReturn(200);
350 when(request.send()).thenReturn(response);
351 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
352 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
353 .thenReturn(DeviceTest.createTestDevice());
355 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
356 Device device = fixture.getDeviceInfo(deviceId);
357 assertEquals(deviceId, device.id);
361 void getDeviceInfoErrorCases()
362 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
363 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
364 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
366 Request request = mock(Request.class);
367 when(request.header(anyString(), anyString())).thenReturn(request);
368 ContentResponse response = mock(ContentResponse.class);
369 when(response.getStatus()).thenReturn(200);
370 when(request.send()).thenReturn(response);
371 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
373 @SuppressWarnings("unchecked")
374 ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
375 .forClass(BiFunction.class);
377 when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
378 .thenReturn(DeviceTest.createTestDevice());
380 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
381 fixture.getDeviceInfo(deviceId);
383 BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
384 Exception e = errorResponseHandler.apply(500,
385 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
387 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
390 e = errorResponseHandler.apply(404,
391 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
394 e = errorResponseHandler.apply(500, "");
395 assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
400 void getServiceData() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
401 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
402 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
403 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
405 Request request = mock(Request.class);
406 when(request.header(anyString(), anyString())).thenReturn(request);
407 ContentResponse response = mock(ContentResponse.class);
408 when(response.getStatus()).thenReturn(200);
409 when(response.getContentAsString()).thenReturn("""
411 "@type":"DeviceServiceData",
412 "path":"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel",
414 "deviceId":"hdm:ZigBee:000d6f0004b93361",
418 "type":"LOW_BATTERY",
425 when(request.send()).thenReturn(response);
426 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
428 DeviceServiceData serviceData = fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel");
429 assertNotNull(serviceData);
430 Faults faults = serviceData.faults;
431 assertNotNull(faults);
432 assertEquals("LOW_BATTERY", faults.entries.get(0).type);
436 void getServiceDataError() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
437 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
438 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
439 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
441 Request request = mock(Request.class);
442 when(request.header(anyString(), anyString())).thenReturn(request);
443 ContentResponse response = mock(ContentResponse.class);
444 when(response.getStatus()).thenReturn(500);
445 when(response.getContentAsString()).thenReturn(
446 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
447 when(request.send()).thenReturn(response);
448 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
449 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
450 .thenReturn(DeviceTest.createTestDevice());
452 BoschSHCException e = assertThrows(BoschSHCException.class,
453 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
455 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
460 void getServiceDataErrorNoRestExceptionResponse()
461 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
462 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
463 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
464 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
466 Request request = mock(Request.class);
467 when(request.header(anyString(), anyString())).thenReturn(request);
468 ContentResponse response = mock(ContentResponse.class);
469 when(response.getStatus()).thenReturn(500);
470 when(response.getContentAsString()).thenReturn("");
471 when(request.send()).thenReturn(response);
472 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
474 BoschSHCException e = assertThrows(BoschSHCException.class,
475 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
477 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
482 void putState() throws InterruptedException, TimeoutException, ExecutionException {
483 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
484 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
485 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
486 when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod();
488 Request request = mock(Request.class);
489 when(request.header(anyString(), anyString())).thenReturn(request);
490 ContentResponse response = mock(ContentResponse.class);
492 when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
493 .thenReturn(request);
494 when(request.send()).thenReturn(response);
496 BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
497 binarySwitchState.on = true;
498 fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
502 void getUserStateInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
503 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
504 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
505 String stateId = UUID.randomUUID().toString();
507 Request request = mock(Request.class);
508 when(request.header(anyString(), anyString())).thenReturn(request);
509 ContentResponse response = mock(ContentResponse.class);
510 when(response.getStatus()).thenReturn(200);
511 when(request.send()).thenReturn(response);
512 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
513 when(httpClient.sendRequest(same(request), same(UserDefinedState.class), any(), any()))
514 .thenReturn(UserDefinedStateTest.createTestState(stateId));
516 UserDefinedState userState = fixture.getUserStateInfo(stateId);
517 assertEquals(stateId, userState.getId());
521 void getUserStateInfoErrorCases()
522 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
523 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
524 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
526 Request request = mock(Request.class);
527 when(request.header(anyString(), anyString())).thenReturn(request);
528 ContentResponse response = mock(ContentResponse.class);
529 when(response.getStatus()).thenReturn(200);
530 when(request.send()).thenReturn(response);
531 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
533 @SuppressWarnings("unchecked")
534 ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
535 .forClass(BiFunction.class);
537 String stateId = "abcdef";
538 when(httpClient.sendRequest(same(request), same(UserDefinedState.class), any(),
539 errorResponseHandlerCaptor.capture())).thenReturn(UserDefinedStateTest.createTestState(stateId));
541 fixture.getUserStateInfo(stateId);
543 BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
544 Exception e = errorResponseHandler.apply(500,
545 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
547 "Request for info of user-defined state abcdef failed with status code 500 and error code testErrorCode",
550 e = errorResponseHandler.apply(404,
551 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
554 e = errorResponseHandler.apply(500, "");
555 assertEquals("Request for info of user-defined state abcdef failed with status code 500", e.getMessage());
559 void getUserStates() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
560 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
561 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
562 String stateId = UUID.randomUUID().toString();
564 Request request = mock(Request.class);
565 when(request.header(anyString(), anyString())).thenReturn(request);
566 ContentResponse response = mock(ContentResponse.class);
567 when(response.getStatus()).thenReturn(200);
568 when(request.send()).thenReturn(response);
569 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
570 when(response.getContentAsString()).thenReturn(
571 GsonUtils.DEFAULT_GSON_INSTANCE.toJson(List.of(UserDefinedStateTest.createTestState(stateId))));
573 List<UserDefinedState> userStates = fixture.getUserStates();
574 assertEquals(1, userStates.size());
578 void getUserStatesReturnsEmptyListIfRequestNotSuccessful()
579 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
580 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
581 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
583 Request request = mock(Request.class);
584 when(request.header(anyString(), anyString())).thenReturn(request);
585 ContentResponse response = mock(ContentResponse.class);
586 when(response.getStatus()).thenReturn(401);
587 when(request.send()).thenReturn(response);
588 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
590 List<UserDefinedState> userStates = fixture.getUserStates();
591 assertTrue(userStates.isEmpty());
595 void getUserStatesReturnsEmptyListIfExceptionHappened()
596 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
597 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
598 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
600 Request request = mock(Request.class);
601 when(request.header(anyString(), anyString())).thenReturn(request);
602 ContentResponse response = mock(ContentResponse.class);
603 when(response.getStatus()).thenReturn(401);
604 when(request.send()).thenThrow(new TimeoutException("text exception"));
605 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
607 List<UserDefinedState> userStates = fixture.getUserStates();
608 assertTrue(userStates.isEmpty());
612 void afterEach() throws Exception {
617 void handleLongPollResultNoDeviceId() {
618 List<Thing> things = new ArrayList<Thing>();
619 when(thing.getThings()).thenReturn(things);
621 Thing thing = mock(Thing.class);
624 BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
625 when(thing.getHandler()).thenReturn(thingHandler);
630 "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
631 "@type": "DeviceServiceData",
634 "@type": "powerSwitchState",
637 "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
642 LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
643 assertNotNull(longPollResult);
645 fixture.handleLongPollResult(longPollResult);
647 verify(thingHandler).getBoschID();
648 verifyNoMoreInteractions(thingHandler);
652 void handleLongPollResult() {
653 List<Thing> things = new ArrayList<Thing>();
654 when(thing.getThings()).thenReturn(things);
656 Thing thing = mock(Thing.class);
659 BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
660 when(thing.getHandler()).thenReturn(thingHandler);
662 when(thingHandler.getBoschID()).thenReturn("hdm:HomeMaticIP:3014F711A0001916D859A8A9");
667 "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
668 "@type": "DeviceServiceData",
671 "@type": "powerSwitchState",
674 "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
679 LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
680 assertNotNull(longPollResult);
682 fixture.handleLongPollResult(longPollResult);
684 verify(thingHandler).getBoschID();
686 JsonElement expectedState = JsonParser.parseString("""
688 "@type": "powerSwitchState",
693 verify(thingHandler).processUpdate("PowerSwitch", expectedState);
697 void handleLongPollResultHandleChildUpdate() {
698 List<Thing> things = new ArrayList<Thing>();
699 when(thing.getThings()).thenReturn(things);
701 Thing thing = mock(Thing.class);
704 BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
705 when(thing.getHandler()).thenReturn(thingHandler);
707 when(thingHandler.getBoschID()).thenReturn("hdm:ZigBee:70ac08fffefead2d");
712 "path": "/devices/hdm:ZigBee:70ac08fffefead2d#3/services/PowerSwitch",
713 "@type": "DeviceServiceData",
716 "@type": "powerSwitchState",
719 "deviceId": "hdm:ZigBee:70ac08fffefead2d#3"
724 LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
725 assertNotNull(longPollResult);
727 fixture.handleLongPollResult(longPollResult);
729 verify(thingHandler).getBoschID();
731 JsonElement expectedState = JsonParser.parseString("""
733 "@type": "powerSwitchState",
738 verify(thingHandler).processChildUpdate("hdm:ZigBee:70ac08fffefead2d#3", "PowerSwitch", expectedState);
742 void handleLongPollResultScenarioTriggered() {
743 Channel channel = mock(Channel.class);
744 when(thing.getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED)).thenReturn(channel);
745 when(thingHandlerCallback.isChannelLinked(any())).thenReturn(true);
750 "@type": "scenarioTriggered",
751 "name": "My Scenario",
752 "id": "509bd737-eed0-40b7-8caa-e8686a714399",
753 "lastTimeTriggered": "1693758693032"
758 LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
759 assertNotNull(longPollResult);
761 fixture.handleLongPollResult(longPollResult);
763 verify(thingHandlerCallback).stateUpdated(any(), eq(new StringType("My Scenario")));
767 void handleLongPollResultUserDefinedState() {
768 List<Thing> things = new ArrayList<Thing>();
769 when(thing.getThings()).thenReturn(things);
771 Thing thing = mock(Thing.class);
774 BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
775 when(thing.getHandler()).thenReturn(thingHandler);
777 when(thingHandler.getBoschID()).thenReturn("3d8023d6-69ca-4e79-89dd-7090295cefbf");
783 "@type": "userDefinedState",
784 "name": "Test State",
785 "id": "3d8023d6-69ca-4e79-89dd-7090295cefbf",
791 LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
792 assertNotNull(longPollResult);
794 fixture.handleLongPollResult(longPollResult);
796 JsonElement expectedState = new JsonPrimitive(true);
798 verify(thingHandler).processUpdate("3d8023d6-69ca-4e79-89dd-7090295cefbf", expectedState);
802 void handleLongPollFailure() {
803 Throwable e = new RuntimeException("Test exception");
804 fixture.handleLongPollFailure(e);
806 ThingStatusInfo expectedStatus = ThingStatusInfoBuilder
807 .create(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE).build();
808 verify(thingHandlerCallback).statusUpdated(thing, expectedStatus);
812 void getDevices() throws InterruptedException, TimeoutException, ExecutionException {
813 Request request = mock(Request.class);
814 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
815 ContentResponse contentResponse = mock(ContentResponse.class);
816 when(request.send()).thenReturn(contentResponse);
817 when(contentResponse.getStatus()).thenReturn(200);
818 String devicesJson = """
822 "rootDeviceId": "64-da-a0-3e-81-0c",
823 "id": "hdm:ZigBee:0c4314fffea15de7",
824 "deviceServiceIds": [
825 "CommunicationQuality",
828 "PowerSwitchConfiguration",
831 "manufacturer": "BOSCH",
833 "deviceModel": "PLUG_COMPACT",
834 "serial": "0C4314FFFE802BE2",
836 "iconId": "icon_plug_lamp_table",
837 "name": "My Lamp Plug",
838 "status": "AVAILABLE",
839 "childDeviceIds": [],
840 "supportedProfiles": [
848 "rootDeviceId": "64-da-a0-3e-81-0c",
849 "id": "hdm:ZigBee:000d6f0012f13bfa",
850 "deviceServiceIds": [
852 "CommunicationQuality",
858 "manufacturer": "BOSCH",
861 "serial": "000D6F0012F0da96",
862 "profile": "GENERIC",
863 "name": "My Motion Detector",
864 "status": "AVAILABLE",
865 "childDeviceIds": [],
866 "supportedProfiles": []
870 when(contentResponse.getContentAsString()).thenReturn(devicesJson);
872 List<Device> devices = fixture.getDevices();
874 assertEquals(2, devices.size());
876 Device plugDevice = devices.get(0);
877 assertEquals("hdm:ZigBee:0c4314fffea15de7", plugDevice.id);
878 assertEquals(5, plugDevice.deviceServiceIds.size());
879 assertEquals(0, plugDevice.childDeviceIds.size());
881 Device motionDetectorDevice = devices.get(1);
882 assertEquals("hdm:ZigBee:000d6f0012f13bfa", motionDetectorDevice.id);
883 assertEquals(6, motionDetectorDevice.deviceServiceIds.size());
884 assertEquals(0, motionDetectorDevice.childDeviceIds.size());
888 void getDevicesErrorRestResponse() throws InterruptedException, TimeoutException, ExecutionException {
889 Request request = mock(Request.class);
890 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
891 ContentResponse contentResponse = mock(ContentResponse.class);
892 when(request.send()).thenReturn(contentResponse);
893 when(contentResponse.getStatus()).thenReturn(400); // bad request
895 List<Device> devices = fixture.getDevices();
897 assertThat(devices, hasSize(0));
901 @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutExceptionArguments()")
902 void getDevicesHandleExceptions() throws InterruptedException, TimeoutException, ExecutionException {
903 Request request = mock(Request.class);
904 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
905 when(request.send()).thenThrow(new ExecutionException(new RuntimeException("Test Exception")));
907 List<Device> devices = fixture.getDevices();
909 assertThat(devices, hasSize(0));
913 void getRooms() throws InterruptedException, TimeoutException, ExecutionException {
914 Request request = mock(Request.class);
915 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
916 ContentResponse contentResponse = mock(ContentResponse.class);
917 when(request.send()).thenReturn(contentResponse);
918 when(contentResponse.getStatus()).thenReturn(200);
919 String roomsJson = """
924 "iconId": "icon_room_living_room",
925 "name": "Living Room"
930 "iconId": "icon_room_dining_room",
931 "name": "Dining Room"
935 when(contentResponse.getContentAsString()).thenReturn(roomsJson);
937 List<Room> rooms = fixture.getRooms();
939 assertEquals(2, rooms.size());
941 Room livingRoom = rooms.get(0);
942 assertEquals("hz_1", livingRoom.id);
943 assertEquals("Living Room", livingRoom.name);
945 Room diningRoom = rooms.get(1);
946 assertEquals("hz_2", diningRoom.id);
947 assertEquals("Dining Room", diningRoom.name);
951 void getRoomsErrorRestResponse() throws InterruptedException, TimeoutException, ExecutionException {
952 Request request = mock(Request.class);
953 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
954 ContentResponse contentResponse = mock(ContentResponse.class);
955 when(request.send()).thenReturn(contentResponse);
956 when(contentResponse.getStatus()).thenReturn(400); // bad request
958 List<Room> rooms = fixture.getRooms();
960 assertThat(rooms, hasSize(0));
964 @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutExceptionArguments()")
965 void getRoomsHandleExceptions() throws InterruptedException, TimeoutException, ExecutionException {
966 Request request = mock(Request.class);
967 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
968 when(request.send()).thenThrow(new ExecutionException(new RuntimeException("Test Exception")));
970 List<Room> rooms = fixture.getRooms();
972 assertThat(rooms, hasSize(0));
977 assertTrue(fixture.getServices().contains(ThingDiscoveryService.class));
981 void handleCommandIrrelevantChannel() {
982 ChannelUID channelUID = mock(ChannelUID.class);
983 when(channelUID.getId()).thenReturn(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH);
985 fixture.handleCommand(channelUID, OnOffType.ON);
987 verifyNoInteractions(httpClient);
991 void handleCommandTriggerScenario()
992 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
993 ChannelUID channelUID = mock(ChannelUID.class);
994 when(channelUID.getId()).thenReturn(BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO);
996 // required to prevent NPE
997 when(httpClient.sendRequest(any(), eq(Scenario[].class), any(), any())).thenReturn(new Scenario[] {});
999 fixture.handleCommand(channelUID, OnOffType.ON);
1001 verify(httpClient).sendRequest(any(), eq(Scenario[].class), any(), any());
1005 void registerDiscoveryListener() {
1006 ThingDiscoveryService listener = mock(ThingDiscoveryService.class);
1007 assertTrue(fixture.registerDiscoveryListener(listener));
1008 assertFalse(fixture.registerDiscoveryListener(listener));
1012 void unregisterDiscoveryListener() {
1013 assertFalse(fixture.unregisterDiscoveryListener());
1014 fixture.registerDiscoveryListener(mock(ThingDiscoveryService.class));
1015 assertTrue(fixture.unregisterDiscoveryListener());
1019 void initializeNoIpAddress() {
1020 bridgeConfiguration.setProperties(new HashMap<String, Object>());
1022 fixture.initialize();
1024 ThingStatusInfo expectedStatus = ThingStatusInfoBuilder
1025 .create(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR)
1026 .withDescription("@text/offline.conf-error-empty-ip").build();
1027 verify(thingHandlerCallback).statusUpdated(thing, expectedStatus);
1031 void initializeNoPassword() {
1032 HashMap<String, Object> properties = new HashMap<String, Object>();
1033 properties.put("ipAddress", "localhost");
1034 bridgeConfiguration.setProperties(properties);
1036 fixture.initialize();
1038 ThingStatusInfo expectedStatus = ThingStatusInfoBuilder
1039 .create(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR)
1040 .withDescription("@text/offline.conf-error-empty-password").build();
1041 verify(thingHandlerCallback).statusUpdated(thing, expectedStatus);
1045 void checkBridgeAccess() throws InterruptedException, TimeoutException, ExecutionException {
1046 Request request = mock(Request.class);
1047 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
1048 ContentResponse contentResponse = mock(ContentResponse.class);
1049 when(request.send()).thenReturn(contentResponse);
1050 when(contentResponse.getStatus()).thenReturn(200);
1052 assertTrue(fixture.checkBridgeAccess());
1056 void checkBridgeAccessRestResponseError() throws InterruptedException, TimeoutException, ExecutionException {
1057 Request request = mock(Request.class);
1058 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
1059 ContentResponse contentResponse = mock(ContentResponse.class);
1060 when(request.send()).thenReturn(contentResponse);
1061 when(contentResponse.getStatus()).thenReturn(400);
1063 assertFalse(fixture.checkBridgeAccess());
1067 @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutExceptionArguments()")
1068 void checkBridgeAccessRestException(Exception e) throws InterruptedException, TimeoutException, ExecutionException {
1069 Request request = mock(Request.class);
1070 when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
1071 when(request.send()).thenThrow(e);
1073 assertFalse(fixture.checkBridgeAccess());
1077 void getPublicInformation() throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
1078 fixture.getPublicInformation();
1080 verify(httpClient).createRequest(any(), same(HttpMethod.GET));
1081 verify(httpClient).sendRequest(any(), same(PublicInformation.class), any(), isNull());