2 * Copyright © 2019 FRINX s.r.o. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.nb.rfc8040.streams.websockets;
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.assertTrue;
12 import static org.mockito.ArgumentMatchers.any;
13 import static org.mockito.ArgumentMatchers.anyBoolean;
14 import static org.mockito.ArgumentMatchers.anyString;
15 import static org.mockito.ArgumentMatchers.eq;
16 import static org.mockito.Mockito.mock;
17 import static org.mockito.Mockito.never;
18 import static org.mockito.Mockito.times;
19 import static org.mockito.Mockito.verify;
20 import static org.mockito.Mockito.verifyNoMoreInteractions;
21 import static org.mockito.Mockito.when;
23 import java.io.IOException;
24 import java.util.List;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jetty.websocket.api.RemoteEndpoint;
29 import org.eclipse.jetty.websocket.api.Session;
30 import org.junit.Test;
31 import org.mockito.ArgumentCaptor;
32 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.BaseListenerInterface;
34 public class WebSocketSessionHandlerTest {
36 private static final class WebSocketTestSessionState {
37 private final BaseListenerInterface listener;
38 private final ScheduledExecutorService executorService;
39 private final WebSocketSessionHandler webSocketSessionHandler;
40 private final int heartbeatInterval;
41 private final int maxFragmentSize;
42 private final ScheduledFuture pingFuture;
44 private WebSocketTestSessionState(final int maxFragmentSize, final int heartbeatInterval) {
45 listener = mock(BaseListenerInterface.class);
46 executorService = mock(ScheduledExecutorService.class);
47 this.heartbeatInterval = heartbeatInterval;
48 this.maxFragmentSize = maxFragmentSize;
49 webSocketSessionHandler = new WebSocketSessionHandler(executorService, listener, maxFragmentSize,
51 pingFuture = mock(ScheduledFuture.class);
52 when(executorService.scheduleWithFixedDelay(any(Runnable.class), eq((long) heartbeatInterval),
53 eq((long) heartbeatInterval), eq(TimeUnit.MILLISECONDS))).thenReturn(pingFuture);
58 public void onWebSocketConnectedWithEnabledPing() {
59 final int heartbeatInterval = 1000;
60 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(
61 1000, heartbeatInterval);
62 final Session session = mock(Session.class);
64 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
65 verify(webSocketTestSessionState.listener).addSubscriber(
66 webSocketTestSessionState.webSocketSessionHandler);
67 verify(webSocketTestSessionState.executorService).scheduleWithFixedDelay(any(Runnable.class),
68 eq((long) webSocketTestSessionState.heartbeatInterval),
69 eq((long) webSocketTestSessionState.heartbeatInterval), eq(TimeUnit.MILLISECONDS));
73 public void onWebSocketConnectedWithDisabledPing() {
74 final int heartbeatInterval = 0;
75 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(
76 1000, heartbeatInterval);
77 final Session session = mock(Session.class);
79 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
80 verify(webSocketTestSessionState.listener).addSubscriber(
81 webSocketTestSessionState.webSocketSessionHandler);
82 verifyNoMoreInteractions(webSocketTestSessionState.executorService);
86 public void onWebSocketConnectedWithAlreadyOpenSession() {
87 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
88 final Session session = mock(Session.class);
89 when(session.isOpen()).thenReturn(true);
91 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
92 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
93 verify(webSocketTestSessionState.listener, times(1)).addSubscriber(any());
97 public void onWebSocketClosedWithOpenSession() {
98 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(200, 10000);
99 final Session session = mock(Session.class);
101 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
102 verify(webSocketTestSessionState.listener).addSubscriber(
103 webSocketTestSessionState.webSocketSessionHandler);
105 webSocketTestSessionState.webSocketSessionHandler.onWebSocketClosed(200, "Simulated close");
106 verify(webSocketTestSessionState.listener).removeSubscriber(
107 webSocketTestSessionState.webSocketSessionHandler);
111 public void onWebSocketClosedWithNotInitialisedSession() {
112 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(300, 12000);
113 webSocketTestSessionState.webSocketSessionHandler.onWebSocketClosed(500, "Simulated close");
114 verifyNoMoreInteractions(webSocketTestSessionState.listener);
118 public void onWebSocketErrorWithEnabledPingAndLivingSession() {
119 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
120 final Session session = mock(Session.class);
121 when(session.isOpen()).thenReturn(true);
122 final Throwable sampleError = new IllegalStateException("Simulated error");
123 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
124 when(webSocketTestSessionState.pingFuture.isCancelled()).thenReturn(false);
125 when(webSocketTestSessionState.pingFuture.isDone()).thenReturn(false);
127 webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
128 verify(webSocketTestSessionState.listener).removeSubscriber(
129 webSocketTestSessionState.webSocketSessionHandler);
130 verify(session).close();
131 verify(webSocketTestSessionState.pingFuture).cancel(anyBoolean());
135 public void onWebSocketErrorWithEnabledPingAndDeadSession() {
136 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
137 final Session session = mock(Session.class);
138 when(session.isOpen()).thenReturn(false);
139 final Throwable sampleError = new IllegalStateException("Simulated error");
140 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
142 webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
143 verify(webSocketTestSessionState.listener).removeSubscriber(
144 webSocketTestSessionState.webSocketSessionHandler);
145 verify(session, never()).close();
146 verify(webSocketTestSessionState.pingFuture).cancel(anyBoolean());
150 public void onWebSocketErrorWithDisabledPingAndDeadSession() {
151 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
152 final Session session = mock(Session.class);
153 when(session.isOpen()).thenReturn(false);
154 final Throwable sampleError = new IllegalStateException("Simulated error");
155 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
156 when(webSocketTestSessionState.pingFuture.isCancelled()).thenReturn(false);
157 when(webSocketTestSessionState.pingFuture.isDone()).thenReturn(true);
159 webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
160 verify(webSocketTestSessionState.listener).removeSubscriber(
161 webSocketTestSessionState.webSocketSessionHandler);
162 verify(session, never()).close();
163 verify(webSocketTestSessionState.pingFuture, never()).cancel(anyBoolean());
167 public void sendDataMessageWithDisabledFragmentation() throws IOException {
168 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(0, 0);
169 final Session session = mock(Session.class);
170 final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
171 when(session.isOpen()).thenReturn(true);
172 when(session.getRemote()).thenReturn(remoteEndpoint);
173 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
175 final String testMessage = generateRandomStringOfLength(100);
176 webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
177 verify(remoteEndpoint).sendString(testMessage);
181 public void sendDataMessageWithDisabledFragAndDeadSession() {
182 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(0, 0);
183 final Session session = mock(Session.class);
184 final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
185 when(session.isOpen()).thenReturn(false);
186 when(session.getRemote()).thenReturn(remoteEndpoint);
187 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
189 final String testMessage = generateRandomStringOfLength(11);
190 webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
191 verifyNoMoreInteractions(remoteEndpoint);
195 public void sendDataMessageWithEnabledFragAndSmallMessage() throws IOException {
196 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
197 final Session session = mock(Session.class);
198 final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
199 when(session.isOpen()).thenReturn(true);
200 when(session.getRemote()).thenReturn(remoteEndpoint);
201 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
203 // in both cases, fragmentation should not be applied
204 final String testMessage1 = generateRandomStringOfLength(100);
205 final String testMessage2 = generateRandomStringOfLength(50);
206 webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage1);
207 webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage2);
208 verify(remoteEndpoint).sendString(testMessage1);
209 verify(remoteEndpoint).sendString(testMessage2);
210 verify(remoteEndpoint, never()).sendPartialString(anyString(), anyBoolean());
214 public void sendDataMessageWithZeroLength() {
215 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
216 final Session session = mock(Session.class);
217 final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
218 when(session.isOpen()).thenReturn(true);
219 when(session.getRemote()).thenReturn(remoteEndpoint);
220 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
222 webSocketTestSessionState.webSocketSessionHandler.sendDataMessage("");
223 verifyNoMoreInteractions(remoteEndpoint);
227 public void sendDataMessageWithEnabledFragAndLargeMessage1() throws IOException {
228 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
229 final Session session = mock(Session.class);
230 final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
231 when(session.isOpen()).thenReturn(true);
232 when(session.getRemote()).thenReturn(remoteEndpoint);
233 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
235 // there should be 10 fragments of length 100 characters
236 final String testMessage = generateRandomStringOfLength(1000);
237 webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
238 final ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
239 final ArgumentCaptor<Boolean> isLastCaptor = ArgumentCaptor.forClass(Boolean.class);
240 verify(remoteEndpoint, times(10)).sendPartialString(
241 messageCaptor.capture(), isLastCaptor.capture());
243 final List<String> allMessages = messageCaptor.getAllValues();
244 final List<Boolean> isLastFlags = isLastCaptor.getAllValues();
245 assertTrue(allMessages.stream().allMatch(s -> s.length() == webSocketTestSessionState.maxFragmentSize));
246 assertTrue(isLastFlags.subList(0, 9).stream().noneMatch(isLast -> isLast));
247 assertTrue(isLastFlags.get(9));
251 public void sendDataMessageWithEnabledFragAndLargeMessage2() throws IOException {
252 final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
253 final Session session = mock(Session.class);
254 final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
255 when(session.isOpen()).thenReturn(true);
256 when(session.getRemote()).thenReturn(remoteEndpoint);
257 webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
259 // there should be 10 fragments, the last fragment should be the shortest one
260 final String testMessage = generateRandomStringOfLength(950);
261 webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
262 final ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
263 final ArgumentCaptor<Boolean> isLastCaptor = ArgumentCaptor.forClass(Boolean.class);
264 verify(remoteEndpoint, times(10)).sendPartialString(
265 messageCaptor.capture(), isLastCaptor.capture());
267 final List<String> allMessages = messageCaptor.getAllValues();
268 final List<Boolean> isLastFlags = isLastCaptor.getAllValues();
269 assertTrue(allMessages.subList(0, 9).stream().allMatch(s ->
270 s.length() == webSocketTestSessionState.maxFragmentSize));
271 assertEquals(50, allMessages.get(9).length());
272 assertTrue(isLastFlags.subList(0, 9).stream().noneMatch(isLast -> isLast));
273 assertTrue(isLastFlags.get(9));
276 private static String generateRandomStringOfLength(final int length) {
277 final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvxyz";
278 final StringBuilder sb = new StringBuilder(length);
279 for (int i = 0; i < length; i++) {
280 int index = (int) (alphabet.length() * Math.random());
281 sb.append(alphabet.charAt(index));
283 return sb.toString();