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.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.junit.jupiter.api.AfterEach;
34 import org.junit.jupiter.api.BeforeAll;
35 import org.junit.jupiter.api.BeforeEach;
36 import org.junit.jupiter.api.Test;
37 import org.mockito.ArgumentCaptor;
38 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
39 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
40 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
41 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
42 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
43 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
44 import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState;
45 import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.ArmActionRequest;
46 import org.openhab.binding.boschshc.internal.services.intrusion.dto.AlarmState;
47 import org.openhab.binding.boschshc.internal.services.intrusion.dto.ArmingState;
48 import org.openhab.binding.boschshc.internal.services.intrusion.dto.IntrusionDetectionSystemState;
49 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
50 import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
51 import org.openhab.core.config.core.Configuration;
52 import org.openhab.core.thing.Bridge;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.ThingHandlerCallback;
57 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
60 * Unit tests for the {@link BridgeHandler}.
62 * @author David Pace - Initial contribution
66 class BridgeHandlerTest {
68 private @NonNullByDefault({}) BridgeHandler fixture;
70 private @NonNullByDefault({}) BoschHttpClient httpClient;
72 private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
75 static void beforeAll() throws IOException {
76 Path mavenTargetFolder = Paths.get("target");
77 assertTrue(Files.exists(mavenTargetFolder), "Maven target folder does not exist.");
78 System.setProperty("openhab.userdata", mavenTargetFolder.toFile().getAbsolutePath());
79 Path etc = mavenTargetFolder.resolve("etc");
80 if (!Files.exists(etc)) {
81 Files.createDirectory(etc);
86 void beforeEach() throws Exception {
87 Bridge bridge = mock(Bridge.class);
88 fixture = new BridgeHandler(bridge);
90 thingHandlerCallback = mock(ThingHandlerCallback.class);
91 fixture.setCallback(thingHandlerCallback);
93 Configuration bridgeConfiguration = new Configuration();
94 Map<String, Object> properties = new HashMap<>();
95 properties.put("ipAddress", "localhost");
96 properties.put("password", "test");
97 bridgeConfiguration.setProperties(properties);
99 Thing thing = mock(Bridge.class);
100 when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
101 // this calls initialize() as well
102 fixture.thingUpdated(thing);
104 // shut down the real HTTP client
105 if (fixture.httpClient != null) {
106 fixture.httpClient.stop();
109 // use a mocked HTTP client
110 httpClient = mock(BoschHttpClient.class);
111 fixture.httpClient = httpClient;
115 void postAction() throws InterruptedException, TimeoutException, ExecutionException {
116 String endpoint = "/intrusion/actions/arm";
117 String url = "https://127.0.0.1:8444/smarthome/intrusion/actions/arm";
118 when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(url);
119 Request mockRequest = mock(Request.class);
120 when(httpClient.createRequest(anyString(), any(), any())).thenReturn(mockRequest);
121 ArmActionRequest request = new ArmActionRequest();
122 request.profileId = "0";
124 fixture.postAction(endpoint, request);
125 verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), same(request));
126 verify(mockRequest).send();
130 void initialAccessHttpClientOffline() {
131 fixture.initialAccess(httpClient);
135 void initialAccessHttpClientOnline() throws InterruptedException {
136 when(httpClient.isOnline()).thenReturn(true);
137 fixture.initialAccess(httpClient);
141 void initialAccessAccessPossible()
142 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
143 when(httpClient.isOnline()).thenReturn(true);
144 when(httpClient.isAccessPossible()).thenReturn(true);
145 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
146 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
148 // mock a request and response to obtain rooms
149 Request roomsRequest = mock(Request.class);
150 ContentResponse roomsResponse = mock(ContentResponse.class);
151 when(roomsResponse.getStatus()).thenReturn(200);
152 when(roomsResponse.getContentAsString()).thenReturn(
153 "[{\"@type\":\"room\",\"id\":\"hz_1\",\"iconId\":\"icon_room_bedroom\",\"name\":\"Bedroom\"}]");
154 when(roomsRequest.send()).thenReturn(roomsResponse);
155 when(httpClient.createRequest(contains("/rooms"), same(HttpMethod.GET))).thenReturn(roomsRequest);
157 // mock a request and response to obtain devices
158 Request devicesRequest = mock(Request.class);
159 ContentResponse devicesResponse = mock(ContentResponse.class);
160 when(devicesResponse.getStatus()).thenReturn(200);
161 when(devicesResponse.getContentAsString()).thenReturn("[{\"@type\":\"device\",\r\n"
162 + " \"rootDeviceId\":\"64-da-a0-02-14-9b\",\r\n"
163 + " \"id\":\"hdm:HomeMaticIP:3014F711A00004953859F31B\",\r\n"
164 + " \"deviceServiceIds\":[\"PowerMeter\",\"PowerSwitch\",\"PowerSwitchProgram\",\"Routing\"],\r\n"
165 + " \"manufacturer\":\"BOSCH\",\r\n" + " \"roomId\":\"hz_3\",\r\n" + " \"deviceModel\":\"PSM\",\r\n"
166 + " \"serial\":\"3014F711A00004953859F31B\",\r\n" + " \"profile\":\"GENERIC\",\r\n"
167 + " \"name\":\"Coffee Machine\",\r\n" + " \"status\":\"AVAILABLE\",\r\n" + " \"childDeviceIds\":[]\r\n"
169 when(devicesRequest.send()).thenReturn(devicesResponse);
170 when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
172 SubscribeResult subscribeResult = new SubscribeResult();
173 when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
175 Request longPollRequest = mock(Request.class);
176 when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
177 argThat((JsonRpcRequest r) -> r.method.equals("RE/longPoll")))).thenReturn(longPollRequest);
179 fixture.initialAccess(httpClient);
180 verify(thingHandlerCallback).statusUpdated(any(),
181 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
185 void getState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
186 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
187 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
188 Request request = mock(Request.class);
189 when(request.header(anyString(), anyString())).thenReturn(request);
190 ContentResponse response = mock(ContentResponse.class);
191 when(response.getStatus()).thenReturn(200);
192 when(response.getContentAsString()).thenReturn("{\r\n" + " \"@type\": \"systemState\",\r\n"
193 + " \"systemAvailability\": {\r\n" + " \"@type\": \"systemAvailabilityState\",\r\n"
194 + " \"available\": true,\r\n" + " \"deleted\": false\r\n" + " },\r\n"
195 + " \"armingState\": {\r\n" + " \"@type\": \"armingState\",\r\n"
196 + " \"state\": \"SYSTEM_DISARMED\",\r\n" + " \"deleted\": false\r\n" + " },\r\n"
197 + " \"alarmState\": {\r\n" + " \"@type\": \"alarmState\",\r\n"
198 + " \"value\": \"ALARM_OFF\",\r\n" + " \"incidents\": [],\r\n"
199 + " \"deleted\": false\r\n" + " },\r\n" + " \"activeConfigurationProfile\": {\r\n"
200 + " \"@type\": \"activeConfigurationProfile\",\r\n" + " \"deleted\": false\r\n"
201 + " },\r\n" + " \"securityGapState\": {\r\n" + " \"@type\": \"securityGapState\",\r\n"
202 + " \"securityGaps\": [],\r\n" + " \"deleted\": false\r\n" + " },\r\n"
203 + " \"deleted\": false\r\n" + " }");
204 when(request.send()).thenReturn(response);
205 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
207 IntrusionDetectionSystemState state = fixture.getState("intrusion/states/system",
208 IntrusionDetectionSystemState.class);
209 assertNotNull(state);
210 assertTrue(state.systemAvailability.available);
211 assertSame(AlarmState.ALARM_OFF, state.alarmState.value);
212 assertSame(ArmingState.SYSTEM_DISARMED, state.armingState.state);
216 void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
217 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
218 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
219 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
221 Request request = mock(Request.class);
222 when(request.header(anyString(), anyString())).thenReturn(request);
223 ContentResponse response = mock(ContentResponse.class);
224 when(response.getStatus()).thenReturn(200);
225 when(response.getContentAsString())
226 .thenReturn("{\n" + " \"@type\": \"shutterContactState\",\n" + " \"value\": \"OPEN\"\n" + " }");
227 when(request.send()).thenReturn(response);
228 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
230 ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
231 "ShutterContact", ShutterContactServiceState.class);
232 assertNotNull(state);
233 assertSame(ShutterContactState.OPEN, state.value);
237 void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
238 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
239 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
241 Request request = mock(Request.class);
242 when(request.header(anyString(), anyString())).thenReturn(request);
243 ContentResponse response = mock(ContentResponse.class);
244 when(response.getStatus()).thenReturn(200);
245 when(request.send()).thenReturn(response);
246 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
247 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
248 .thenReturn(DeviceTest.createTestDevice());
250 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
251 Device device = fixture.getDeviceInfo(deviceId);
252 assertEquals(deviceId, device.id);
256 void getDeviceInfoErrorCases()
257 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
258 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
259 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
261 Request request = mock(Request.class);
262 when(request.header(anyString(), anyString())).thenReturn(request);
263 ContentResponse response = mock(ContentResponse.class);
264 when(response.getStatus()).thenReturn(200);
265 when(request.send()).thenReturn(response);
266 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
268 @SuppressWarnings("unchecked")
269 ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
270 .forClass(BiFunction.class);
272 when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
273 .thenReturn(DeviceTest.createTestDevice());
275 String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
276 fixture.getDeviceInfo(deviceId);
278 BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
279 Exception e = errorResponseHandler.apply(500,
280 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
282 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
285 e = errorResponseHandler.apply(404,
286 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
289 e = errorResponseHandler.apply(500, "");
290 assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
295 void getServiceData() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
296 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
297 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
298 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
300 Request request = mock(Request.class);
301 when(request.header(anyString(), anyString())).thenReturn(request);
302 ContentResponse response = mock(ContentResponse.class);
303 when(response.getStatus()).thenReturn(200);
304 when(response.getContentAsString()).thenReturn("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
305 + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
306 + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
307 + " \"faults\":{ \n" + " \"entries\":[\n" + " {\n"
308 + " \"type\":\"LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n" + " }\n"
309 + " ]\n" + " }\n" + "}");
310 when(request.send()).thenReturn(response);
311 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
313 DeviceServiceData serviceData = fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel");
314 assertNotNull(serviceData);
315 Faults faults = serviceData.faults;
316 assertNotNull(faults);
317 assertEquals("LOW_BATTERY", faults.entries.get(0).type);
321 void getServiceDataError() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
322 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
323 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
324 when(httpClient.getServiceUrl(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(500);
330 when(response.getContentAsString()).thenReturn(
331 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
332 when(request.send()).thenReturn(response);
333 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
334 when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
335 .thenReturn(DeviceTest.createTestDevice());
337 BoschSHCException e = assertThrows(BoschSHCException.class,
338 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
340 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
345 void getServiceDataErrorNoRestExceptionResponse()
346 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
347 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
348 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
349 when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
351 Request request = mock(Request.class);
352 when(request.header(anyString(), anyString())).thenReturn(request);
353 ContentResponse response = mock(ContentResponse.class);
354 when(response.getStatus()).thenReturn(500);
355 when(response.getContentAsString()).thenReturn("");
356 when(request.send()).thenReturn(response);
357 when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
359 BoschSHCException e = assertThrows(BoschSHCException.class,
360 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
362 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
367 void putState() throws InterruptedException, TimeoutException, ExecutionException {
368 when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
369 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
370 when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
372 Request request = mock(Request.class);
373 when(request.header(anyString(), anyString())).thenReturn(request);
374 ContentResponse response = mock(ContentResponse.class);
376 when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
377 .thenReturn(request);
378 when(request.send()).thenReturn(response);
380 BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
381 binarySwitchState.on = true;
382 fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
386 void afterEach() throws Exception {