]> git.basschouten.com Git - openhab-addons.git/blob
524d23fc111b8c9da1d419fa96e22b5bced85fd4
[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.SubscribeResult;
52 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
53 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
54
55 import com.google.gson.JsonObject;
56
57 /**
58  * Unit tests for {@link LongPolling}.
59  *
60  * @author David Pace - Initial contribution
61  *
62  */
63 @NonNullByDefault
64 @ExtendWith(MockitoExtension.class)
65 public class LongPollingTest {
66
67     /**
68      * A dummy implementation of {@link ScheduledFuture}.
69      * <p>
70      * This is required because we can not return <code>null</code> in the executor service test implementation (see
71      * below).
72      *
73      * @author David Pace - Initial contribution
74      *
75      * @param <T> The result type returned by this Future
76      */
77     private static class NullScheduledFuture<T> implements ScheduledFuture<T> {
78
79         @Override
80         public long getDelay(@Nullable TimeUnit unit) {
81             return 0;
82         }
83
84         @Override
85         public int compareTo(@Nullable Delayed o) {
86             return 0;
87         }
88
89         @Override
90         public boolean cancel(boolean mayInterruptIfRunning) {
91             return false;
92         }
93
94         @Override
95         public boolean isCancelled() {
96             return false;
97         }
98
99         @Override
100         public boolean isDone() {
101             return false;
102         }
103
104         @Override
105         public T get() throws InterruptedException, ExecutionException {
106             return null;
107         }
108
109         @Override
110         public T get(long timeout, @Nullable TimeUnit unit)
111                 throws InterruptedException, ExecutionException, TimeoutException {
112             return null;
113         }
114     }
115
116     /**
117      * Executor service implementation that runs all runnables in the same thread in order to enable deterministic
118      * testing.
119      *
120      * @author David Pace - Initial contribution
121      *
122      */
123     private static class SameThreadExecutorService extends AbstractExecutorService implements ScheduledExecutorService {
124
125         private volatile boolean terminated;
126
127         @Override
128         public void shutdown() {
129             terminated = true;
130         }
131
132         @NonNullByDefault({})
133         @Override
134         public List<Runnable> shutdownNow() {
135             return Collections.emptyList();
136         }
137
138         @Override
139         public boolean isShutdown() {
140             return terminated;
141         }
142
143         @Override
144         public boolean isTerminated() {
145             return terminated;
146         }
147
148         @Override
149         public boolean awaitTermination(long timeout, @Nullable TimeUnit unit) throws InterruptedException {
150             shutdown();
151             return terminated;
152         }
153
154         @Override
155         public void execute(@Nullable Runnable command) {
156             if (command != null) {
157                 // execute in the same thread in unit tests
158                 command.run();
159             }
160         }
161
162         @Override
163         public ScheduledFuture<?> schedule(@Nullable Runnable command, long delay, @Nullable TimeUnit unit) {
164             // not used in this tests
165             return new NullScheduledFuture<Object>();
166         }
167
168         @Override
169         public <V> ScheduledFuture<V> schedule(@Nullable Callable<V> callable, long delay, @Nullable TimeUnit unit) {
170             return new NullScheduledFuture<V>();
171         }
172
173         @Override
174         public ScheduledFuture<?> scheduleAtFixedRate(@Nullable Runnable command, long initialDelay, long period,
175                 @Nullable TimeUnit unit) {
176             if (command != null) {
177                 command.run();
178             }
179             return new NullScheduledFuture<Object>();
180         }
181
182         @Override
183         public ScheduledFuture<?> scheduleWithFixedDelay(@Nullable Runnable command, long initialDelay, long delay,
184                 @Nullable TimeUnit unit) {
185             if (command != null) {
186                 command.run();
187             }
188             return new NullScheduledFuture<Object>();
189         }
190     }
191
192     private @NonNullByDefault({}) LongPolling fixture;
193
194     private @NonNullByDefault({}) BoschHttpClient httpClient;
195
196     private @Mock @NonNullByDefault({}) Consumer<@NonNull LongPollResult> longPollHandler;
197
198     private @Mock @NonNullByDefault({}) Consumer<@NonNull Throwable> failureHandler;
199
200     @BeforeEach
201     void beforeEach() {
202         fixture = new LongPolling(new SameThreadExecutorService(), longPollHandler, failureHandler);
203         httpClient = mock(BoschHttpClient.class);
204     }
205
206     @Test
207     void start() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
208         // when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
209         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
210
211         Request subscribeRequest = mock(Request.class);
212         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
213                 argThat((JsonRpcRequest r) -> r.method.equals("RE/subscribe")))).thenReturn(subscribeRequest);
214         SubscribeResult subscribeResult = new SubscribeResult();
215         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
216
217         Request longPollRequest = mock(Request.class);
218         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
219                 argThat((JsonRpcRequest r) -> r.method.equals("RE/longPoll")))).thenReturn(longPollRequest);
220
221         fixture.start(httpClient);
222
223         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
224         verify(longPollRequest).send(completeListener.capture());
225
226         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
227
228         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";
229         Response response = mock(Response.class);
230         bufferingResponseListener.onContent(response,
231                 ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8)));
232
233         Result result = mock(Result.class);
234         bufferingResponseListener.onComplete(result);
235
236         ArgumentCaptor<LongPollResult> longPollResultCaptor = ArgumentCaptor.forClass(LongPollResult.class);
237         verify(longPollHandler).accept(longPollResultCaptor.capture());
238         LongPollResult longPollResult = longPollResultCaptor.getValue();
239         assertEquals(1, longPollResult.result.size());
240         DeviceServiceData longPollResultItem = longPollResult.result.get(0);
241         assertEquals("hdm:HomeMaticIP:3014F711A0001916D859A8A9", longPollResultItem.deviceId);
242         assertEquals("/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch", longPollResultItem.path);
243         assertEquals("PowerSwitch", longPollResultItem.id);
244         JsonObject stateObject = (JsonObject) longPollResultItem.state;
245         assertNotNull(stateObject);
246         assertEquals("ON", stateObject.get("switchState").getAsString());
247     }
248
249     @Test
250     void startSubscriptionFailure()
251             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
252         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any()))
253                 .thenThrow(new ExecutionException("Subscription failed.", null));
254
255         LongPollingFailedException e = assertThrows(LongPollingFailedException.class, () -> fixture.start(httpClient));
256         assertTrue(e.getMessage().contains("Subscription failed."));
257     }
258
259     @Test
260     void startLongPollFailure() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
261         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
262
263         Request request = mock(Request.class);
264         when(httpClient.createRequest(anyString(), same(HttpMethod.POST), any(JsonRpcRequest.class)))
265                 .thenReturn(request);
266         SubscribeResult subscribeResult = new SubscribeResult();
267         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
268
269         Request longPollRequest = mock(Request.class);
270         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
271                 argThat((JsonRpcRequest r) -> r.method.equals("RE/longPoll")))).thenReturn(longPollRequest);
272
273         fixture.start(httpClient);
274
275         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
276         verify(longPollRequest).send(completeListener.capture());
277
278         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
279
280         Result result = mock(Result.class);
281         ExecutionException exception = new ExecutionException("test exception", null);
282         when(result.getFailure()).thenReturn(exception);
283         bufferingResponseListener.onComplete(result);
284
285         ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
286         verify(failureHandler).accept(throwableCaptor.capture());
287         Throwable t = throwableCaptor.getValue();
288         assertEquals("Unexpected exception during long polling request", t.getMessage());
289         assertSame(exception, t.getCause());
290     }
291
292     @Test
293     void startSubscriptionInvalid()
294             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
295         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
296
297         Request subscribeRequest = mock(Request.class);
298         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
299                 argThat((JsonRpcRequest r) -> r.method.equals("RE/subscribe")))).thenReturn(subscribeRequest);
300         SubscribeResult subscribeResult = new SubscribeResult();
301         when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
302
303         Request longPollRequest = mock(Request.class);
304         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
305                 argThat((JsonRpcRequest r) -> r.method.equals("RE/longPoll")))).thenReturn(longPollRequest);
306
307         fixture.start(httpClient);
308
309         ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
310         verify(longPollRequest).send(completeListener.capture());
311
312         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
313
314         String longPollResultJSON = "{\"jsonrpc\":\"2.0\",\"error\": {\"code\":-32001,\"message\":\"No subscription with id: e8fei62b0-0\"}}\n";
315         Response response = mock(Response.class);
316         bufferingResponseListener.onContent(response,
317                 ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8)));
318
319         Result result = mock(Result.class);
320         bufferingResponseListener.onComplete(result);
321     }
322
323     @AfterEach
324     void afterEach() {
325         fixture.stop();
326     }
327 }