Migrate netconf-client-mdsal tests to JUnit5
[netconf.git] / plugins / netconf-client-mdsal / src / test / java / org / opendaylight / netconf / client / mdsal / NetconfDeviceCommunicatorTest.java
1 /*
2  * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  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.netconf.client.mdsal;
9
10 import static org.junit.jupiter.api.Assertions.assertEquals;
11 import static org.junit.jupiter.api.Assertions.assertFalse;
12 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
13 import static org.junit.jupiter.api.Assertions.assertNotNull;
14 import static org.junit.jupiter.api.Assertions.assertTimeout;
15 import static org.junit.jupiter.api.Assertions.assertTrue;
16 import static org.mockito.ArgumentMatchers.any;
17 import static org.mockito.ArgumentMatchers.eq;
18 import static org.mockito.ArgumentMatchers.same;
19 import static org.mockito.Mockito.doNothing;
20 import static org.mockito.Mockito.doReturn;
21 import static org.mockito.Mockito.mock;
22 import static org.mockito.Mockito.never;
23 import static org.mockito.Mockito.reset;
24 import static org.mockito.Mockito.spy;
25 import static org.mockito.Mockito.verify;
26
27 import com.google.common.base.CharMatcher;
28 import com.google.common.base.Strings;
29 import com.google.common.util.concurrent.Futures;
30 import com.google.common.util.concurrent.ListenableFuture;
31 import io.netty.channel.Channel;
32 import io.netty.channel.ChannelFuture;
33 import io.netty.util.concurrent.Future;
34 import io.netty.util.concurrent.GenericFutureListener;
35 import java.io.ByteArrayInputStream;
36 import java.net.InetSocketAddress;
37 import java.time.Duration;
38 import java.util.ArrayList;
39 import java.util.Set;
40 import java.util.UUID;
41 import org.junit.jupiter.api.BeforeEach;
42 import org.junit.jupiter.api.Test;
43 import org.junit.jupiter.api.extension.ExtendWith;
44 import org.mockito.ArgumentCaptor;
45 import org.mockito.Mock;
46 import org.mockito.junit.jupiter.MockitoExtension;
47 import org.opendaylight.netconf.api.CapabilityURN;
48 import org.opendaylight.netconf.api.NamespaceURN;
49 import org.opendaylight.netconf.api.NetconfTerminationReason;
50 import org.opendaylight.netconf.api.messages.NetconfMessage;
51 import org.opendaylight.netconf.api.messages.RpcReplyMessage;
52 import org.opendaylight.netconf.client.NetconfClientSession;
53 import org.opendaylight.netconf.client.NetconfClientSessionListener;
54 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
55 import org.opendaylight.netconf.client.mdsal.api.RemoteDevice;
56 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
57 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.SessionIdType;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.NetconfState;
59 import org.opendaylight.yangtools.util.xml.UntrustedXML;
60 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
61 import org.opendaylight.yangtools.yang.common.ErrorTag;
62 import org.opendaylight.yangtools.yang.common.ErrorType;
63 import org.opendaylight.yangtools.yang.common.QName;
64 import org.opendaylight.yangtools.yang.common.RpcError;
65 import org.opendaylight.yangtools.yang.common.RpcResult;
66 import org.opendaylight.yangtools.yang.common.Uint32;
67
68 @ExtendWith(MockitoExtension.class)
69 class NetconfDeviceCommunicatorTest {
70     private static final SessionIdType SESSION_ID = new SessionIdType(Uint32.ONE);
71
72     @Mock
73     private RemoteDevice<NetconfDeviceCommunicator> mockDevice;
74     @Mock
75     private ChannelFuture mockChannelFuture;
76     @Mock
77     private Future operationFuture;
78
79     private NetconfClientSession spySession;
80     private NetconfDeviceCommunicator communicator;
81
82     @BeforeEach
83     void setUp() {
84         communicator = new NetconfDeviceCommunicator(
85                 new RemoteDeviceId("test", InetSocketAddress.createUnresolved("localhost", 22)), mockDevice, 10);
86         spySession = spy(new NetconfClientSession(mock(NetconfClientSessionListener.class), mock(Channel.class),
87             SESSION_ID, Set.of()));
88     }
89
90     void setupSession() {
91         doNothing().when(mockDevice).onRemoteSessionUp(any(NetconfSessionPreferences.class),
92                 any(NetconfDeviceCommunicator.class));
93         communicator.onSessionUp(spySession);
94     }
95
96     private ListenableFuture<RpcResult<NetconfMessage>> sendRequest() {
97         return sendRequest(UUID.randomUUID().toString(), true);
98     }
99
100     @SuppressWarnings("unchecked")
101     private ListenableFuture<RpcResult<NetconfMessage>> sendRequest(final String messageID,
102             final boolean doLastTest) {
103         final var doc = UntrustedXML.newDocumentBuilder().newDocument();
104         final var element = doc.createElement("request");
105         element.setAttribute("message-id", messageID);
106         doc.appendChild(element);
107         final var message = new NetconfMessage(doc);
108
109         doReturn(mockChannelFuture).when(mockChannelFuture)
110                 .addListener(any(GenericFutureListener.class));
111         doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
112
113         final var resultFuture = communicator.sendRequest(message, QName.create("", "mockRpc"));
114         if (doLastTest) {
115             assertNotNull(resultFuture, "ListenableFuture is null");
116         }
117         return resultFuture;
118     }
119
120     @SuppressWarnings("unchecked")
121     private ListenableFuture<RpcResult<NetconfMessage>> sendRequestWithoutMocking(final String messageID,
122         final boolean doLastTest) {
123         final var doc = UntrustedXML.newDocumentBuilder().newDocument();
124         final var element = doc.createElement("request");
125         element.setAttribute("message-id", messageID);
126         doc.appendChild(element);
127         final var message = new NetconfMessage(doc);
128
129         final var resultFuture =
130             communicator.sendRequest(message, QName.create("", "mockRpc"));
131         if (doLastTest) {
132             assertNotNull(resultFuture, "ListenableFuture is null");
133         }
134         return resultFuture;
135     }
136
137     @Test
138     void testOnSessionUp() {
139         final var testCapability = "urn:opendaylight:params:xml:ns:test?module=test-module&revision=2014-06-02";
140         final var serverCapabilities = Set.of(
141             CapabilityURN.ROLLBACK_ON_ERROR,
142             NetconfState.QNAME.getNamespace().toString(),
143             testCapability);
144         doReturn(serverCapabilities).when(spySession).getServerCapabilities();
145
146         final var netconfSessionPreferences = ArgumentCaptor.forClass(NetconfSessionPreferences.class);
147         doNothing().when(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
148
149         communicator.onSessionUp(spySession);
150
151         verify(spySession).getServerCapabilities();
152         verify(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
153
154         NetconfSessionPreferences actualCapabilites = netconfSessionPreferences.getValue();
155         assertTrue(actualCapabilites.containsNonModuleCapability(
156                 "urn:ietf:params:netconf:capability:rollback-on-error:1.0"));
157         assertFalse(actualCapabilites.containsNonModuleCapability(testCapability));
158         assertEquals(Set.of(QName.create("urn:opendaylight:params:xml:ns:test", "2014-06-02", "test-module")),
159                 actualCapabilites.moduleBasedCaps().keySet());
160         assertTrue(actualCapabilites.isRollbackSupported());
161         assertTrue(actualCapabilites.isMonitoringSupported());
162         assertEquals(SESSION_ID, actualCapabilites.sessionId());
163     }
164
165     @SuppressWarnings("unchecked")
166     @Test
167     void testOnSessionDown() {
168         assertTimeout(Duration.ofMillis(5000), () -> {
169             setupSession();
170
171             final var resultFuture1 = sendRequest();
172             final var resultFuture2 = sendRequest();
173
174             doNothing().when(mockDevice).onRemoteSessionDown();
175
176             communicator.onSessionDown(spySession, new Exception("mock ex"));
177
178             verifyErrorRpcResult(resultFuture1.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
179             verifyErrorRpcResult(resultFuture2.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
180
181             verify(mockDevice).onRemoteSessionDown();
182
183             reset(mockDevice);
184
185             communicator.onSessionDown(spySession, new Exception("mock ex"));
186
187             verify(mockDevice, never()).onRemoteSessionDown();
188         });
189     }
190
191     @Test
192     void testOnSessionTerminated() throws Exception {
193         setupSession();
194
195         final ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest();
196
197         doNothing().when(mockDevice).onRemoteSessionDown();
198
199         final var reasonText = "testing terminate";
200         final var reason = new NetconfTerminationReason(reasonText);
201         communicator.onSessionTerminated(spySession, reason);
202
203         final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
204         assertEquals(reasonText, rpcError.getMessage(), "RpcError message");
205
206         verify(mockDevice).onRemoteSessionDown();
207     }
208
209     @Test
210     void testClose() {
211         communicator.close();
212         verify(mockDevice, never()).onRemoteSessionDown();
213     }
214
215     @SuppressWarnings("unchecked")
216     @Test
217     void testSendRequest() throws Exception {
218         setupSession();
219
220         final var message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
221         final var rpc = QName.create("", "mockRpc");
222
223         final var futureListener = ArgumentCaptor.forClass(GenericFutureListener.class);
224
225         doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
226         doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
227
228         final var resultFuture = communicator.sendRequest(message, rpc);
229
230         verify(spySession).sendMessage(same(message));
231
232         assertNotNull(resultFuture, "ListenableFuture is null");
233
234         verify(mockChannelFuture).addListener(futureListener.capture());
235         doReturn(null).when(operationFuture).cause();
236         futureListener.getValue().operationComplete(operationFuture);
237
238         // verify it is not cancelled nor has an error set
239         assertFalse(resultFuture.isDone());
240     }
241
242     @Test
243     void testSendRequestWithNoSession() throws Exception {
244         final var message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
245         final var rpc = QName.create("", "mockRpc");
246
247         final var resultFuture = communicator.sendRequest(message, rpc);
248
249         assertNotNull(resultFuture, "ListenableFuture is null");
250
251         // Should have an immediate result
252         final var rpcResult = Futures.getDone(resultFuture);
253
254         verifyErrorRpcResult(rpcResult, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
255     }
256
257     private static NetconfMessage createSuccessResponseMessage(final String messageID) {
258         final var doc = UntrustedXML.newDocumentBuilder().newDocument();
259         final var rpcReply = doc.createElementNS(NamespaceURN.BASE, RpcReplyMessage.ELEMENT_NAME);
260         rpcReply.setAttribute("message-id", messageID);
261         final var element = doc.createElementNS("ns", "data");
262         element.setTextContent(messageID);
263         rpcReply.appendChild(element);
264         doc.appendChild(rpcReply);
265
266         return new NetconfMessage(doc);
267     }
268
269     @SuppressWarnings("unchecked")
270     @Test
271     void testSendRequestWithWithSendFailure() throws Exception {
272         setupSession();
273
274         final var message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
275         final var rpc = QName.create("", "mockRpc");
276
277         final var futureListener = ArgumentCaptor.forClass(GenericFutureListener.class);
278
279         doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
280         doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
281
282         final var resultFuture = communicator.sendRequest(message, rpc);
283
284         assertNotNull(resultFuture, "ListenableFuture is null");
285
286         verify(mockChannelFuture).addListener(futureListener.capture());
287
288         doReturn(new Exception("mock error")).when(operationFuture).cause();
289         futureListener.getValue().operationComplete(operationFuture);
290
291         // Should have an immediate result
292         final var rpcResult = Futures.getDone(resultFuture);
293
294         final var rpcError = verifyErrorRpcResult(rpcResult, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
295         assertEquals(true, rpcError.getMessage().contains("mock error"),
296             "RpcError message contains \"mock error\"");
297     }
298
299     //Test scenario verifying whether missing message is handled
300     @Test
301     void testOnMissingResponseMessage() throws Exception {
302
303         setupSession();
304
305         final var messageID = UUID.randomUUID().toString();
306         final var resultFuture = sendRequest(messageID, true);
307
308         //response messages 1,2 are omitted
309         communicator.onMessage(spySession, createSuccessResponseMessage(messageID));
310
311         verifyResponseMessage(resultFuture.get(), messageID);
312     }
313
314     @Test
315     void testOnSuccessfulResponseMessage() throws Exception {
316         setupSession();
317
318         final var messageID1 = UUID.randomUUID().toString();
319         final var resultFuture1 = sendRequest(messageID1, true);
320
321         final var messageID2 = UUID.randomUUID().toString();
322         final var resultFuture2 = sendRequest(messageID2, true);
323
324         communicator.onMessage(spySession, createSuccessResponseMessage(messageID1));
325         communicator.onMessage(spySession, createSuccessResponseMessage(messageID2));
326
327         verifyResponseMessage(resultFuture1.get(), messageID1);
328         verifyResponseMessage(resultFuture2.get(), messageID2);
329     }
330
331     @Test
332     void testOnResponseMessageWithError() throws Exception {
333         setupSession();
334
335         final var messageID = UUID.randomUUID().toString();
336         final var resultFuture = sendRequest(messageID, true);
337
338         communicator.onMessage(spySession, createErrorResponseMessage(messageID));
339
340         final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.RPC, ErrorTag.MISSING_ATTRIBUTE);
341         assertEquals("Missing attribute", rpcError.getMessage(), "RpcError message");
342
343         final var errorInfo = rpcError.getInfo();
344         assertNotNull("RpcError info is null", errorInfo);
345         assertTrue(errorInfo.contains("<bad-attribute>foo</bad-attribute>"), "Error info contains \"foo\"");
346         assertTrue(errorInfo.contains("<bad-element>bar</bad-element>"), "Error info contains \"bar\"");
347     }
348
349     @Test
350     void testOnResponseMessageWithMultipleErrors() throws Exception {
351         setupSession();
352
353         final var messageID = UUID.randomUUID().toString();
354         final var resultFuture = sendRequest(messageID, true);
355
356         communicator.onMessage(spySession, createMultiErrorResponseMessage(messageID));
357
358         final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.OPERATION_FAILED);
359
360         final var errorInfo = rpcError.getInfo();
361         assertNotNull("RpcError info is null", errorInfo);
362
363         final var errorInfoMessages = rpcError.getInfo();
364         final var errMsg1 = "Number of member links configured, i.e [1], "
365                 + "for interface [ae0]is lesser than the required minimum [2].";
366         final var errMsg2 = "configuration check-out failed";
367         assertTrue(errorInfoMessages.contains(errMsg1) && errorInfoMessages.contains(errMsg2),
368             String.format("Error info contains \"%s\" or \"%s\'", errMsg1, errMsg2));
369     }
370
371     @Test
372     void testOnResponseMessageWithWrongMessageID() throws Exception {
373         setupSession();
374
375         final var messageID = UUID.randomUUID().toString();
376         final var resultFuture = sendRequest(messageID, true);
377
378         communicator.onMessage(spySession, createSuccessResponseMessage(UUID.randomUUID().toString()));
379
380         final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
381         assertFalse(Strings.isNullOrEmpty(rpcError.getMessage()), "RpcError message non-empty");
382
383         final var errorInfo = rpcError.getInfo();
384         assertNotNull("RpcError info is null", errorInfo);
385         assertTrue(errorInfo.contains("actual-message-id"), "Error info contains \"actual-message-id\"");
386         assertTrue(errorInfo.contains("expected-message-id"), "Error info contains \"expected-message-id\"");
387     }
388
389     @Test
390     void testConcurrentMessageLimit() {
391         setupSession();
392         final var messageID = new ArrayList<String>();
393
394         for (int i = 0; i < 10; i++) {
395             messageID.add(UUID.randomUUID().toString());
396             final var resultFuture = sendRequest(messageID.get(i), false);
397             assertInstanceOf(UncancellableFuture.class, resultFuture, "ListenableFuture is null");
398         }
399
400         final var notWorkingMessageID = UUID.randomUUID().toString();
401         var resultFuture = sendRequestWithoutMocking(notWorkingMessageID, false);
402         assertFalse(resultFuture instanceof UncancellableFuture, "ListenableFuture is null");
403
404         communicator.onMessage(spySession, createSuccessResponseMessage(messageID.get(0)));
405
406         resultFuture = sendRequest(messageID.get(0), false);
407         assertNotNull(resultFuture, "ListenableFuture is null");
408     }
409
410     private static NetconfMessage createMultiErrorResponseMessage(final String messageID) throws Exception {
411         // multiple rpc-errors which simulate actual response like in NETCONF-666
412         final var xmlStr = "<nc:rpc-reply xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\" "
413                 + "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" "
414                 + "xmlns:junos=\"http://xml.juniper.net/junos/18.4R1/junos\" message-id=\"" + messageID + "\">"
415                 + "<nc:rpc-error>\n"
416                 + "<nc:error-type>protocol</nc:error-type>\n"
417                 + "<nc:error-tag>operation-failed</nc:error-tag>\n"
418                 + "<nc:error-severity>error</nc:error-severity>\n"
419                 + "<source-daemon>\n"
420                 + "dcd\n"
421                 + "</source-daemon>\n"
422                 + "<nc:error-message>\n"
423                 + "Number of member links configured, i.e [1], "
424                 + "for interface [ae0]is lesser than the required minimum [2].\n"
425                 + "</nc:error-message>\n"
426                 + "</nc:rpc-error>\n"
427                 + "<nc:rpc-error>\n"
428                 + "<nc:error-type>protocol</nc:error-type>\n"
429                 + "<nc:error-tag>operation-failed</nc:error-tag>\n"
430                 + "<nc:error-severity>error</nc:error-severity>\n"
431                 + "<nc:error-message>\n"
432                 + "configuration check-out failed\n"
433                 + "</nc:error-message>\n"
434                 + "</nc:rpc-error>\n"
435                 + "</nc:rpc-reply>";
436
437         final var bis = new ByteArrayInputStream(xmlStr.getBytes());
438         final var doc = UntrustedXML.newDocumentBuilder().parse(bis);
439         return new NetconfMessage(doc);
440     }
441
442     private static NetconfMessage createErrorResponseMessage(final String messageID) throws Exception {
443         final var xmlStr = "<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\""
444                 + "           message-id=\"" + messageID + "\">"
445                 + "  <rpc-error>"
446                 + "    <error-type>rpc</error-type>"
447                 + "    <error-tag>missing-attribute</error-tag>"
448                 + "    <error-severity>error</error-severity>"
449                 + "    <error-message>Missing attribute</error-message>"
450                 + "    <error-info>"
451                 + "      <bad-attribute>foo</bad-attribute>"
452                 + "      <bad-element>bar</bad-element>"
453                 + "    </error-info>"
454                 + "  </rpc-error>"
455                 + "</rpc-reply>";
456
457         final var bis = new ByteArrayInputStream(xmlStr.getBytes());
458         final var doc = UntrustedXML.newDocumentBuilder().parse(bis);
459         return new NetconfMessage(doc);
460     }
461
462     private static void verifyResponseMessage(final RpcResult<NetconfMessage> rpcResult, final String dataText) {
463         assertNotNull(rpcResult, "RpcResult is null");
464         assertTrue(rpcResult.isSuccessful(), "isSuccessful");
465         final var messageResult = rpcResult.getResult();
466         assertNotNull(messageResult, "getResult");
467     }
468
469     private static RpcError verifyErrorRpcResult(final RpcResult<NetconfMessage> rpcResult,
470             final ErrorType expErrorType, final ErrorTag expErrorTag) {
471         assertNotNull(rpcResult, "RpcResult is null");
472         assertFalse(rpcResult.isSuccessful(), "isSuccessful");
473         assertNotNull(rpcResult.getErrors(), "RpcResult errors is null");
474         assertEquals(1, rpcResult.getErrors().size(), "Errors size");
475         final var rpcError = rpcResult.getErrors().iterator().next();
476         assertEquals(ErrorSeverity.ERROR, rpcError.getSeverity(), "getErrorSeverity");
477         assertEquals(expErrorType, rpcError.getErrorType(), "getErrorType");
478         assertEquals(expErrorTag, rpcError.getTag(), "getErrorTag");
479
480         final var msg = rpcError.getMessage();
481         assertNotNull("getMessage is null", msg);
482         assertFalse(msg.isEmpty(), "getMessage is empty");
483         assertFalse(CharMatcher.whitespace().matchesAllOf(msg), "getMessage is blank");
484         return rpcError;
485     }
486 }