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