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