2 * Copyright (c) 2010-2023 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;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeoutException;
27 import java.util.function.BiFunction;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.client.api.Request;
33 import org.eclipse.jetty.http.HttpMethod;
34 import org.junit.jupiter.api.AfterEach;
35 import org.junit.jupiter.api.BeforeAll;
36 import org.junit.jupiter.api.BeforeEach;
37 import org.junit.jupiter.api.Test;
38 import org.mockito.ArgumentCaptor;
39 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
40 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
41 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
42 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
43 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
44 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
45 import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState;
46 import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.ArmActionRequest;
47 import org.openhab.binding.boschshc.internal.services.intrusion.dto.AlarmState;
48 import org.openhab.binding.boschshc.internal.services.intrusion.dto.ArmingState;
49 import org.openhab.binding.boschshc.internal.services.intrusion.dto.IntrusionDetectionSystemState;
50 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
51 import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
52 import org.openhab.core.config.core.Configuration;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.ThingHandlerCallback;
58 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
61 * Unit tests for the {@link BridgeHandler}.
63 * @author David Pace - Initial contribution
67 class BridgeHandlerTest {
69 private @NonNullByDefault({}) BridgeHandler fixture;
71 private @NonNullByDefault({}) BoschHttpClient httpClient;
73 private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
76 static void beforeAll() throws IOException {
77 Path mavenTargetFolder = Paths.get("target");
78 assertTrue(Files.exists(mavenTargetFolder), "Maven target folder does not exist.");
79 System.setProperty("openhab.userdata", mavenTargetFolder.toFile().getAbsolutePath());
80 Path etc = mavenTargetFolder.resolve("etc");
81 if (!Files.exists(etc)) {
82 Files.createDirectory(etc);
87 void beforeEach() throws Exception {
88 Bridge bridge = mock(Bridge.class);
89 fixture = new BridgeHandler(bridge);
91 thingHandlerCallback = mock(ThingHandlerCallback.class);
92 fixture.setCallback(thingHandlerCallback);
94 Configuration bridgeConfiguration = new Configuration();
95 Map<@Nullable String, @Nullable Object> properties = new HashMap<>();
96 properties.put("ipAddress", "localhost");
97 properties.put("password", "test");
98 bridgeConfiguration.setProperties(properties);
100 Thing thing = mock(Bridge.class);
101 when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
102 // this calls initialize() as well
103 fixture.thingUpdated(thing);
105 // shut down the real HTTP client
106 if (fixture.httpClient != null) {
107 fixture.httpClient.stop();
110 // use a mocked HTTP client
111 httpClient = mock(BoschHttpClient.class);
112 fixture.httpClient = httpClient;
116 void postAction() throws InterruptedException, TimeoutException, ExecutionException {
117 String endpoint = "/intrusion/actions/arm";
118 String url = "https://127.0.0.1:8444/smarthome/intrusion/actions/arm";
119 when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(url);
120 Request mockRequest = mock(Request.class);
121 when(httpClient.createRequest(anyString(), any(), any())).thenReturn(mockRequest);
122 ArmActionRequest request = new ArmActionRequest();
123 request.profileId = "0";
125 fixture.postAction(endpoint, request);
126 verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), same(request));
127 verify(mockRequest).send();
131 void initialAccessHttpClientOffline() {
132 fixture.initialAccess(httpClient);
136 void initialAccessHttpClientOnline() throws InterruptedException {
137 when(httpClient.isOnline()).thenReturn(true);
138 fixture.initialAccess(httpClient);
142 void initialAccessAccessPossible()
143 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
144 when(httpClient.isOnline()).thenReturn(true);
145 when(httpClient.isAccessPossible()).thenReturn(true);
146 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
147 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
149 // mock a request and response to obtain rooms
150 Request roomsRequest = mock(Request.class);
151 ContentResponse roomsResponse = mock(ContentResponse.class);
152 when(roomsResponse.getStatus()).thenReturn(200);
153 when(roomsResponse.getContentAsString()).thenReturn(
154 "[{\"@type\":\"room\",\"id\":\"hz_1\",\"iconId\":\"icon_room_bedroom\",\"name\":\"Bedroom\"}]");
155 when(roomsRequest.send()).thenReturn(roomsResponse);
156 when(httpClient.createRequest(contains("/rooms"), same(HttpMethod.GET))).thenReturn(roomsRequest);
158 // mock a request and response to obtain devices
159 Request devicesRequest = mock(Request.class);
160 ContentResponse devicesResponse = mock(ContentResponse.class);
161 when(devicesResponse.getStatus()).thenReturn(200);
162 when(devicesResponse.getContentAsString()).thenReturn("""
164 "rootDeviceId":"64-da-a0-02-14-9b",
165 "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
166 "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
167 "manufacturer":"BOSCH",
170 "serial":"3014F711A00004953859F31B",
172 "name":"Coffee Machine",
173 "status":"AVAILABLE",
177 when(devicesRequest.send()).thenReturn(devicesResponse);
178 when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
180 SubscribeResult subscribeResult = new SubscribeResult();
181 when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
183 Request longPollRequest = mock(Request.class);
184 when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
185 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
187 fixture.initialAccess(httpClient);
188 verify(thingHandlerCallback).statusUpdated(any(),
189 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
193 void getState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
194 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
195 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
196 Request request = mock(Request.class);
197 when(request.header(anyString(), anyString())).thenReturn(request);
198 ContentResponse response = mock(ContentResponse.class);
199 when(response.getStatus()).thenReturn(200);
200 when(response.getContentAsString()).thenReturn("""
202 "@type": "systemState",
203 "systemAvailability": {
204 "@type": "systemAvailabilityState",
209 "@type": "armingState",
210 "state": "SYSTEM_DISARMED",
214 "@type": "alarmState",
215 "value": "ALARM_OFF",
219 "activeConfigurationProfile": {
220 "@type": "activeConfigurationProfile",
223 "securityGapState": {
224 "@type": "securityGapState",
231 when(request.send()).thenReturn(response);
232 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
234 IntrusionDetectionSystemState state = fixture.getState("intrusion/states/system",
235 IntrusionDetectionSystemState.class);
236 assertNotNull(state);
237 assertTrue(state.systemAvailability.available);
238 assertSame(AlarmState.ALARM_OFF, state.alarmState.value);
239 assertSame(ArmingState.SYSTEM_DISARMED, state.armingState.state);
243 void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
244 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
245 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
246 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
248 Request request = mock(Request.class);
249 when(request.header(anyString(), anyString())).thenReturn(request);
250 ContentResponse response = mock(ContentResponse.class);
251 when(response.getStatus()).thenReturn(200);
252 when(response.getContentAsString())
253 .thenReturn("{\n" + " \"@type\": \"shutterContactState\",\n" + " \"value\": \"OPEN\"\n" + " }");
254 when(request.send()).thenReturn(response);
255 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
257 ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
258 "ShutterContact", ShutterContactServiceState.class);
259 assertNotNull(state);
260 assertSame(ShutterContactState.OPEN, state.value);
264 void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
265 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
266 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
268 Request request = mock(Request.class);
269 when(request.header(anyString(), anyString())).thenReturn(request);
270 ContentResponse response = mock(ContentResponse.class);
271 when(response.getStatus()).thenReturn(200);
272 when(request.send()).thenReturn(response);
273 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
274 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
275 .thenReturn(DeviceTest.createTestDevice());
277 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
278 Device device = fixture.getDeviceInfo(deviceId);
279 assertEquals(deviceId, device.id);
283 void getDeviceInfoErrorCases()
284 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
285 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
286 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
288 Request request = mock(Request.class);
289 when(request.header(anyString(), anyString())).thenReturn(request);
290 ContentResponse response = mock(ContentResponse.class);
291 when(response.getStatus()).thenReturn(200);
292 when(request.send()).thenReturn(response);
293 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
295 @SuppressWarnings("unchecked")
296 ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
297 .forClass(BiFunction.class);
299 when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
300 .thenReturn(DeviceTest.createTestDevice());
302 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
303 fixture.getDeviceInfo(deviceId);
305 BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
306 Exception e = errorResponseHandler.apply(500,
307 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
309 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
312 e = errorResponseHandler.apply(404,
313 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
316 e = errorResponseHandler.apply(500, "");
317 assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
322 void getServiceData() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
323 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
324 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
325 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
327 Request request = mock(Request.class);
328 when(request.header(anyString(), anyString())).thenReturn(request);
329 ContentResponse response = mock(ContentResponse.class);
330 when(response.getStatus()).thenReturn(200);
331 when(response.getContentAsString()).thenReturn("""
333 "@type":"DeviceServiceData",
334 "path":"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel",
336 "deviceId":"hdm:ZigBee:000d6f0004b93361",
340 "type":"LOW_BATTERY",
347 when(request.send()).thenReturn(response);
348 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
350 DeviceServiceData serviceData = fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel");
351 assertNotNull(serviceData);
352 Faults faults = serviceData.faults;
353 assertNotNull(faults);
354 assertEquals("LOW_BATTERY", faults.entries.get(0).type);
358 void getServiceDataError() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
359 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
360 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
361 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
363 Request request = mock(Request.class);
364 when(request.header(anyString(), anyString())).thenReturn(request);
365 ContentResponse response = mock(ContentResponse.class);
366 when(response.getStatus()).thenReturn(500);
367 when(response.getContentAsString()).thenReturn(
368 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
369 when(request.send()).thenReturn(response);
370 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
371 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
372 .thenReturn(DeviceTest.createTestDevice());
374 BoschSHCException e = assertThrows(BoschSHCException.class,
375 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
377 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
382 void getServiceDataErrorNoRestExceptionResponse()
383 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
384 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
385 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
386 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
388 Request request = mock(Request.class);
389 when(request.header(anyString(), anyString())).thenReturn(request);
390 ContentResponse response = mock(ContentResponse.class);
391 when(response.getStatus()).thenReturn(500);
392 when(response.getContentAsString()).thenReturn("");
393 when(request.send()).thenReturn(response);
394 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
396 BoschSHCException e = assertThrows(BoschSHCException.class,
397 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
399 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
404 void putState() throws InterruptedException, TimeoutException, ExecutionException {
405 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
406 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
407 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
409 Request request = mock(Request.class);
410 when(request.header(anyString(), anyString())).thenReturn(request);
411 ContentResponse response = mock(ContentResponse.class);
413 when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
414 .thenReturn(request);
415 when(request.send()).thenReturn(response);
417 BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
418 binarySwitchState.on = true;
419 fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
423 void afterEach() throws Exception {