2 * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. 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.netconf.client.mdsal;
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;
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;
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;
68 @ExtendWith(MockitoExtension.class)
69 class NetconfDeviceCommunicatorTest {
70 private static final SessionIdType SESSION_ID = new SessionIdType(Uint32.ONE);
73 private RemoteDevice<NetconfDeviceCommunicator> mockDevice;
75 private ChannelFuture mockChannelFuture;
77 private Future operationFuture;
79 private NetconfClientSession spySession;
80 private NetconfDeviceCommunicator communicator;
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()));
91 doNothing().when(mockDevice).onRemoteSessionUp(any(NetconfSessionPreferences.class),
92 any(NetconfDeviceCommunicator.class));
93 communicator.onSessionUp(spySession);
96 private ListenableFuture<RpcResult<NetconfMessage>> sendRequest() {
97 return sendRequest(UUID.randomUUID().toString(), true);
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);
109 doReturn(mockChannelFuture).when(mockChannelFuture)
110 .addListener(any(GenericFutureListener.class));
111 doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
113 final var resultFuture = communicator.sendRequest(message, QName.create("", "mockRpc"));
115 assertNotNull(resultFuture, "ListenableFuture is null");
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);
129 final var resultFuture =
130 communicator.sendRequest(message, QName.create("", "mockRpc"));
132 assertNotNull(resultFuture, "ListenableFuture is null");
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(),
144 doReturn(serverCapabilities).when(spySession).getServerCapabilities();
146 final var netconfSessionPreferences = ArgumentCaptor.forClass(NetconfSessionPreferences.class);
147 doNothing().when(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
149 communicator.onSessionUp(spySession);
151 verify(spySession).getServerCapabilities();
152 verify(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
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());
165 @SuppressWarnings("unchecked")
167 void testOnSessionDown() {
168 assertTimeout(Duration.ofMillis(5000), () -> {
171 final var resultFuture1 = sendRequest();
172 final var resultFuture2 = sendRequest();
174 doNothing().when(mockDevice).onRemoteSessionDown();
176 communicator.onSessionDown(spySession, new Exception("mock ex"));
178 verifyErrorRpcResult(resultFuture1.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
179 verifyErrorRpcResult(resultFuture2.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
181 verify(mockDevice).onRemoteSessionDown();
185 communicator.onSessionDown(spySession, new Exception("mock ex"));
187 verify(mockDevice, never()).onRemoteSessionDown();
192 void testOnSessionTerminated() throws Exception {
195 final ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest();
197 doNothing().when(mockDevice).onRemoteSessionDown();
199 final var reasonText = "testing terminate";
200 final var reason = new NetconfTerminationReason(reasonText);
201 communicator.onSessionTerminated(spySession, reason);
203 final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
204 assertEquals(reasonText, rpcError.getMessage(), "RpcError message");
206 verify(mockDevice).onRemoteSessionDown();
211 communicator.close();
212 verify(mockDevice, never()).onRemoteSessionDown();
215 @SuppressWarnings("unchecked")
217 void testSendRequest() throws Exception {
220 final var message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
221 final var rpc = QName.create("", "mockRpc");
223 final var futureListener = ArgumentCaptor.forClass(GenericFutureListener.class);
225 doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
226 doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
228 final var resultFuture = communicator.sendRequest(message, rpc);
230 verify(spySession).sendMessage(same(message));
232 assertNotNull(resultFuture, "ListenableFuture is null");
234 verify(mockChannelFuture).addListener(futureListener.capture());
235 doReturn(null).when(operationFuture).cause();
236 futureListener.getValue().operationComplete(operationFuture);
238 // verify it is not cancelled nor has an error set
239 assertFalse(resultFuture.isDone());
243 void testSendRequestWithNoSession() throws Exception {
244 final var message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
245 final var rpc = QName.create("", "mockRpc");
247 final var resultFuture = communicator.sendRequest(message, rpc);
249 assertNotNull(resultFuture, "ListenableFuture is null");
251 // Should have an immediate result
252 final var rpcResult = Futures.getDone(resultFuture);
254 verifyErrorRpcResult(rpcResult, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
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);
266 return new NetconfMessage(doc);
269 @SuppressWarnings("unchecked")
271 void testSendRequestWithWithSendFailure() throws Exception {
274 final var message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
275 final var rpc = QName.create("", "mockRpc");
277 final var futureListener = ArgumentCaptor.forClass(GenericFutureListener.class);
279 doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
280 doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
282 final var resultFuture = communicator.sendRequest(message, rpc);
284 assertNotNull(resultFuture, "ListenableFuture is null");
286 verify(mockChannelFuture).addListener(futureListener.capture());
288 doReturn(new Exception("mock error")).when(operationFuture).cause();
289 futureListener.getValue().operationComplete(operationFuture);
291 // Should have an immediate result
292 final var rpcResult = Futures.getDone(resultFuture);
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\"");
299 //Test scenario verifying whether missing message is handled
301 void testOnMissingResponseMessage() throws Exception {
305 final var messageID = UUID.randomUUID().toString();
306 final var resultFuture = sendRequest(messageID, true);
308 //response messages 1,2 are omitted
309 communicator.onMessage(spySession, createSuccessResponseMessage(messageID));
311 verifyResponseMessage(resultFuture.get(), messageID);
315 void testOnSuccessfulResponseMessage() throws Exception {
318 final var messageID1 = UUID.randomUUID().toString();
319 final var resultFuture1 = sendRequest(messageID1, true);
321 final var messageID2 = UUID.randomUUID().toString();
322 final var resultFuture2 = sendRequest(messageID2, true);
324 communicator.onMessage(spySession, createSuccessResponseMessage(messageID1));
325 communicator.onMessage(spySession, createSuccessResponseMessage(messageID2));
327 verifyResponseMessage(resultFuture1.get(), messageID1);
328 verifyResponseMessage(resultFuture2.get(), messageID2);
332 void testOnResponseMessageWithError() throws Exception {
335 final var messageID = UUID.randomUUID().toString();
336 final var resultFuture = sendRequest(messageID, true);
338 communicator.onMessage(spySession, createErrorResponseMessage(messageID));
340 final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.RPC, ErrorTag.MISSING_ATTRIBUTE);
341 assertEquals("Missing attribute", rpcError.getMessage(), "RpcError message");
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\"");
350 void testOnResponseMessageWithMultipleErrors() throws Exception {
353 final var messageID = UUID.randomUUID().toString();
354 final var resultFuture = sendRequest(messageID, true);
356 communicator.onMessage(spySession, createMultiErrorResponseMessage(messageID));
358 final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.OPERATION_FAILED);
360 final var errorInfo = rpcError.getInfo();
361 assertNotNull("RpcError info is null", errorInfo);
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));
372 void testOnResponseMessageWithWrongMessageID() throws Exception {
375 final var messageID = UUID.randomUUID().toString();
376 final var resultFuture = sendRequest(messageID, true);
378 communicator.onMessage(spySession, createSuccessResponseMessage(UUID.randomUUID().toString()));
380 final var rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
381 assertFalse(Strings.isNullOrEmpty(rpcError.getMessage()), "RpcError message non-empty");
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\"");
390 void testConcurrentMessageLimit() {
392 final var messageID = new ArrayList<String>();
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");
400 final var notWorkingMessageID = UUID.randomUUID().toString();
401 var resultFuture = sendRequestWithoutMocking(notWorkingMessageID, false);
402 assertFalse(resultFuture instanceof UncancellableFuture, "ListenableFuture is null");
404 communicator.onMessage(spySession, createSuccessResponseMessage(messageID.get(0)));
406 resultFuture = sendRequest(messageID.get(0), false);
407 assertNotNull(resultFuture, "ListenableFuture is null");
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 + "\">"
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"
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"
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"
437 final var bis = new ByteArrayInputStream(xmlStr.getBytes());
438 final var doc = UntrustedXML.newDocumentBuilder().parse(bis);
439 return new NetconfMessage(doc);
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 + "\">"
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>"
451 + " <bad-attribute>foo</bad-attribute>"
452 + " <bad-element>bar</bad-element>"
457 final var bis = new ByteArrayInputStream(xmlStr.getBytes());
458 final var doc = UntrustedXML.newDocumentBuilder().parse(bis);
459 return new NetconfMessage(doc);
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");
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");
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");