]> git.basschouten.com Git - openhab-addons.git/blob
628607ca9ba1ce53492904064dbb3267e299a0ce
[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.nio.ByteBuffer;
20 import java.nio.charset.StandardCharsets;
21 import java.util.Collections;
22 import java.util.List;
23 import java.util.concurrent.AbstractExecutorService;
24 import java.util.concurrent.Callable;
25 import java.util.concurrent.Delayed;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
31 import java.util.function.Consumer;
32
33 import org.eclipse.jdt.annotation.NonNull;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.eclipse.jetty.client.api.Request;
37 import org.eclipse.jetty.client.api.Response;
38 import org.eclipse.jetty.client.api.Response.CompleteListener;
39 import org.eclipse.jetty.client.api.Result;
40 import org.eclipse.jetty.client.util.BufferingResponseListener;
41 import org.eclipse.jetty.http.HttpMethod;
42 import org.junit.jupiter.api.AfterEach;
43 import org.junit.jupiter.api.BeforeEach;
44 import org.junit.jupiter.api.Test;
45 import org.junit.jupiter.api.extension.ExtendWith;
46 import org.mockito.ArgumentCaptor;
47 import org.mockito.Mock;
48 import org.mockito.junit.jupiter.MockitoExtension;
49 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
50 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
51 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
52 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
53 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
54 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
55 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
56
57 import com.google.gson.JsonObject;
58
59 /**
60  * Unit tests for {@link LongPolling}.
61  *
62  * @author David Pace - Initial contribution
63  *
64  */
65 @NonNullByDefault
66 @ExtendWith(MockitoExtension.class)
67 class LongPollingTest {
68
69     /**
70      * A dummy implementation of {@link ScheduledFuture}.
71      * <p>
72      * This is required because we can not return <code>null</code> in the executor service test implementation (see
73      * below).
74      *
75      * @author David Pace - Initial contribution
76      *
77      * @param <T> The result type returned by this Future
78      */
79     private static class NullScheduledFuture<T> implements ScheduledFuture<T> {
80
81         @Override
82         public long getDelay(@Nullable TimeUnit unit) {
83             return 0;
84         }
85
86         @Override
87         public int compareTo(@Nullable Delayed o) {
88             return 0;
89         }
90
91         @Override
92         public boolean cancel(boolean mayInterruptIfRunning) {
93             return false;
94         }
95
96         @Override
97         public boolean isCancelled() {
98             return false;
99         }
100
101         @Override
102         public boolean isDone() {
103             return false;
104         }
105
106         @Override
107         public T get() throws InterruptedException, ExecutionException {
108             return null;
109         }
110
111         @Override
112         public T get(long timeout, @Nullable TimeUnit unit)
113                 throws InterruptedException, ExecutionException, TimeoutException {
114             return null;
115         }
116     }
117
118     /**
119      * Executor service implementation that runs all runnables in the same thread in order to enable deterministic
120      * testing.
121      *
122      * @author David Pace - Initial contribution
123      *
124      */
125     private static class SameThreadExecutorService extends AbstractExecutorService implements ScheduledExecutorService {
126
127         private volatile boolean terminated;
128
129         @Override
130         public void shutdown() {
131             terminated = true;
132         }
133
134         @NonNullByDefault({})
135         @Override
136         public List<Runnable> shutdownNow() {
137             return Collections.emptyList();
138         }
139
140         @Override
141         public boolean isShutdown() {
142             return terminated;
143         }
144
145         @Override
146         public boolean isTerminated() {
147             return terminated;
148         }
149
150         @Override
151         public boolean awaitTermination(long timeout, @Nullable TimeUnit unit) throws InterruptedException {
152             shutdown();
153             return terminated;
154         }
155
156         @Override
157         public void execute(@Nullable Runnable command) {
158             if (command != null) {
159                 // execute in the same thread in unit tests
160                 command.run();
161             }
162         }
163
164         @Override
165         public ScheduledFuture<?> schedule(@Nullable Runnable command, long delay, @Nullable TimeUnit unit) {
166             // not used in this tests
167             return new NullScheduledFuture<Object>();
168         }
169
170         @Override
171         public <V> ScheduledFuture<V> schedule(@Nullable Callable<V> callable, long delay, @Nullable TimeUnit unit) {
172             return new NullScheduledFuture<V>();
173         }
174
175         @Override
176         public ScheduledFuture<?> scheduleAtFixedRate(@Nullable Runnable command, long initialDelay, long period,
177                 @Nullable TimeUnit unit) {
178             if (command != null) {
179                 command.run();
180             }
181             return new NullScheduledFuture<Object>();
182         }
183
184         @Override
185         public ScheduledFuture<?> scheduleWithFixedDelay(@Nullable Runnable command, long initialDelay, long delay,
186                 @Nullable TimeUnit unit) {
187             if (command != null) {
188                 command.run();
189             }
190             return new NullScheduledFuture<Object>();
191         }
192     }
193
194     private @NonNullByDefault({}) LongPolling fixture;
195
196     private @NonNullByDefault({}) BoschHttpClient httpClient;
197
198     private @Mock @NonNullByDefault({}) Consumer<@NonNull LongPollResult> longPollHandler;
199
200     private @Mock @NonNullByDefault({}) Consumer<@NonNull Throwable> failureHandler;
201
202     @BeforeEach
203     void beforeEach() {
204         fixture = new LongPolling(new SameThreadExecutorService(), longPollHandler, failureHandler);
205         httpClient = mock(BoschHttpClient.class);
206     }
207
208     @Test
209     void start() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
210         // when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
211         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
212
213         Request subscribeRequest = mock(Request.class);
214         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
215                 argThat((JsonRpcRequest r) -> "RE/subscribe".equals(r.method)))).thenReturn(subscribeRequest);
216         SubscribeResult subscribeResult = new SubscribeResult();
217         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
218
219         Request longPollRequest = mock(Request.class);
220         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
221                 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
222
223         fixture.start(httpClient);
224
225         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
226         verify(longPollRequest).send(completeListener.capture());
227
228         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
229
230         String longPollResultJSON = "{\"result\":[{\"path\":\"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch\",\"@type\":\"DeviceServiceData\",\"id\":\"PowerSwitch\",\"state\":{\"@type\":\"powerSwitchState\",\"switchState\":\"ON\"},\"deviceId\":\"hdm:HomeMaticIP:3014F711A0001916D859A8A9\"}],\"jsonrpc\":\"2.0\"}\n";
231         Response response = mock(Response.class);
232         bufferingResponseListener.onContent(response,
233                 ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8)));
234
235         Result result = mock(Result.class);
236         bufferingResponseListener.onComplete(result);
237
238         ArgumentCaptor<LongPollResult> longPollResultCaptor = ArgumentCaptor.forClass(LongPollResult.class);
239         verify(longPollHandler).accept(longPollResultCaptor.capture());
240         LongPollResult longPollResult = longPollResultCaptor.getValue();
241         assertEquals(1, longPollResult.result.size());
242         assertEquals(longPollResult.result.get(0).getClass(), DeviceServiceData.class);
243         DeviceServiceData longPollResultItem = (DeviceServiceData) longPollResult.result.get(0);
244         assertEquals("hdm:HomeMaticIP:3014F711A0001916D859A8A9", longPollResultItem.deviceId);
245         assertEquals("/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch", longPollResultItem.path);
246         assertEquals("PowerSwitch", longPollResultItem.id);
247         JsonObject stateObject = (JsonObject) longPollResultItem.state;
248         assertNotNull(stateObject);
249         assertEquals("ON", stateObject.get("switchState").getAsString());
250     }
251
252     @Test
253     void startLongPollingReceiveScenario()
254             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
255         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
256
257         Request subscribeRequest = mock(Request.class);
258         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
259                 argThat((JsonRpcRequest r) -> "RE/subscribe".equals(r.method)))).thenReturn(subscribeRequest);
260         SubscribeResult subscribeResult = new SubscribeResult();
261         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
262
263         Request longPollRequest = mock(Request.class);
264         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
265                 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
266
267         fixture.start(httpClient);
268
269         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
270         verify(longPollRequest).send(completeListener.capture());
271
272         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
273
274         String longPollResultJSON = "{\"result\":[{\"@type\": \"scenarioTriggered\",\"name\": \"My scenario\",\"id\": \"509bd737-eed0-40b7-8caa-e8686a714399\",\"lastTimeTriggered\": \"1693758693032\"}],\"jsonrpc\":\"2.0\"}\n";
275         Response response = mock(Response.class);
276         bufferingResponseListener.onContent(response,
277                 ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8)));
278
279         Result result = mock(Result.class);
280         bufferingResponseListener.onComplete(result);
281
282         ArgumentCaptor<LongPollResult> longPollResultCaptor = ArgumentCaptor.forClass(LongPollResult.class);
283         verify(longPollHandler).accept(longPollResultCaptor.capture());
284         LongPollResult longPollResult = longPollResultCaptor.getValue();
285         assertEquals(1, longPollResult.result.size());
286         assertEquals(longPollResult.result.get(0).getClass(), Scenario.class);
287         Scenario longPollResultItem = (Scenario) longPollResult.result.get(0);
288         assertEquals("509bd737-eed0-40b7-8caa-e8686a714399", longPollResultItem.id);
289         assertEquals("My scenario", longPollResultItem.name);
290         assertEquals("1693758693032", longPollResultItem.lastTimeTriggered);
291     }
292
293     @Test
294     void startLongPollingReceiveUserDefinedState()
295             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
296         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
297
298         Request subscribeRequest = mock(Request.class);
299         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
300                 argThat((JsonRpcRequest r) -> "RE/subscribe".equals(r.method)))).thenReturn(subscribeRequest);
301         SubscribeResult subscribeResult = new SubscribeResult();
302         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
303
304         Request longPollRequest = mock(Request.class);
305         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
306                 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
307
308         fixture.start(httpClient);
309
310         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
311         verify(longPollRequest).send(completeListener.capture());
312
313         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
314
315         String longPollResultJSON = "{\"result\":[{\"deleted\":false,\"@type\":\"userDefinedState\",\"name\":\"My User state\",\"id\":\"23d34fa6-382a-444d-8aae-89c706e22155\",\"state\":true}],\"jsonrpc\":\"2.0\"}\n";
316         Response response = mock(Response.class);
317         bufferingResponseListener.onContent(response,
318                 ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8)));
319
320         Result result = mock(Result.class);
321         bufferingResponseListener.onComplete(result);
322
323         ArgumentCaptor<LongPollResult> longPollResultCaptor = ArgumentCaptor.forClass(LongPollResult.class);
324         verify(longPollHandler).accept(longPollResultCaptor.capture());
325         LongPollResult longPollResult = longPollResultCaptor.getValue();
326         assertEquals(1, longPollResult.result.size());
327         assertEquals(longPollResult.result.get(0).getClass(), UserDefinedState.class);
328         UserDefinedState longPollResultItem = (UserDefinedState) longPollResult.result.get(0);
329         assertEquals("23d34fa6-382a-444d-8aae-89c706e22155", longPollResultItem.getId());
330         assertEquals("My User state", longPollResultItem.getName());
331         assertTrue(longPollResultItem.isState());
332     }
333
334     @Test
335     void startSubscriptionFailure()
336             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
337         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any()))
338                 .thenThrow(new ExecutionException("Subscription failed.", null));
339
340         LongPollingFailedException e = assertThrows(LongPollingFailedException.class, () -> fixture.start(httpClient));
341         assertTrue(e.getMessage().contains("Subscription failed."));
342     }
343
344     @Test
345     void startLongPollFailure() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
346         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
347
348         Request request = mock(Request.class);
349         when(httpClient.createRequest(anyString(), same(HttpMethod.POST), any(JsonRpcRequest.class)))
350                 .thenReturn(request);
351         SubscribeResult subscribeResult = new SubscribeResult();
352         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
353
354         Request longPollRequest = mock(Request.class);
355         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
356                 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
357
358         fixture.start(httpClient);
359
360         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
361         verify(longPollRequest).send(completeListener.capture());
362
363         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
364
365         Result result = mock(Result.class);
366         ExecutionException exception = new ExecutionException("test exception", null);
367         when(result.getFailure()).thenReturn(exception);
368         bufferingResponseListener.onComplete(result);
369
370         ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
371         verify(failureHandler).accept(throwableCaptor.capture());
372         Throwable t = throwableCaptor.getValue();
373         assertEquals("Unexpected exception during long polling request", t.getMessage());
374         assertSame(exception, t.getCause());
375     }
376
377     @Test
378     void startSubscriptionInvalid()
379             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
380         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
381
382         Request subscribeRequest = mock(Request.class);
383         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
384                 argThat((JsonRpcRequest r) -> "RE/subscribe".equals(r.method)))).thenReturn(subscribeRequest);
385         SubscribeResult subscribeResult = new SubscribeResult();
386         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
387
388         Request longPollRequest = mock(Request.class);
389         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
390                 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
391
392         fixture.start(httpClient);
393
394         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
395         verify(longPollRequest).send(completeListener.capture());
396
397         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
398
399         String longPollResultJSON = "{\"jsonrpc\":\"2.0\",\"error\": {\"code\":-32001,\"message\":\"No subscription with id: e8fei62b0-0\"}}\n";
400         Response response = mock(Response.class);
401         bufferingResponseListener.onContent(response,
402                 ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8)));
403
404         Result result = mock(Result.class);
405         bufferingResponseListener.onComplete(result);
406     }
407
408     @AfterEach
409     void afterEach() {
410         fixture.stop();
411     }
412 }