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("[{\"@type\":\"device\",\r\n"
163 + " \"rootDeviceId\":\"64-da-a0-02-14-9b\",\r\n"
164 + " \"id\":\"hdm:HomeMaticIP:3014F711A00004953859F31B\",\r\n"
165 + " \"deviceServiceIds\":[\"PowerMeter\",\"PowerSwitch\",\"PowerSwitchProgram\",\"Routing\"],\r\n"
166 + " \"manufacturer\":\"BOSCH\",\r\n" + " \"roomId\":\"hz_3\",\r\n" + " \"deviceModel\":\"PSM\",\r\n"
167 + " \"serial\":\"3014F711A00004953859F31B\",\r\n" + " \"profile\":\"GENERIC\",\r\n"
168 + " \"name\":\"Coffee Machine\",\r\n" + " \"status\":\"AVAILABLE\",\r\n" + " \"childDeviceIds\":[]\r\n"
170 when(devicesRequest.send()).thenReturn(devicesResponse);
171 when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
173 SubscribeResult subscribeResult = new SubscribeResult();
174 when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
176 Request longPollRequest = mock(Request.class);
177 when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
178 argThat((JsonRpcRequest r) -> r.method.equals("RE/longPoll")))).thenReturn(longPollRequest);
180 fixture.initialAccess(httpClient);
181 verify(thingHandlerCallback).statusUpdated(any(),
182 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
186 void getState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
187 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
188 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
189 Request request = mock(Request.class);
190 when(request.header(anyString(), anyString())).thenReturn(request);
191 ContentResponse response = mock(ContentResponse.class);
192 when(response.getStatus()).thenReturn(200);
193 when(response.getContentAsString()).thenReturn("{\r\n" + " \"@type\": \"systemState\",\r\n"
194 + " \"systemAvailability\": {\r\n" + " \"@type\": \"systemAvailabilityState\",\r\n"
195 + " \"available\": true,\r\n" + " \"deleted\": false\r\n" + " },\r\n"
196 + " \"armingState\": {\r\n" + " \"@type\": \"armingState\",\r\n"
197 + " \"state\": \"SYSTEM_DISARMED\",\r\n" + " \"deleted\": false\r\n" + " },\r\n"
198 + " \"alarmState\": {\r\n" + " \"@type\": \"alarmState\",\r\n"
199 + " \"value\": \"ALARM_OFF\",\r\n" + " \"incidents\": [],\r\n"
200 + " \"deleted\": false\r\n" + " },\r\n" + " \"activeConfigurationProfile\": {\r\n"
201 + " \"@type\": \"activeConfigurationProfile\",\r\n" + " \"deleted\": false\r\n"
202 + " },\r\n" + " \"securityGapState\": {\r\n" + " \"@type\": \"securityGapState\",\r\n"
203 + " \"securityGaps\": [],\r\n" + " \"deleted\": false\r\n" + " },\r\n"
204 + " \"deleted\": false\r\n" + " }");
205 when(request.send()).thenReturn(response);
206 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
208 IntrusionDetectionSystemState state = fixture.getState("intrusion/states/system",
209 IntrusionDetectionSystemState.class);
210 assertNotNull(state);
211 assertTrue(state.systemAvailability.available);
212 assertSame(AlarmState.ALARM_OFF, state.alarmState.value);
213 assertSame(ArmingState.SYSTEM_DISARMED, state.armingState.state);
217 void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
218 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
219 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
220 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
222 Request request = mock(Request.class);
223 when(request.header(anyString(), anyString())).thenReturn(request);
224 ContentResponse response = mock(ContentResponse.class);
225 when(response.getStatus()).thenReturn(200);
226 when(response.getContentAsString())
227 .thenReturn("{\n" + " \"@type\": \"shutterContactState\",\n" + " \"value\": \"OPEN\"\n" + " }");
228 when(request.send()).thenReturn(response);
229 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
231 ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
232 "ShutterContact", ShutterContactServiceState.class);
233 assertNotNull(state);
234 assertSame(ShutterContactState.OPEN, state.value);
238 void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
239 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
240 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
242 Request request = mock(Request.class);
243 when(request.header(anyString(), anyString())).thenReturn(request);
244 ContentResponse response = mock(ContentResponse.class);
245 when(response.getStatus()).thenReturn(200);
246 when(request.send()).thenReturn(response);
247 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
248 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
249 .thenReturn(DeviceTest.createTestDevice());
251 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
252 Device device = fixture.getDeviceInfo(deviceId);
253 assertEquals(deviceId, device.id);
257 void getDeviceInfoErrorCases()
258 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
259 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
260 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
262 Request request = mock(Request.class);
263 when(request.header(anyString(), anyString())).thenReturn(request);
264 ContentResponse response = mock(ContentResponse.class);
265 when(response.getStatus()).thenReturn(200);
266 when(request.send()).thenReturn(response);
267 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
269 @SuppressWarnings("unchecked")
270 ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
271 .forClass(BiFunction.class);
273 when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
274 .thenReturn(DeviceTest.createTestDevice());
276 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
277 fixture.getDeviceInfo(deviceId);
279 BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
280 Exception e = errorResponseHandler.apply(500,
281 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
283 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
286 e = errorResponseHandler.apply(404,
287 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
290 e = errorResponseHandler.apply(500, "");
291 assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
296 void getServiceData() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
297 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
298 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
299 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
301 Request request = mock(Request.class);
302 when(request.header(anyString(), anyString())).thenReturn(request);
303 ContentResponse response = mock(ContentResponse.class);
304 when(response.getStatus()).thenReturn(200);
305 when(response.getContentAsString()).thenReturn("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
306 + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
307 + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
308 + " \"faults\":{ \n" + " \"entries\":[\n" + " {\n"
309 + " \"type\":\"LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n" + " }\n"
310 + " ]\n" + " }\n" + "}");
311 when(request.send()).thenReturn(response);
312 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
314 DeviceServiceData serviceData = fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel");
315 assertNotNull(serviceData);
316 Faults faults = serviceData.faults;
317 assertNotNull(faults);
318 assertEquals("LOW_BATTERY", faults.entries.get(0).type);
322 void getServiceDataError() 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(500);
331 when(response.getContentAsString()).thenReturn(
332 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
333 when(request.send()).thenReturn(response);
334 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
335 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
336 .thenReturn(DeviceTest.createTestDevice());
338 BoschSHCException e = assertThrows(BoschSHCException.class,
339 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
341 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
346 void getServiceDataErrorNoRestExceptionResponse()
347 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
348 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
349 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
350 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
352 Request request = mock(Request.class);
353 when(request.header(anyString(), anyString())).thenReturn(request);
354 ContentResponse response = mock(ContentResponse.class);
355 when(response.getStatus()).thenReturn(500);
356 when(response.getContentAsString()).thenReturn("");
357 when(request.send()).thenReturn(response);
358 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
360 BoschSHCException e = assertThrows(BoschSHCException.class,
361 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
363 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
368 void putState() throws InterruptedException, TimeoutException, ExecutionException {
369 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
370 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
371 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
373 Request request = mock(Request.class);
374 when(request.header(anyString(), anyString())).thenReturn(request);
375 ContentResponse response = mock(ContentResponse.class);
377 when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
378 .thenReturn(request);
379 when(request.send()).thenReturn(response);
381 BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
382 binarySwitchState.on = true;
383 fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
387 void afterEach() throws Exception {