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.junit.jupiter.api.Assertions.*;
16 import static org.mockito.ArgumentMatchers.*;
17 import static org.mockito.Mockito.*;
19 import java.io.IOException;
20 import java.nio.file.Files;
21 import java.nio.file.Path;
22 import java.nio.file.Paths;
23 import java.util.HashMap;
24 import java.util.List;
26 import java.util.UUID;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeoutException;
29 import java.util.function.BiFunction;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.http.HttpMethod;
36 import org.junit.jupiter.api.AfterEach;
37 import org.junit.jupiter.api.BeforeAll;
38 import org.junit.jupiter.api.BeforeEach;
39 import org.junit.jupiter.api.Test;
40 import org.mockito.ArgumentCaptor;
41 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
42 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
43 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
44 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
45 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
46 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
47 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest;
48 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
49 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
50 import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState;
51 import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.ArmActionRequest;
52 import org.openhab.binding.boschshc.internal.services.intrusion.dto.AlarmState;
53 import org.openhab.binding.boschshc.internal.services.intrusion.dto.ArmingState;
54 import org.openhab.binding.boschshc.internal.services.intrusion.dto.IntrusionDetectionSystemState;
55 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
56 import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
57 import org.openhab.core.config.core.Configuration;
58 import org.openhab.core.thing.Bridge;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.binding.ThingHandlerCallback;
63 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
66 * Unit tests for the {@link BridgeHandler}.
68 * @author David Pace - Initial contribution
72 class BridgeHandlerTest {
74 private @NonNullByDefault({}) BridgeHandler fixture;
76 private @NonNullByDefault({}) BoschHttpClient httpClient;
78 private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
81 static void beforeAll() throws IOException {
82 Path mavenTargetFolder = Paths.get("target");
83 assertTrue(Files.exists(mavenTargetFolder), "Maven target folder does not exist.");
84 System.setProperty("openhab.userdata", mavenTargetFolder.toFile().getAbsolutePath());
85 Path etc = mavenTargetFolder.resolve("etc");
86 if (!Files.exists(etc)) {
87 Files.createDirectory(etc);
92 void beforeEach() throws Exception {
93 Bridge bridge = mock(Bridge.class);
94 fixture = new BridgeHandler(bridge);
96 thingHandlerCallback = mock(ThingHandlerCallback.class);
97 fixture.setCallback(thingHandlerCallback);
99 Configuration bridgeConfiguration = new Configuration();
100 Map<@Nullable String, @Nullable Object> properties = new HashMap<>();
101 properties.put("ipAddress", "localhost");
102 properties.put("password", "test");
103 bridgeConfiguration.setProperties(properties);
105 Thing thing = mock(Bridge.class);
106 when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
107 // this calls initialize() as well
108 fixture.thingUpdated(thing);
110 // shut down the real HTTP client
111 if (fixture.httpClient != null) {
112 fixture.httpClient.stop();
115 // use a mocked HTTP client
116 httpClient = mock(BoschHttpClient.class);
117 fixture.httpClient = httpClient;
121 void postAction() throws InterruptedException, TimeoutException, ExecutionException {
122 String endpoint = "/intrusion/actions/arm";
123 String url = "https://127.0.0.1:8444/smarthome/intrusion/actions/arm";
124 when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(url);
125 Request mockRequest = mock(Request.class);
126 when(httpClient.createRequest(anyString(), any(), any())).thenReturn(mockRequest);
127 ArmActionRequest request = new ArmActionRequest();
128 request.profileId = "0";
130 fixture.postAction(endpoint, request);
131 verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), same(request));
132 verify(mockRequest).send();
136 void initialAccessHttpClientOffline() {
137 fixture.initialAccess(httpClient);
141 void initialAccessHttpClientOnline() throws InterruptedException {
142 when(httpClient.isOnline()).thenReturn(true);
143 fixture.initialAccess(httpClient);
147 void initialAccessAccessPossible()
148 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
149 when(httpClient.isOnline()).thenReturn(true);
150 when(httpClient.isAccessPossible()).thenReturn(true);
151 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
152 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
154 // mock a request and response to obtain rooms
155 Request roomsRequest = mock(Request.class);
156 ContentResponse roomsResponse = mock(ContentResponse.class);
157 when(roomsResponse.getStatus()).thenReturn(200);
158 when(roomsResponse.getContentAsString()).thenReturn(
159 "[{\"@type\":\"room\",\"id\":\"hz_1\",\"iconId\":\"icon_room_bedroom\",\"name\":\"Bedroom\"}]");
160 when(roomsRequest.send()).thenReturn(roomsResponse);
161 when(httpClient.createRequest(contains("/rooms"), same(HttpMethod.GET))).thenReturn(roomsRequest);
163 // mock a request and response to obtain devices
164 Request devicesRequest = mock(Request.class);
165 ContentResponse devicesResponse = mock(ContentResponse.class);
166 when(devicesResponse.getStatus()).thenReturn(200);
167 when(devicesResponse.getContentAsString()).thenReturn("""
169 "rootDeviceId":"64-da-a0-02-14-9b",
170 "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
171 "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
172 "manufacturer":"BOSCH",
175 "serial":"3014F711A00004953859F31B",
177 "name":"Coffee Machine",
178 "status":"AVAILABLE",
182 when(devicesRequest.send()).thenReturn(devicesResponse);
183 when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
185 SubscribeResult subscribeResult = new SubscribeResult();
186 when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
188 Request longPollRequest = mock(Request.class);
189 when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
190 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
192 fixture.initialAccess(httpClient);
193 verify(thingHandlerCallback).statusUpdated(any(),
194 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
198 void getState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
199 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
200 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
201 Request request = mock(Request.class);
202 when(request.header(anyString(), anyString())).thenReturn(request);
203 ContentResponse response = mock(ContentResponse.class);
204 when(response.getStatus()).thenReturn(200);
205 when(response.getContentAsString()).thenReturn("""
207 "@type": "systemState",
208 "systemAvailability": {
209 "@type": "systemAvailabilityState",
214 "@type": "armingState",
215 "state": "SYSTEM_DISARMED",
219 "@type": "alarmState",
220 "value": "ALARM_OFF",
224 "activeConfigurationProfile": {
225 "@type": "activeConfigurationProfile",
228 "securityGapState": {
229 "@type": "securityGapState",
236 when(request.send()).thenReturn(response);
237 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
239 IntrusionDetectionSystemState state = fixture.getState("intrusion/states/system",
240 IntrusionDetectionSystemState.class);
241 assertNotNull(state);
242 assertTrue(state.systemAvailability.available);
243 assertSame(AlarmState.ALARM_OFF, state.alarmState.value);
244 assertSame(ArmingState.SYSTEM_DISARMED, state.armingState.state);
248 void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
249 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
250 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
251 when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod();
252 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
254 Request request = mock(Request.class);
255 when(request.header(anyString(), anyString())).thenReturn(request);
256 ContentResponse response = mock(ContentResponse.class);
257 when(response.getStatus()).thenReturn(200);
258 when(response.getContentAsString())
259 .thenReturn("{\n" + " \"@type\": \"shutterContactState\",\n" + " \"value\": \"OPEN\"\n" + " }");
260 when(request.send()).thenReturn(response);
261 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
263 ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
264 "ShutterContact", ShutterContactServiceState.class);
265 assertNotNull(state);
266 assertSame(ShutterContactState.OPEN, state.value);
270 void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
271 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
272 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
274 Request request = mock(Request.class);
275 when(request.header(anyString(), anyString())).thenReturn(request);
276 ContentResponse response = mock(ContentResponse.class);
277 when(response.getStatus()).thenReturn(200);
278 when(request.send()).thenReturn(response);
279 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
280 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
281 .thenReturn(DeviceTest.createTestDevice());
283 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
284 Device device = fixture.getDeviceInfo(deviceId);
285 assertEquals(deviceId, device.id);
289 void getDeviceInfoErrorCases()
290 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
291 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
292 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
294 Request request = mock(Request.class);
295 when(request.header(anyString(), anyString())).thenReturn(request);
296 ContentResponse response = mock(ContentResponse.class);
297 when(response.getStatus()).thenReturn(200);
298 when(request.send()).thenReturn(response);
299 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
301 @SuppressWarnings("unchecked")
302 ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
303 .forClass(BiFunction.class);
305 when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
306 .thenReturn(DeviceTest.createTestDevice());
308 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
309 fixture.getDeviceInfo(deviceId);
311 BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
312 Exception e = errorResponseHandler.apply(500,
313 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
315 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
318 e = errorResponseHandler.apply(404,
319 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
322 e = errorResponseHandler.apply(500, "");
323 assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
328 void getServiceData() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
329 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
330 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
331 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
333 Request request = mock(Request.class);
334 when(request.header(anyString(), anyString())).thenReturn(request);
335 ContentResponse response = mock(ContentResponse.class);
336 when(response.getStatus()).thenReturn(200);
337 when(response.getContentAsString()).thenReturn("""
339 "@type":"DeviceServiceData",
340 "path":"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel",
342 "deviceId":"hdm:ZigBee:000d6f0004b93361",
346 "type":"LOW_BATTERY",
353 when(request.send()).thenReturn(response);
354 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
356 DeviceServiceData serviceData = fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel");
357 assertNotNull(serviceData);
358 Faults faults = serviceData.faults;
359 assertNotNull(faults);
360 assertEquals("LOW_BATTERY", faults.entries.get(0).type);
364 void getServiceDataError() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
365 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
366 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
367 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
369 Request request = mock(Request.class);
370 when(request.header(anyString(), anyString())).thenReturn(request);
371 ContentResponse response = mock(ContentResponse.class);
372 when(response.getStatus()).thenReturn(500);
373 when(response.getContentAsString()).thenReturn(
374 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
375 when(request.send()).thenReturn(response);
376 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
377 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
378 .thenReturn(DeviceTest.createTestDevice());
380 BoschSHCException e = assertThrows(BoschSHCException.class,
381 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
383 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
388 void getServiceDataErrorNoRestExceptionResponse()
389 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
390 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
391 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
392 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
394 Request request = mock(Request.class);
395 when(request.header(anyString(), anyString())).thenReturn(request);
396 ContentResponse response = mock(ContentResponse.class);
397 when(response.getStatus()).thenReturn(500);
398 when(response.getContentAsString()).thenReturn("");
399 when(request.send()).thenReturn(response);
400 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
402 BoschSHCException e = assertThrows(BoschSHCException.class,
403 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
405 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
410 void putState() throws InterruptedException, TimeoutException, ExecutionException {
411 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
412 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
413 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
414 when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod();
416 Request request = mock(Request.class);
417 when(request.header(anyString(), anyString())).thenReturn(request);
418 ContentResponse response = mock(ContentResponse.class);
420 when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
421 .thenReturn(request);
422 when(request.send()).thenReturn(response);
424 BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
425 binarySwitchState.on = true;
426 fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
430 void getUserStateInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
431 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
432 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
433 String stateId = UUID.randomUUID().toString();
435 Request request = mock(Request.class);
436 when(request.header(anyString(), anyString())).thenReturn(request);
437 ContentResponse response = mock(ContentResponse.class);
438 when(response.getStatus()).thenReturn(200);
439 when(request.send()).thenReturn(response);
440 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
441 when(httpClient.sendRequest(same(request), same(UserDefinedState.class), any(), any()))
442 .thenReturn(UserDefinedStateTest.createTestState(stateId));
444 UserDefinedState userState = fixture.getUserStateInfo(stateId);
445 assertEquals(stateId, userState.getId());
449 void getUserStates() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
450 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
451 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
452 String stateId = UUID.randomUUID().toString();
454 Request request = mock(Request.class);
455 when(request.header(anyString(), anyString())).thenReturn(request);
456 ContentResponse response = mock(ContentResponse.class);
457 when(response.getStatus()).thenReturn(200);
458 when(request.send()).thenReturn(response);
459 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
460 when(response.getContentAsString()).thenReturn(
461 GsonUtils.DEFAULT_GSON_INSTANCE.toJson(List.of(UserDefinedStateTest.createTestState(stateId))));
463 List<UserDefinedState> userStates = fixture.getUserStates();
464 assertEquals(1, userStates.size());
468 void getUserStatesReturnsEmptyListIfRequestNotSuccessful()
469 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
470 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
471 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
473 Request request = mock(Request.class);
474 when(request.header(anyString(), anyString())).thenReturn(request);
475 ContentResponse response = mock(ContentResponse.class);
476 when(response.getStatus()).thenReturn(401);
477 when(request.send()).thenReturn(response);
478 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
480 List<UserDefinedState> userStates = fixture.getUserStates();
481 assertTrue(userStates.isEmpty());
485 void getUserStatesReturnsEmptyListIfExceptionHappened()
486 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
487 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
488 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
490 Request request = mock(Request.class);
491 when(request.header(anyString(), anyString())).thenReturn(request);
492 ContentResponse response = mock(ContentResponse.class);
493 when(response.getStatus()).thenReturn(401);
494 when(request.send()).thenThrow(new TimeoutException("text exception"));
495 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
497 List<UserDefinedState> userStates = fixture.getUserStates();
498 assertTrue(userStates.isEmpty());
502 void afterEach() throws Exception {