]> git.basschouten.com Git - openhab-addons.git/blob
bc3708f2bb53191257a68c2a2cbdb3f9c554522f
[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.webthing.internal.client;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
17
18 import java.io.IOException;
19 import java.net.InetSocketAddress;
20 import java.net.URI;
21 import java.net.http.WebSocket;
22 import java.nio.ByteBuffer;
23 import java.nio.charset.StandardCharsets;
24 import java.nio.file.Files;
25 import java.nio.file.Paths;
26 import java.time.Duration;
27 import java.util.Map;
28 import java.util.concurrent.CompletableFuture;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.Future;
31 import java.util.concurrent.ScheduledExecutorService;
32 import java.util.concurrent.atomic.AtomicBoolean;
33 import java.util.concurrent.atomic.AtomicReference;
34 import java.util.function.BiConsumer;
35 import java.util.function.Consumer;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.websocket.api.BatchMode;
41 import org.eclipse.jetty.websocket.api.CloseStatus;
42 import org.eclipse.jetty.websocket.api.RemoteEndpoint;
43 import org.eclipse.jetty.websocket.api.Session;
44 import org.eclipse.jetty.websocket.api.SuspendToken;
45 import org.eclipse.jetty.websocket.api.UpgradeRequest;
46 import org.eclipse.jetty.websocket.api.UpgradeResponse;
47 import org.eclipse.jetty.websocket.api.WebSocketListener;
48 import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
49 import org.eclipse.jetty.websocket.api.WebSocketPolicy;
50 import org.eclipse.jetty.websocket.api.WriteCallback;
51 import org.junit.jupiter.api.Test;
52 import org.openhab.binding.webthing.internal.client.dto.PropertyStatusMessage;
53
54 import com.google.gson.Gson;
55
56 /**
57  *
58  *
59  * @author Gregor Roth - Initial contribution
60  */
61 @NonNullByDefault
62 public class WebthingTest {
63     private static final Gson GSON = new Gson();
64
65     @Test
66     public void testWebthingDescription() throws Exception {
67         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
68         var request = Mocks.mockRequest(null, load("/windsensor_response.json"));
69         when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
70
71         var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
72         when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
73
74         var webthing = createTestWebthing("http://example.org:8090", httpClient);
75         var metadata = webthing.getThingDescription();
76         assertEquals("Wind", metadata.title);
77     }
78
79     @Test
80     public void testWebthingDescriptionUnsetSchema() throws Exception {
81         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
82         var request = Mocks.mockRequest(null, load("/unsetschema_response.json"));
83         when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
84
85         var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
86         when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
87
88         var webthing = createTestWebthing("http://example.org:8090", httpClient);
89         var metadata = webthing.getThingDescription();
90         assertEquals("Wind", metadata.title);
91     }
92
93     @Test
94     public void testWebthingDescriptionUNsupportedSchema() throws Exception {
95         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
96         var request = Mocks.mockRequest(null, load("/unknownschema_response.json"));
97         when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
98
99         try {
100             createTestWebthing("http://example.org:8090", httpClient);
101             fail();
102         } catch (IOException e) {
103             assertEquals(
104                     "unsupported schema (@context parameter) https://www.w3.org/2019/wot/td/v1 (Supported schemas are https://webthings.io/schemas and https://iot.mozilla.org/schemas)",
105                     e.getMessage());
106         }
107     }
108
109     @Test
110     public void testReadReadOnlyProperty() throws Exception {
111         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
112         var request = Mocks.mockRequest(null, load("/windsensor_response.json"));
113         when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
114
115         var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
116         when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
117
118         var webthing = createTestWebthing("http://example.org:8090", httpClient);
119
120         assertEquals(34.0, webthing.readProperty("windspeed"));
121         try {
122             webthing.writeProperty("windspeed", 23.0);
123             fail();
124         } catch (PropertyAccessException e) {
125             assertEquals(
126                     "could not write windspeed (http://example.org:8090/properties/windspeed) with 23.0. Property is readOnly",
127                     e.getMessage());
128         }
129     }
130
131     @Test
132     public void testReadPropertyTest() throws Exception {
133         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
134         var request = Mocks.mockRequest(null, load("/awning_response.json"));
135         when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
136
137         var request2 = Mocks.mockRequest(null, load("/awning_property.json"));
138         when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
139                 .thenReturn(request2);
140
141         var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
142
143         assertEquals(85.0, webthing.readProperty("target_position"));
144     }
145
146     @Test
147     public void testWriteProperty() throws Exception {
148         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
149         var request = Mocks.mockRequest(null, load("/awning_response.json"));
150         when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
151
152         var request2 = Mocks.mockRequest("{\"target_position\":10}", load("/awning_property.json"));
153         when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
154                 .thenReturn(request2);
155
156         var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
157         webthing.writeProperty("target_position", 10);
158     }
159
160     @Test
161     public void testWritePropertyError() throws Exception {
162         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
163         var request = Mocks.mockRequest(null, load("/awning_response.json"));
164         when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
165
166         var request2 = Mocks.mockRequest("{\"target_position\":10}", load("/awning_property.json"), 200, 400);
167         when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
168                 .thenReturn(request2);
169
170         var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
171         try {
172             webthing.writeProperty("target_position", 10);
173             fail();
174         } catch (PropertyAccessException e) {
175             assertEquals(
176                     "could not write target_position (http://example.org:8090/0/properties/target_position) with 10",
177                     e.getMessage());
178         }
179     }
180
181     @Test
182     public void testReadPropertyError() throws Exception {
183         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
184         var request = Mocks.mockRequest(null, load("/windsensor_response.json"));
185         when(httpClient.newRequest(URI.create("http://example.org:8090"))).thenReturn(request);
186
187         var request2 = Mocks.mockRequest(null, load("/windsensor_response.json"), 500, 200);
188         when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
189
190         var webthing = createTestWebthing("http://example.org:8090", httpClient);
191         try {
192             webthing.readProperty("windspeed");
193             fail();
194         } catch (PropertyAccessException e) {
195             assertEquals("could not read windspeed (http://example.org:8090/properties/windspeed)", e.getMessage());
196         }
197     }
198
199     @Test
200     public void testWebSocket() throws Exception {
201         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
202         var request = Mocks.mockRequest(null, load("/awning_response.json"));
203         when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
204
205         var request2 = Mocks.mockRequest(null, load("/awning_property.json"));
206         when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
207                 .thenReturn(request2);
208
209         var errorHandler = new ErrorHandler();
210         var webSocketFactory = new TestWebsocketConnectionFactory();
211         var webthing = createTestWebthing("http://example.org:8090/0", httpClient, errorHandler, webSocketFactory);
212
213         var propertyChangedListenerImpl = new PropertyChangedListenerImpl();
214         webthing.observeProperty("target_position", propertyChangedListenerImpl);
215
216         var webSocketServerSide = webSocketFactory.webSocketRef.get();
217         var message = new PropertyStatusMessage();
218         message.messageType = "propertyStatus";
219         message.data = Map.of("target_position", 33);
220         webSocketServerSide.sendToClient(message);
221
222         while (propertyChangedListenerImpl.valueRef.get() == null) {
223             try {
224                 Thread.sleep(100);
225             } catch (InterruptedException ignore) {
226             }
227         }
228         assertEquals(33.0, propertyChangedListenerImpl.valueRef.get());
229
230         webSocketServerSide.sendCloseToClient();
231         assertEquals("websocket closed by peer. ", errorHandler.errorRef.get());
232     }
233
234     @Test
235     public void testWebSocketReceiveTimout() throws Exception {
236         var httpClient = mock(org.eclipse.jetty.client.HttpClient.class);
237         var request = Mocks.mockRequest(null, load("/awning_response.json"));
238         when(httpClient.newRequest(URI.create("http://example.org:8090/0"))).thenReturn(request);
239
240         var request2 = Mocks.mockRequest(null, load("/awning_property.json"));
241         when(httpClient.newRequest(URI.create("http://example.org:8090/0/properties/target_position")))
242                 .thenReturn(request2);
243
244         var errorHandler = new ErrorHandler();
245         var webSocketFactory = new TestWebsocketConnectionFactory();
246         var pingPeriod = Duration.ofMillis(300);
247         var webthing = createTestWebthing("http://example.org:8090/0", httpClient, errorHandler, webSocketFactory,
248                 pingPeriod);
249
250         var propertyChangedListenerImpl = new PropertyChangedListenerImpl();
251         webthing.observeProperty("target_position", propertyChangedListenerImpl);
252         webSocketFactory.webSocketRef.get().ignorePing.set(true);
253
254         try {
255             Thread.sleep(pingPeriod.dividedBy(2).toMillis());
256         } catch (InterruptedException ignore) {
257         }
258         assertNull(errorHandler.errorRef.get());
259
260         try {
261             Thread.sleep(pingPeriod.multipliedBy(4).toMillis());
262         } catch (InterruptedException ignore) {
263         }
264         assertTrue(errorHandler.errorRef.get().startsWith("connection seems to be broken (last message received at"));
265     }
266
267     public static String load(String name) throws Exception {
268         return new String(Files.readAllBytes(Paths.get(WebthingTest.class.getResource(name).toURI())));
269     }
270
271     public static ConsumedThingImpl createTestWebthing(String uri, HttpClient httpClient) throws IOException {
272         return createTestWebthing(uri, httpClient, (String) -> {
273         }, new TestWebsocketConnectionFactory());
274     }
275
276     public static ConsumedThingImpl createTestWebthing(String uri, HttpClient httpClient, Consumer<String> errorHandler,
277             WebSocketConnectionFactory websocketConnectionFactory, Duration pingPeriod) throws IOException {
278         return new ConsumedThingImpl(httpClient, URI.create(uri), Executors.newSingleThreadScheduledExecutor(),
279                 errorHandler, websocketConnectionFactory, pingPeriod);
280     }
281
282     public static ConsumedThingImpl createTestWebthing(String uri, HttpClient httpClient, Consumer<String> errorHandler,
283             WebSocketConnectionFactory websocketConnectionFactory) throws IOException {
284         return createTestWebthing(uri, httpClient, errorHandler, websocketConnectionFactory, Duration.ofSeconds(100));
285     }
286
287     public static class TestWebsocketConnectionFactory implements WebSocketConnectionFactory {
288         public final AtomicReference<WebSocketImpl> webSocketRef = new AtomicReference<>();
289
290         @Override
291         public WebSocketConnection create(URI webSocketURI, ScheduledExecutorService executor,
292                 Consumer<String> errorHandler, Duration pingPeriod) {
293             var webSocketConnection = new WebSocketConnectionImpl(executor, errorHandler, pingPeriod);
294             var webSocket = new WebSocketImpl(webSocketConnection);
295             webSocketRef.set(webSocket);
296             webSocketConnection.onWebSocketConnect(webSocket);
297             return webSocketConnection;
298         }
299     }
300
301     public static final class WebSocketImpl implements Session {
302         private final WebSocketListener listener;
303         private final WebSocketPingPongListener pongListener;
304         public AtomicBoolean ignorePing = new AtomicBoolean(false);
305
306         WebSocketImpl(WebSocketConnectionImpl connection) {
307             this.listener = connection;
308             this.pongListener = connection;
309         }
310
311         @Override
312         public void close() {
313         }
314
315         @Override
316         public void close(@Nullable CloseStatus closeStatus) {
317         }
318
319         @Override
320         public void close(int statusCode, @Nullable String reason) {
321         }
322
323         @Override
324         public void disconnect() throws IOException {
325         }
326
327         @Override
328         public long getIdleTimeout() {
329             return 0;
330         }
331
332         @Override
333         public InetSocketAddress getLocalAddress() {
334             return InetSocketAddress.createUnresolved("test", 23);
335         }
336
337         @Override
338         public WebSocketPolicy getPolicy() {
339             return WebSocketPolicy.newClientPolicy();
340         }
341
342         @Override
343         public String getProtocolVersion() {
344             return "1";
345         }
346
347         @Override
348         public RemoteEndpoint getRemote() {
349             return new RemoteEndpoint() {
350                 @Override
351                 public void sendBytes(@Nullable ByteBuffer data) throws IOException {
352                 }
353
354                 @Override
355                 public Future sendBytesByFuture(@Nullable ByteBuffer data) {
356                     throw new UnsupportedOperationException();
357                 }
358
359                 @Override
360                 public void sendBytes(@Nullable ByteBuffer data, @Nullable WriteCallback callback) {
361                 }
362
363                 @Override
364                 public void sendPartialBytes(@Nullable ByteBuffer fragment, boolean isLast) throws IOException {
365                 }
366
367                 @Override
368                 public void sendPartialString(@Nullable String fragment, boolean isLast) throws IOException {
369                 }
370
371                 @Override
372                 public void sendPing(@Nullable ByteBuffer applicationData) throws IOException {
373                     if (!ignorePing.get()) {
374                         pongListener.onWebSocketPong(applicationData);
375                     }
376                 }
377
378                 @Override
379                 public void sendPong(@Nullable ByteBuffer applicationData) throws IOException {
380                 }
381
382                 @Override
383                 public void sendString(@Nullable String text) throws IOException {
384                 }
385
386                 @Override
387                 public Future sendStringByFuture(@Nullable String text) {
388                     throw new UnsupportedOperationException();
389                 }
390
391                 @Override
392                 public void sendString(@Nullable String text, @Nullable WriteCallback callback) {
393                 }
394
395                 @Override
396                 public BatchMode getBatchMode() {
397                     return BatchMode.AUTO;
398                 }
399
400                 @Override
401                 public void setBatchMode(@Nullable BatchMode mode) {
402                 }
403
404                 @Override
405                 public InetSocketAddress getInetSocketAddress() {
406                     throw new UnsupportedOperationException();
407                 }
408
409                 @Override
410                 public void flush() throws IOException {
411                 }
412
413                 @Override
414                 public int getMaxOutgoingFrames() {
415                     return 0;
416                 }
417
418                 @Override
419                 public void setMaxOutgoingFrames(int maxOutgoingFrames) {
420                 }
421             };
422         }
423
424         @Override
425         public InetSocketAddress getRemoteAddress() {
426             return InetSocketAddress.createUnresolved("test", 12);
427         }
428
429         @Override
430         public UpgradeRequest getUpgradeRequest() {
431             throw new UnsupportedOperationException();
432         }
433
434         @Override
435         public UpgradeResponse getUpgradeResponse() {
436             throw new UnsupportedOperationException();
437         }
438
439         @Override
440         public boolean isOpen() {
441             return false;
442         }
443
444         @Override
445         public boolean isSecure() {
446             return false;
447         }
448
449         @Override
450         public void setIdleTimeout(long ms) {
451         }
452
453         @Override
454         public SuspendToken suspend() {
455             return new SuspendToken() {
456
457                 @Override
458                 public void resume() {
459                 }
460             };
461         }
462
463         public void sendToClient(PropertyStatusMessage message) {
464             var data = GSON.toJson(message);
465             listener.onWebSocketText(data);
466         }
467
468         public void sendCloseToClient() {
469             listener.onWebSocketClose(200, "");
470         }
471
472         public CompletableFuture<WebSocket> sendPing(String message) {
473             if (!ignorePing.get()) {
474                 var bytes = message.getBytes(StandardCharsets.UTF_8);
475                 listener.onWebSocketBinary(bytes, 0, bytes.length);
476             }
477             return CompletableFuture.completedFuture(null);
478         }
479     }
480
481     private static final class PropertyChangedListenerImpl implements BiConsumer<String, Object> {
482         public final AtomicReference<String> propertyNameRef = new AtomicReference<>();
483         public final AtomicReference<Object> valueRef = new AtomicReference<>();
484
485         @Override
486         public void accept(String propertyName, Object value) {
487             propertyNameRef.set(propertyName);
488             valueRef.set(value);
489         }
490     }
491
492     public static class ErrorHandler implements Consumer<String> {
493         public final AtomicReference<String> errorRef = new AtomicReference<>();
494
495         @Override
496         public void accept(String error) {
497             errorRef.set(error);
498         }
499     }
500 }