e12b1bb4dea349e70558e1fd8ffa1e6f387c58e0
[netconf.git] / restconf / restconf-nb-rfc8040 / src / test / java / org / opendaylight / restconf / nb / rfc8040 / streams / websockets / WebSocketSessionHandlerTest.java
1 /*
2  * Copyright © 2019 FRINX s.r.o. All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.restconf.nb.rfc8040.streams.websockets;
10
11 import java.io.IOException;
12 import java.util.List;
13 import java.util.concurrent.ScheduledExecutorService;
14 import java.util.concurrent.ScheduledFuture;
15 import java.util.concurrent.TimeUnit;
16 import org.eclipse.jetty.websocket.api.RemoteEndpoint;
17 import org.eclipse.jetty.websocket.api.Session;
18 import org.junit.Assert;
19 import org.junit.Test;
20 import org.mockito.ArgumentCaptor;
21 import org.mockito.Mockito;
22 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.BaseListenerInterface;
23
24 public class WebSocketSessionHandlerTest {
25
26     private static final class WebSocketTestSessionState {
27         private final BaseListenerInterface listener;
28         private final ScheduledExecutorService executorService;
29         private final WebSocketSessionHandler webSocketSessionHandler;
30         private final int heartbeatInterval;
31         private final int maxFragmentSize;
32         private final ScheduledFuture pingFuture;
33
34         private WebSocketTestSessionState(final int maxFragmentSize, final int heartbeatInterval) {
35             listener = Mockito.mock(BaseListenerInterface.class);
36             executorService = Mockito.mock(ScheduledExecutorService.class);
37             this.heartbeatInterval = heartbeatInterval;
38             this.maxFragmentSize = maxFragmentSize;
39             webSocketSessionHandler = new WebSocketSessionHandler(executorService, listener, maxFragmentSize,
40                     heartbeatInterval);
41             pingFuture = Mockito.mock(ScheduledFuture.class);
42             Mockito.when(executorService.scheduleWithFixedDelay(Mockito.any(Runnable.class),
43                     Mockito.eq((long) heartbeatInterval), Mockito.eq((long) heartbeatInterval),
44                     Mockito.eq(TimeUnit.MILLISECONDS))).thenReturn(pingFuture);
45         }
46     }
47
48     @Test
49     public void onWebSocketConnectedWithEnabledPing() {
50         final int heartbeatInterval = 1000;
51         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(
52                 1000, heartbeatInterval);
53         final Session session = Mockito.mock(Session.class);
54
55         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
56         Mockito.verify(webSocketTestSessionState.listener).addSubscriber(
57                 webSocketTestSessionState.webSocketSessionHandler);
58         Mockito.verify(webSocketTestSessionState.executorService).scheduleWithFixedDelay(Mockito.any(Runnable.class),
59                 Mockito.eq((long) webSocketTestSessionState.heartbeatInterval),
60                 Mockito.eq((long) webSocketTestSessionState.heartbeatInterval), Mockito.eq(TimeUnit.MILLISECONDS));
61     }
62
63     @Test
64     public void onWebSocketConnectedWithDisabledPing() {
65         final int heartbeatInterval = 0;
66         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(
67                 1000, heartbeatInterval);
68         final Session session = Mockito.mock(Session.class);
69
70         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
71         Mockito.verify(webSocketTestSessionState.listener).addSubscriber(
72                 webSocketTestSessionState.webSocketSessionHandler);
73         Mockito.verifyZeroInteractions(webSocketTestSessionState.executorService);
74     }
75
76     @Test
77     public void onWebSocketConnectedWithAlreadyOpenSession() {
78         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
79         final Session session = Mockito.mock(Session.class);
80         Mockito.when(session.isOpen()).thenReturn(true);
81
82         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
83         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
84         Mockito.verify(webSocketTestSessionState.listener, Mockito.times(1)).addSubscriber(Mockito.any());
85     }
86
87     @Test
88     public void onWebSocketClosedWithOpenSession() {
89         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(200, 10000);
90         final Session session = Mockito.mock(Session.class);
91
92         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
93         Mockito.verify(webSocketTestSessionState.listener).addSubscriber(
94                 webSocketTestSessionState.webSocketSessionHandler);
95
96         webSocketTestSessionState.webSocketSessionHandler.onWebSocketClosed(200, "Simulated close");
97         Mockito.verify(webSocketTestSessionState.listener).removeSubscriber(
98                 webSocketTestSessionState.webSocketSessionHandler);
99     }
100
101     @Test
102     public void onWebSocketClosedWithNotInitialisedSession() {
103         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(300, 12000);
104         webSocketTestSessionState.webSocketSessionHandler.onWebSocketClosed(500, "Simulated close");
105         Mockito.verifyZeroInteractions(webSocketTestSessionState.listener);
106     }
107
108     @Test
109     public void onWebSocketErrorWithEnabledPingAndLivingSession() {
110         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
111         final Session session = Mockito.mock(Session.class);
112         Mockito.when(session.isOpen()).thenReturn(true);
113         final Throwable sampleError = new IllegalStateException("Simulated error");
114         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
115         Mockito.when(webSocketTestSessionState.pingFuture.isCancelled()).thenReturn(false);
116         Mockito.when(webSocketTestSessionState.pingFuture.isDone()).thenReturn(false);
117
118         webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
119         Mockito.verify(webSocketTestSessionState.listener).removeSubscriber(
120                 webSocketTestSessionState.webSocketSessionHandler);
121         Mockito.verify(session).close();
122         Mockito.verify(webSocketTestSessionState.pingFuture).cancel(Mockito.anyBoolean());
123     }
124
125     @Test
126     public void onWebSocketErrorWithEnabledPingAndDeadSession() {
127         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
128         final Session session = Mockito.mock(Session.class);
129         Mockito.when(session.isOpen()).thenReturn(false);
130         final Throwable sampleError = new IllegalStateException("Simulated error");
131         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
132
133         webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
134         Mockito.verify(webSocketTestSessionState.listener).removeSubscriber(
135                 webSocketTestSessionState.webSocketSessionHandler);
136         Mockito.verify(session, Mockito.never()).close();
137         Mockito.verify(webSocketTestSessionState.pingFuture).cancel(Mockito.anyBoolean());
138     }
139
140     @Test
141     public void onWebSocketErrorWithDisabledPingAndDeadSession() {
142         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(150, 8000);
143         final Session session = Mockito.mock(Session.class);
144         Mockito.when(session.isOpen()).thenReturn(false);
145         final Throwable sampleError = new IllegalStateException("Simulated error");
146         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
147         Mockito.when(webSocketTestSessionState.pingFuture.isCancelled()).thenReturn(false);
148         Mockito.when(webSocketTestSessionState.pingFuture.isDone()).thenReturn(true);
149
150         webSocketTestSessionState.webSocketSessionHandler.onWebSocketError(sampleError);
151         Mockito.verify(webSocketTestSessionState.listener).removeSubscriber(
152                 webSocketTestSessionState.webSocketSessionHandler);
153         Mockito.verify(session, Mockito.never()).close();
154         Mockito.verify(webSocketTestSessionState.pingFuture, Mockito.never()).cancel(Mockito.anyBoolean());
155     }
156
157     @Test
158     public void sendDataMessageWithDisabledFragmentation() throws IOException {
159         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(0, 0);
160         final Session session = Mockito.mock(Session.class);
161         final RemoteEndpoint remoteEndpoint = Mockito.mock(RemoteEndpoint.class);
162         Mockito.when(session.isOpen()).thenReturn(true);
163         Mockito.when(session.getRemote()).thenReturn(remoteEndpoint);
164         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
165
166         final String testMessage = generateRandomStringOfLength(100);
167         webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
168         Mockito.verify(remoteEndpoint).sendString(testMessage);
169     }
170
171     @Test
172     public void sendDataMessageWithDisabledFragAndDeadSession() {
173         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(0, 0);
174         final Session session = Mockito.mock(Session.class);
175         final RemoteEndpoint remoteEndpoint = Mockito.mock(RemoteEndpoint.class);
176         Mockito.when(session.isOpen()).thenReturn(false);
177         Mockito.when(session.getRemote()).thenReturn(remoteEndpoint);
178         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
179
180         final String testMessage = generateRandomStringOfLength(11);
181         webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
182         Mockito.verifyZeroInteractions(remoteEndpoint);
183     }
184
185     @Test
186     public void sendDataMessageWithEnabledFragAndSmallMessage() throws IOException {
187         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
188         final Session session = Mockito.mock(Session.class);
189         final RemoteEndpoint remoteEndpoint = Mockito.mock(RemoteEndpoint.class);
190         Mockito.when(session.isOpen()).thenReturn(true);
191         Mockito.when(session.getRemote()).thenReturn(remoteEndpoint);
192         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
193
194         // in both cases, fragmentation should not be applied
195         final String testMessage1 = generateRandomStringOfLength(100);
196         final String testMessage2 = generateRandomStringOfLength(50);
197         webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage1);
198         webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage2);
199         Mockito.verify(remoteEndpoint).sendString(testMessage1);
200         Mockito.verify(remoteEndpoint).sendString(testMessage2);
201         Mockito.verify(remoteEndpoint, Mockito.never()).sendPartialString(Mockito.anyString(), Mockito.anyBoolean());
202     }
203
204     @Test
205     public void sendDataMessageWithZeroLength() {
206         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
207         final Session session = Mockito.mock(Session.class);
208         final RemoteEndpoint remoteEndpoint = Mockito.mock(RemoteEndpoint.class);
209         Mockito.when(session.isOpen()).thenReturn(true);
210         Mockito.when(session.getRemote()).thenReturn(remoteEndpoint);
211         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
212
213         webSocketTestSessionState.webSocketSessionHandler.sendDataMessage("");
214         Mockito.verifyZeroInteractions(remoteEndpoint);
215     }
216
217     @Test
218     public void sendDataMessageWithEnabledFragAndLargeMessage1() throws IOException {
219         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
220         final Session session = Mockito.mock(Session.class);
221         final RemoteEndpoint remoteEndpoint = Mockito.mock(RemoteEndpoint.class);
222         Mockito.when(session.isOpen()).thenReturn(true);
223         Mockito.when(session.getRemote()).thenReturn(remoteEndpoint);
224         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
225
226         // there should be 10 fragments of length 100 characters
227         final String testMessage = generateRandomStringOfLength(1000);
228         webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
229         final ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
230         final ArgumentCaptor<Boolean> isLastCaptor = ArgumentCaptor.forClass(Boolean.class);
231         Mockito.verify(remoteEndpoint, Mockito.times(10)).sendPartialString(
232                 messageCaptor.capture(), isLastCaptor.capture());
233
234         final List<String> allMessages = messageCaptor.getAllValues();
235         final List<Boolean> isLastFlags = isLastCaptor.getAllValues();
236         Assert.assertTrue(allMessages.stream().allMatch(s -> s.length() == webSocketTestSessionState.maxFragmentSize));
237         Assert.assertTrue(isLastFlags.subList(0, 9).stream().noneMatch(isLast -> isLast));
238         Assert.assertTrue(isLastFlags.get(9));
239     }
240
241     @Test
242     public void sendDataMessageWithEnabledFragAndLargeMessage2() throws IOException {
243         final WebSocketTestSessionState webSocketTestSessionState = new WebSocketTestSessionState(100, 0);
244         final Session session = Mockito.mock(Session.class);
245         final RemoteEndpoint remoteEndpoint = Mockito.mock(RemoteEndpoint.class);
246         Mockito.when(session.isOpen()).thenReturn(true);
247         Mockito.when(session.getRemote()).thenReturn(remoteEndpoint);
248         webSocketTestSessionState.webSocketSessionHandler.onWebSocketConnected(session);
249
250         // there should be 10 fragments, the last fragment should be the shortest one
251         final String testMessage = generateRandomStringOfLength(950);
252         webSocketTestSessionState.webSocketSessionHandler.sendDataMessage(testMessage);
253         final ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
254         final ArgumentCaptor<Boolean> isLastCaptor = ArgumentCaptor.forClass(Boolean.class);
255         Mockito.verify(remoteEndpoint, Mockito.times(10)).sendPartialString(
256                 messageCaptor.capture(), isLastCaptor.capture());
257
258         final List<String> allMessages = messageCaptor.getAllValues();
259         final List<Boolean> isLastFlags = isLastCaptor.getAllValues();
260         Assert.assertTrue(allMessages.subList(0, 9).stream().allMatch(s ->
261                 s.length() == webSocketTestSessionState.maxFragmentSize));
262         Assert.assertEquals(50, allMessages.get(9).length());
263         Assert.assertTrue(isLastFlags.subList(0, 9).stream().noneMatch(isLast -> isLast));
264         Assert.assertTrue(isLastFlags.get(9));
265     }
266
267     private static String generateRandomStringOfLength(final int length) {
268         final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvxyz";
269         final StringBuilder sb = new StringBuilder(length);
270         for (int i = 0; i < length; i++) {
271             int index = (int) (alphabet.length() * Math.random());
272             sb.append(alphabet.charAt(index));
273         }
274         return sb.toString();
275     }
276 }