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