]> git.basschouten.com Git - openhab-addons.git/blob
bd71f83b376fcd6585023da984037e4fe8a3f8cb
[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("""
163                 [{"@type":"device",
164                  "rootDeviceId":"64-da-a0-02-14-9b",
165                  "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
166                  "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
167                  "manufacturer":"BOSCH",
168                  "roomId":"hz_3",
169                  "deviceModel":"PSM",
170                  "serial":"3014F711A00004953859F31B",
171                  "profile":"GENERIC",
172                  "name":"Coffee Machine",
173                  "status":"AVAILABLE",
174                  "childDeviceIds":[]
175                  }]\
176                 """);
177         when(devicesRequest.send()).thenReturn(devicesResponse);
178         when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
179
180         SubscribeResult subscribeResult = new SubscribeResult();
181         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
182
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);
186
187         fixture.initialAccess(httpClient);
188         verify(thingHandlerCallback).statusUpdated(any(),
189                 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
190     }
191
192     @Test
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("""
201                 {
202                      "@type": "systemState",
203                      "systemAvailability": {
204                          "@type": "systemAvailabilityState",
205                          "available": true,
206                          "deleted": false
207                      },
208                      "armingState": {
209                          "@type": "armingState",
210                          "state": "SYSTEM_DISARMED",
211                          "deleted": false
212                      },
213                      "alarmState": {
214                          "@type": "alarmState",
215                          "value": "ALARM_OFF",
216                          "incidents": [],
217                          "deleted": false
218                      },
219                      "activeConfigurationProfile": {
220                          "@type": "activeConfigurationProfile",
221                          "deleted": false
222                      },
223                      "securityGapState": {
224                          "@type": "securityGapState",
225                          "securityGaps": [],
226                          "deleted": false
227                      },
228                      "deleted": false
229                  }\
230                 """);
231         when(request.send()).thenReturn(response);
232         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
233
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);
240     }
241
242     @Test
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();
247
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);
256
257         ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
258                 "ShutterContact", ShutterContactServiceState.class);
259         assertNotNull(state);
260         assertSame(ShutterContactState.OPEN, state.value);
261     }
262
263     @Test
264     void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
265         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
266         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
267
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());
276
277         String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
278         Device device = fixture.getDeviceInfo(deviceId);
279         assertEquals(deviceId, device.id);
280     }
281
282     @Test
283     void getDeviceInfoErrorCases()
284             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
285         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
286         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
287
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);
294
295         @SuppressWarnings("unchecked")
296         ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
297                 .forClass(BiFunction.class);
298
299         when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
300                 .thenReturn(DeviceTest.createTestDevice());
301
302         String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
303         fixture.getDeviceInfo(deviceId);
304
305         BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
306         Exception e = errorResponseHandler.apply(500,
307                 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
308         assertEquals(
309                 "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
310                 e.getMessage());
311
312         e = errorResponseHandler.apply(404,
313                 "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
314         assertNotNull(e);
315
316         e = errorResponseHandler.apply(500, "");
317         assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
318                 e.getMessage());
319     }
320
321     @Test
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();
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(200);
331         when(response.getContentAsString()).thenReturn("""
332                 {
333                     "@type":"DeviceServiceData",
334                     "path":"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel",
335                     "id":"BatteryLevel",
336                     "deviceId":"hdm:ZigBee:000d6f0004b93361",
337                     "faults":{\s
338                         "entries":[
339                           {
340                             "type":"LOW_BATTERY",
341                             "category":"WARNING"
342                           }
343                         ]
344                     }
345                 }\
346                 """);
347         when(request.send()).thenReturn(response);
348         when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
349
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);
355     }
356
357     @Test
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();
362
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());
373
374         BoschSHCException e = assertThrows(BoschSHCException.class,
375                 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
376         assertEquals(
377                 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
378                 e.getMessage());
379     }
380
381     @Test
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();
387
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);
395
396         BoschSHCException e = assertThrows(BoschSHCException.class,
397                 () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
398         assertEquals(
399                 "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
400                 e.getMessage());
401     }
402
403     @Test
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();
408
409         Request request = mock(Request.class);
410         when(request.header(anyString(), anyString())).thenReturn(request);
411         ContentResponse response = mock(ContentResponse.class);
412
413         when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
414                 .thenReturn(request);
415         when(request.send()).thenReturn(response);
416
417         BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
418         binarySwitchState.on = true;
419         fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
420     }
421
422     @AfterEach
423     void afterEach() throws Exception {
424         fixture.dispose();
425     }
426 }