2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschshc.internal.devices.bridge;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.ArgumentMatchers.*;
17 import static org.mockito.Mockito.*;
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;
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;
57 import com.google.gson.JsonObject;
60 * Unit tests for {@link LongPolling}.
62 * @author David Pace - Initial contribution
66 @ExtendWith(MockitoExtension.class)
67 class LongPollingTest {
70 * A dummy implementation of {@link ScheduledFuture}.
72 * This is required because we can not return <code>null</code> in the executor service test implementation (see
75 * @author David Pace - Initial contribution
77 * @param <T> The result type returned by this Future
79 private static class NullScheduledFuture<T> implements ScheduledFuture<T> {
82 public long getDelay(@Nullable TimeUnit unit) {
87 public int compareTo(@Nullable Delayed o) {
92 public boolean cancel(boolean mayInterruptIfRunning) {
97 public boolean isCancelled() {
102 public boolean isDone() {
107 public T get() throws InterruptedException, ExecutionException {
112 public T get(long timeout, @Nullable TimeUnit unit)
113 throws InterruptedException, ExecutionException, TimeoutException {
119 * Executor service implementation that runs all runnables in the same thread in order to enable deterministic
122 * @author David Pace - Initial contribution
125 private static class SameThreadExecutorService extends AbstractExecutorService implements ScheduledExecutorService {
127 private volatile boolean terminated;
130 public void shutdown() {
134 @NonNullByDefault({})
136 public List<Runnable> shutdownNow() {
137 return Collections.emptyList();
141 public boolean isShutdown() {
146 public boolean isTerminated() {
151 public boolean awaitTermination(long timeout, @Nullable TimeUnit unit) throws InterruptedException {
157 public void execute(@Nullable Runnable command) {
158 if (command != null) {
159 // execute in the same thread in unit tests
165 public ScheduledFuture<?> schedule(@Nullable Runnable command, long delay, @Nullable TimeUnit unit) {
166 // not used in this tests
167 return new NullScheduledFuture<Object>();
171 public <V> ScheduledFuture<V> schedule(@Nullable Callable<V> callable, long delay, @Nullable TimeUnit unit) {
172 return new NullScheduledFuture<V>();
176 public ScheduledFuture<?> scheduleAtFixedRate(@Nullable Runnable command, long initialDelay, long period,
177 @Nullable TimeUnit unit) {
178 if (command != null) {
181 return new NullScheduledFuture<Object>();
185 public ScheduledFuture<?> scheduleWithFixedDelay(@Nullable Runnable command, long initialDelay, long delay,
186 @Nullable TimeUnit unit) {
187 if (command != null) {
190 return new NullScheduledFuture<Object>();
194 private @NonNullByDefault({}) LongPolling fixture;
196 private @NonNullByDefault({}) BoschHttpClient httpClient;
198 private @Mock @NonNullByDefault({}) Consumer<@NonNull LongPollResult> longPollHandler;
200 private @Mock @NonNullByDefault({}) Consumer<@NonNull Throwable> failureHandler;
204 fixture = new LongPolling(new SameThreadExecutorService(), longPollHandler, failureHandler);
205 httpClient = mock(BoschHttpClient.class);
209 void start() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
210 // when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
211 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
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);
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);
223 fixture.start(httpClient);
225 ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
226 verify(longPollRequest).send(completeListener.capture());
228 BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
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)));
235 Result result = mock(Result.class);
236 bufferingResponseListener.onComplete(result);
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());
253 void startLongPollingReceiveScenario()
254 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
255 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
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);
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);
267 fixture.start(httpClient);
269 ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
270 verify(longPollRequest).send(completeListener.capture());
272 BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
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)));
279 Result result = mock(Result.class);
280 bufferingResponseListener.onComplete(result);
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);
294 void startLongPollingReceiveUserDefinedState()
295 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
296 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
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);
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);
308 fixture.start(httpClient);
310 ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
311 verify(longPollRequest).send(completeListener.capture());
313 BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
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)));
320 Result result = mock(Result.class);
321 bufferingResponseListener.onComplete(result);
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());
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));
340 LongPollingFailedException e = assertThrows(LongPollingFailedException.class, () -> fixture.start(httpClient));
341 assertTrue(e.getMessage().contains("Subscription failed."));
345 void startLongPollFailure() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
346 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
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);
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);
358 fixture.start(httpClient);
360 ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
361 verify(longPollRequest).send(completeListener.capture());
363 BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
365 Result result = mock(Result.class);
366 ExecutionException exception = new ExecutionException("test exception", null);
367 when(result.getFailure()).thenReturn(exception);
368 bufferingResponseListener.onComplete(result);
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());
378 void startSubscriptionInvalid()
379 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
380 when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
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);
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);
392 fixture.start(httpClient);
394 ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
395 verify(longPollRequest).send(completeListener.capture());
397 BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
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)));
404 Result result = mock(Result.class);
405 bufferingResponseListener.onComplete(result);