]> git.basschouten.com Git - openhab-addons.git/blob
a6be12174ecba3f9c392b4cd7ac64bcecea9b3c9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.boschshc.internal.devices.bridge;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.ArgumentMatchers.*;
17 import static org.mockito.Mockito.*;
18
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.Map;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeoutException;
27 import java.util.function.BiFunction;
28
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;
59
60 /**
61  * Unit tests for the {@link BridgeHandler}.
62  *
63  * @author David Pace - Initial contribution
64  *
65  */
66 @NonNullByDefault
67 class BridgeHandlerTest {
68
69     private @NonNullByDefault({}) BridgeHandler fixture;
70
71     private @NonNullByDefault({}) BoschHttpClient httpClient;
72
73     private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
74
75     @BeforeAll
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);
83         }
84     }
85
86     @BeforeEach
87     void beforeEach() throws Exception {
88         Bridge bridge = mock(Bridge.class);
89         fixture = new BridgeHandler(bridge);
90
91         thingHandlerCallback = mock(ThingHandlerCallback.class);
92         fixture.setCallback(thingHandlerCallback);
93
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);
99
100         Thing thing = mock(Bridge.class);
101         when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
102         // this calls initialize() as well
103         fixture.thingUpdated(thing);
104
105         // shut down the real HTTP client
106         if (fixture.httpClient != null) {
107             fixture.httpClient.stop();
108         }
109
110         // use a mocked HTTP client
111         httpClient = mock(BoschHttpClient.class);
112         fixture.httpClient = httpClient;
113     }
114
115     @Test
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";
124
125         fixture.postAction(endpoint, request);
126         verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), same(request));
127         verify(mockRequest).send();
128     }
129
130     @Test
131     void initialAccessHttpClientOffline() {
132         fixture.initialAccess(httpClient);
133     }
134
135     @Test
136     void initialAccessHttpClientOnline() throws InterruptedException {
137         when(httpClient.isOnline()).thenReturn(true);
138         fixture.initialAccess(httpClient);
139     }
140
141     @Test
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();
148
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);
157
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"
169                 + " }]");
170         when(devicesRequest.send()).thenReturn(devicesResponse);
171         when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
172
173         SubscribeResult subscribeResult = new SubscribeResult();
174         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
175
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);
179
180         fixture.initialAccess(httpClient);
181         verify(thingHandlerCallback).statusUpdated(any(),
182                 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
183     }
184
185     @Test
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);
207
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);
214     }
215
216     @Test
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();
221
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);
230
231         ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
232                 "ShutterContact", ShutterContactServiceState.class);
233         assertNotNull(state);
234         assertSame(ShutterContactState.OPEN, state.value);
235     }
236
237     @Test
238     void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
239         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
240         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
241
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());
250
251         String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
252         Device device = fixture.getDeviceInfo(deviceId);
253         assertEquals(deviceId, device.id);
254     }
255
256     @Test
257     void getDeviceInfoErrorCases()
258             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
259         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
260         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
261
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);
268
269         @SuppressWarnings("unchecked")
270         ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
271                 .forClass(BiFunction.class);
272
273         when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
274                 .thenReturn(DeviceTest.createTestDevice());
275
276         String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
277         fixture.getDeviceInfo(deviceId);
278
279         BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
280         Exception e = errorResponseHandler.apply(500,
281                 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
282         assertEquals(
283                 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
284                 e.getMessage());
285
286         e = errorResponseHandler.apply(404,
287                 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
288         assertNotNull(e);
289
290         e = errorResponseHandler.apply(500, "");
291         assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
292                 e.getMessage());
293     }
294
295     @Test
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();
300
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);
313
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);
319     }
320
321     @Test
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();
326
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());
337
338         BoschSHCException e = assertThrows(BoschSHCException.class,
339                 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
340         assertEquals(
341                 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
342                 e.getMessage());
343     }
344
345     @Test
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();
351
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);
359
360         BoschSHCException e = assertThrows(BoschSHCException.class,
361                 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
362         assertEquals(
363                 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
364                 e.getMessage());
365     }
366
367     @Test
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();
372
373         Request request = mock(Request.class);
374         when(request.header(anyString(), anyString())).thenReturn(request);
375         ContentResponse response = mock(ContentResponse.class);
376
377         when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
378                 .thenReturn(request);
379         when(request.send()).thenReturn(response);
380
381         BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
382         binarySwitchState.on = true;
383         fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
384     }
385
386     @AfterEach
387     void afterEach() throws Exception {
388         fixture.dispose();
389     }
390 }