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