2 * Copyright (c) 2010-2021 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.webthing.internal.client;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
18 import java.io.IOException;
19 import java.net.InetSocketAddress;
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;
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;
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;
55 import com.google.gson.Gson;
60 * @author Gregor Roth - Initial contribution
63 public class WebthingTest {
64 private static final Gson GSON = new Gson();
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);
72 var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
73 when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
75 var webthing = createTestWebthing("http://example.org:8090", httpClient);
76 var metadata = webthing.getThingDescription();
77 assertEquals("Wind", metadata.title);
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);
86 var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
87 when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
89 var webthing = createTestWebthing("http://example.org:8090", httpClient);
90 var metadata = webthing.getThingDescription();
91 assertEquals("Wind", metadata.title);
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);
101 createTestWebthing("http://example.org:8090", httpClient);
103 } catch (IOException e) {
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)",
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);
116 var request2 = Mocks.mockRequest(null, load("/windsensor_property.json"));
117 when(httpClient.newRequest(URI.create("http://example.org:8090/properties/windspeed"))).thenReturn(request2);
119 var webthing = createTestWebthing("http://example.org:8090", httpClient);
121 assertEquals(34.0, webthing.readProperty("windspeed"));
123 webthing.writeProperty("windspeed", 23.0);
125 } catch (PropertyAccessException e) {
127 "could not write windspeed (http://example.org:8090/properties/windspeed) with 23.0. Property is readOnly",
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);
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);
142 var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
144 assertEquals(85.0, webthing.readProperty("target_position"));
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);
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);
157 var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
158 webthing.writeProperty("target_position", 10);
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);
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);
171 var webthing = createTestWebthing("http://example.org:8090/0", httpClient);
173 webthing.writeProperty("target_position", 10);
175 } catch (PropertyAccessException e) {
177 "could not write target_position (http://example.org:8090/0/properties/target_position) with 10",
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);
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);
191 var webthing = createTestWebthing("http://example.org:8090", httpClient);
193 webthing.readProperty("windspeed");
195 } catch (PropertyAccessException e) {
196 assertEquals("could not read windspeed (http://example.org:8090/properties/windspeed)", e.getMessage());
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);
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);
210 var errorHandler = new ErrorHandler();
211 var webSocketFactory = new TestWebsocketConnectionFactory();
212 var webthing = createTestWebthing("http://example.org:8090/0", httpClient, errorHandler, webSocketFactory);
214 var propertyChangedListenerImpl = new PropertyChangedListenerImpl();
215 webthing.observeProperty("target_position", propertyChangedListenerImpl);
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);
223 while (propertyChangedListenerImpl.valueRef.get() == null) {
226 } catch (InterruptedException ignore) {
229 assertEquals(33.0, propertyChangedListenerImpl.valueRef.get());
231 webSocketServerSide.sendCloseToClient();
232 assertEquals("websocket closed by peer. ", errorHandler.errorRef.get());
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);
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);
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,
251 var propertyChangedListenerImpl = new PropertyChangedListenerImpl();
252 webthing.observeProperty("target_position", propertyChangedListenerImpl);
253 webSocketFactory.webSocketRef.get().ignorePing.set(true);
256 Thread.sleep(pingPeriod.dividedBy(2).toMillis());
257 } catch (InterruptedException ignore) {
259 assertNull(errorHandler.errorRef.get());
262 Thread.sleep(pingPeriod.multipliedBy(4).toMillis());
263 } catch (InterruptedException ignore) {
265 assertTrue(errorHandler.errorRef.get().startsWith("connection seems to be broken (last message received at"));
268 public static String load(String name) throws Exception {
269 return new String(Files.readAllBytes(Paths.get(WebthingTest.class.getResource(name).toURI())));
272 public static ConsumedThingImpl createTestWebthing(String uri, HttpClient httpClient) throws IOException {
273 return createTestWebthing(uri, httpClient, (String) -> {
274 }, new TestWebsocketConnectionFactory());
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);
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));
288 public static class TestWebsocketConnectionFactory implements WebSocketConnectionFactory {
289 public final AtomicReference<WebSocketImpl> webSocketRef = new AtomicReference<>();
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;
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);
307 WebSocketImpl(WebSocketConnectionImpl connection) {
308 this.listener = connection;
309 this.pongListener = connection;
313 public void close() {
317 public void close(@Nullable CloseStatus closeStatus) {
321 public void close(int statusCode, @Nullable String reason) {
325 public void disconnect() throws IOException {
329 public long getIdleTimeout() {
334 public InetSocketAddress getLocalAddress() {
335 return InetSocketAddress.createUnresolved("test", 23);
339 public WebSocketPolicy getPolicy() {
340 return WebSocketPolicy.newClientPolicy();
344 public String getProtocolVersion() {
349 public RemoteEndpoint getRemote() {
350 return new RemoteEndpoint() {
352 public void sendBytes(@Nullable ByteBuffer data) throws IOException {
356 public Future sendBytesByFuture(@Nullable ByteBuffer data) {
357 throw new UnsupportedOperationException();
361 public void sendBytes(@Nullable ByteBuffer data, @Nullable WriteCallback callback) {
365 public void sendPartialBytes(@Nullable ByteBuffer fragment, boolean isLast) throws IOException {
369 public void sendPartialString(@Nullable String fragment, boolean isLast) throws IOException {
373 public void sendPing(@Nullable ByteBuffer applicationData) throws IOException {
374 if (!ignorePing.get()) {
375 pongListener.onWebSocketPong(applicationData);
380 public void sendPong(@Nullable ByteBuffer applicationData) throws IOException {
384 public void sendString(@Nullable String text) throws IOException {
388 public Future sendStringByFuture(@Nullable String text) {
389 throw new UnsupportedOperationException();
393 public void sendString(@Nullable String text, @Nullable WriteCallback callback) {
397 public BatchMode getBatchMode() {
398 return BatchMode.AUTO;
402 public void setBatchMode(@Nullable BatchMode mode) {
406 public InetSocketAddress getInetSocketAddress() {
407 throw new UnsupportedOperationException();
411 public void flush() throws IOException {
415 public int getMaxOutgoingFrames() {
420 public void setMaxOutgoingFrames(int maxOutgoingFrames) {
426 public InetSocketAddress getRemoteAddress() {
427 return InetSocketAddress.createUnresolved("test", 12);
431 public UpgradeRequest getUpgradeRequest() {
432 throw new UnsupportedOperationException();
436 public UpgradeResponse getUpgradeResponse() {
437 throw new UnsupportedOperationException();
441 public boolean isOpen() {
446 public boolean isSecure() {
451 public void setIdleTimeout(long ms) {
455 public SuspendToken suspend() {
456 return new SuspendToken() {
459 public void resume() {
464 public void sendToClient(PropertyStatusMessage message) {
465 var data = GSON.toJson(message);
466 listener.onWebSocketText(data);
469 public void sendCloseToClient() {
470 listener.onWebSocketClose(200, "");
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);
478 return CompletableFuture.completedFuture(null);
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<>();
487 public void accept(String propertyName, Object value) {
488 propertyNameRef.set(propertyName);
493 public static class ErrorHandler implements Consumer<String> {
494 public final AtomicReference<String> errorRef = new AtomicReference<>();
497 public void accept(String error) {