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.Assert.assertEquals;
11 import static org.junit.Assert.assertFalse;
12 import static org.junit.Assert.assertNotNull;
13 import static org.junit.Assert.assertTrue;
14 import static org.mockito.ArgumentMatchers.any;
15 import static org.mockito.ArgumentMatchers.eq;
16 import static org.mockito.ArgumentMatchers.same;
17 import static org.mockito.Mockito.doNothing;
18 import static org.mockito.Mockito.doReturn;
19 import static org.mockito.Mockito.mock;
20 import static org.mockito.Mockito.never;
21 import static org.mockito.Mockito.reset;
22 import static org.mockito.Mockito.spy;
23 import static org.mockito.Mockito.verify;
25 import com.google.common.base.CharMatcher;
26 import com.google.common.base.Strings;
27 import com.google.common.util.concurrent.Futures;
28 import com.google.common.util.concurrent.ListenableFuture;
29 import io.netty.channel.Channel;
30 import io.netty.channel.ChannelFuture;
31 import io.netty.util.concurrent.Future;
32 import io.netty.util.concurrent.GenericFutureListener;
33 import java.io.ByteArrayInputStream;
34 import java.net.InetSocketAddress;
35 import java.util.ArrayList;
37 import java.util.UUID;
38 import javax.xml.parsers.ParserConfigurationException;
39 import org.junit.Before;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 import org.mockito.ArgumentCaptor;
43 import org.mockito.Mock;
44 import org.mockito.junit.MockitoJUnitRunner;
45 import org.opendaylight.netconf.api.CapabilityURN;
46 import org.opendaylight.netconf.api.NamespaceURN;
47 import org.opendaylight.netconf.api.NetconfTerminationReason;
48 import org.opendaylight.netconf.api.messages.NetconfMessage;
49 import org.opendaylight.netconf.api.messages.RpcReplyMessage;
50 import org.opendaylight.netconf.client.NetconfClientSession;
51 import org.opendaylight.netconf.client.NetconfClientSessionListener;
52 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
53 import org.opendaylight.netconf.client.mdsal.api.RemoteDevice;
54 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
55 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.SessionIdType;
56 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.NetconfState;
57 import org.opendaylight.yangtools.util.xml.UntrustedXML;
58 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
59 import org.opendaylight.yangtools.yang.common.ErrorTag;
60 import org.opendaylight.yangtools.yang.common.ErrorType;
61 import org.opendaylight.yangtools.yang.common.QName;
62 import org.opendaylight.yangtools.yang.common.RpcError;
63 import org.opendaylight.yangtools.yang.common.RpcResult;
64 import org.opendaylight.yangtools.yang.common.Uint32;
65 import org.w3c.dom.Document;
66 import org.w3c.dom.Element;
68 @RunWith(MockitoJUnitRunner.StrictStubs.class)
69 public class NetconfDeviceCommunicatorTest {
70 private static final SessionIdType SESSION_ID = new SessionIdType(Uint32.ONE);
73 private RemoteDevice<NetconfDeviceCommunicator> mockDevice;
75 private NetconfClientSession spySession;
76 private NetconfDeviceCommunicator communicator;
79 public void setUp() throws Exception {
80 communicator = new NetconfDeviceCommunicator(
81 new RemoteDeviceId("test", InetSocketAddress.createUnresolved("localhost", 22)), mockDevice, 10);
82 spySession = spy(new NetconfClientSession(mock(NetconfClientSessionListener.class), mock(Channel.class),
83 SESSION_ID, Set.of()));
87 doNothing().when(mockDevice).onRemoteSessionUp(any(NetconfSessionPreferences.class),
88 any(NetconfDeviceCommunicator.class));
89 communicator.onSessionUp(spySession);
92 private ListenableFuture<RpcResult<NetconfMessage>> sendRequest() throws Exception {
93 return sendRequest(UUID.randomUUID().toString(), true);
96 @SuppressWarnings("unchecked")
97 private ListenableFuture<RpcResult<NetconfMessage>> sendRequest(final String messageID,
98 final boolean doLastTest) throws Exception {
99 Document doc = UntrustedXML.newDocumentBuilder().newDocument();
100 Element element = doc.createElement("request");
101 element.setAttribute("message-id", messageID);
102 doc.appendChild(element);
103 NetconfMessage message = new NetconfMessage(doc);
105 ChannelFuture mockChannelFuture = mock(ChannelFuture.class);
106 doReturn(mockChannelFuture).when(mockChannelFuture)
107 .addListener(any(GenericFutureListener.class));
108 doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
110 ListenableFuture<RpcResult<NetconfMessage>> resultFuture =
111 communicator.sendRequest(message, QName.create("", "mockRpc"));
113 assertNotNull("ListenableFuture is null", resultFuture);
119 public void testOnSessionUp() {
120 final var testCapability = "urn:opendaylight:params:xml:ns:test?module=test-module&revision=2014-06-02";
121 final var serverCapabilities = Set.of(
122 CapabilityURN.ROLLBACK_ON_ERROR,
123 NetconfState.QNAME.getNamespace().toString(),
125 doReturn(serverCapabilities).when(spySession).getServerCapabilities();
127 final var netconfSessionPreferences = ArgumentCaptor.forClass(NetconfSessionPreferences.class);
128 doNothing().when(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
130 communicator.onSessionUp(spySession);
132 verify(spySession).getServerCapabilities();
133 verify(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
135 NetconfSessionPreferences actualCapabilites = netconfSessionPreferences.getValue();
136 assertTrue(actualCapabilites.containsNonModuleCapability(
137 "urn:ietf:params:netconf:capability:rollback-on-error:1.0"));
138 assertFalse(actualCapabilites.containsNonModuleCapability(testCapability));
139 assertEquals(Set.of(QName.create("urn:opendaylight:params:xml:ns:test", "2014-06-02", "test-module")),
140 actualCapabilites.moduleBasedCaps().keySet());
141 assertTrue(actualCapabilites.isRollbackSupported());
142 assertTrue(actualCapabilites.isMonitoringSupported());
143 assertEquals(SESSION_ID, actualCapabilites.sessionId());
146 @SuppressWarnings("unchecked")
147 @Test(timeout = 5000)
148 public void testOnSessionDown() throws Exception {
151 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest();
152 final ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest();
154 doNothing().when(mockDevice).onRemoteSessionDown();
156 communicator.onSessionDown(spySession, new Exception("mock ex"));
158 verifyErrorRpcResult(resultFuture1.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
159 verifyErrorRpcResult(resultFuture2.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
161 verify(mockDevice).onRemoteSessionDown();
165 communicator.onSessionDown(spySession, new Exception("mock ex"));
167 verify(mockDevice, never()).onRemoteSessionDown();
171 public void testOnSessionTerminated() throws Exception {
174 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest();
176 doNothing().when(mockDevice).onRemoteSessionDown();
178 String reasonText = "testing terminate";
179 NetconfTerminationReason reason = new NetconfTerminationReason(reasonText);
180 communicator.onSessionTerminated(spySession, reason);
182 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
183 assertEquals("RpcError message", reasonText, rpcError.getMessage());
185 verify(mockDevice).onRemoteSessionDown();
189 public void testClose() throws Exception {
190 communicator.close();
191 verify(mockDevice, never()).onRemoteSessionDown();
194 @SuppressWarnings("unchecked")
196 public void testSendRequest() throws Exception {
199 NetconfMessage message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
200 QName rpc = QName.create("", "mockRpc");
202 final var futureListener = ArgumentCaptor.forClass(GenericFutureListener.class);
204 ChannelFuture mockChannelFuture = mock(ChannelFuture.class);
205 doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
206 doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
208 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest(message, rpc);
210 verify(spySession).sendMessage(same(message));
212 assertNotNull("ListenableFuture is null", resultFuture);
214 verify(mockChannelFuture).addListener(futureListener.capture());
215 Future<Void> operationFuture = mock(Future.class);
216 doReturn(null).when(operationFuture).cause();
217 futureListener.getValue().operationComplete(operationFuture);
219 // verify it is not cancelled nor has an error set
220 assertFalse(resultFuture.isDone());
224 public void testSendRequestWithNoSession() throws Exception {
225 NetconfMessage message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
226 QName rpc = QName.create("", "mockRpc");
228 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest(message, rpc);
230 assertNotNull("ListenableFuture is null", resultFuture);
232 // Should have an immediate result
233 RpcResult<NetconfMessage> rpcResult = Futures.getDone(resultFuture);
235 verifyErrorRpcResult(rpcResult, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
238 private static NetconfMessage createSuccessResponseMessage(final String messageID)
239 throws ParserConfigurationException {
240 Document doc = UntrustedXML.newDocumentBuilder().newDocument();
241 Element rpcReply = doc.createElementNS(NamespaceURN.BASE, RpcReplyMessage.ELEMENT_NAME);
242 rpcReply.setAttribute("message-id", messageID);
243 Element element = doc.createElementNS("ns", "data");
244 element.setTextContent(messageID);
245 rpcReply.appendChild(element);
246 doc.appendChild(rpcReply);
248 return new NetconfMessage(doc);
251 @SuppressWarnings("unchecked")
253 public void testSendRequestWithWithSendFailure() throws Exception {
256 NetconfMessage message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
257 QName rpc = QName.create("", "mockRpc");
259 final var futureListener = ArgumentCaptor.forClass(GenericFutureListener.class);
261 ChannelFuture mockChannelFuture = mock(ChannelFuture.class);
262 doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
263 doReturn(mockChannelFuture).when(spySession).sendMessage(same(message));
265 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest(message, rpc);
267 assertNotNull("ListenableFuture is null", resultFuture);
269 verify(mockChannelFuture).addListener(futureListener.capture());
271 Future<Void> operationFuture = mock(Future.class);
272 doReturn(new Exception("mock error")).when(operationFuture).cause();
273 futureListener.getValue().operationComplete(operationFuture);
275 // Should have an immediate result
276 RpcResult<NetconfMessage> rpcResult = Futures.getDone(resultFuture);
278 RpcError rpcError = verifyErrorRpcResult(rpcResult, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
279 assertEquals("RpcError message contains \"mock error\"", true,
280 rpcError.getMessage().contains("mock error"));
283 //Test scenario verifying whether missing message is handled
285 public void testOnMissingResponseMessage() throws Exception {
289 String messageID1 = UUID.randomUUID().toString();
290 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest(messageID1, true);
292 String messageID2 = UUID.randomUUID().toString();
293 ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest(messageID2, true);
295 String messageID3 = UUID.randomUUID().toString();
296 ListenableFuture<RpcResult<NetconfMessage>> resultFuture3 = sendRequest(messageID3, true);
298 //response messages 1,2 are omitted
299 communicator.onMessage(spySession, createSuccessResponseMessage(messageID3));
301 verifyResponseMessage(resultFuture3.get(), messageID3);
305 public void testOnSuccessfulResponseMessage() throws Exception {
308 String messageID1 = UUID.randomUUID().toString();
309 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest(messageID1, true);
311 String messageID2 = UUID.randomUUID().toString();
312 final ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest(messageID2, true);
314 communicator.onMessage(spySession, createSuccessResponseMessage(messageID1));
315 communicator.onMessage(spySession, createSuccessResponseMessage(messageID2));
317 verifyResponseMessage(resultFuture1.get(), messageID1);
318 verifyResponseMessage(resultFuture2.get(), messageID2);
322 public void testOnResponseMessageWithError() throws Exception {
325 String messageID = UUID.randomUUID().toString();
326 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID, true);
328 communicator.onMessage(spySession, createErrorResponseMessage(messageID));
330 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.RPC, ErrorTag.MISSING_ATTRIBUTE);
331 assertEquals("RpcError message", "Missing attribute", rpcError.getMessage());
333 String errorInfo = rpcError.getInfo();
334 assertNotNull("RpcError info is null", errorInfo);
335 assertTrue("Error info contains \"foo\"", errorInfo.contains("<bad-attribute>foo</bad-attribute>"));
336 assertTrue("Error info contains \"bar\"", errorInfo.contains("<bad-element>bar</bad-element>"));
340 public void testOnResponseMessageWithMultipleErrors() throws Exception {
343 String messageID = UUID.randomUUID().toString();
344 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID, true);
346 communicator.onMessage(spySession, createMultiErrorResponseMessage(messageID));
348 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.OPERATION_FAILED);
350 String errorInfo = rpcError.getInfo();
351 assertNotNull("RpcError info is null", errorInfo);
353 String errorInfoMessages = rpcError.getInfo();
354 String errMsg1 = "Number of member links configured, i.e [1], "
355 + "for interface [ae0]is lesser than the required minimum [2].";
356 String errMsg2 = "configuration check-out failed";
357 assertTrue(String.format("Error info contains \"%s\" or \"%s\'", errMsg1, errMsg2),
358 errorInfoMessages.contains(errMsg1) && errorInfoMessages.contains(errMsg2));
362 public void testOnResponseMessageWithWrongMessageID() throws Exception {
365 String messageID = UUID.randomUUID().toString();
366 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID, true);
368 communicator.onMessage(spySession, createSuccessResponseMessage(UUID.randomUUID().toString()));
370 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
371 assertFalse("RpcError message non-empty", Strings.isNullOrEmpty(rpcError.getMessage()));
373 String errorInfo = rpcError.getInfo();
374 assertNotNull("RpcError info is null", errorInfo);
375 assertTrue("Error info contains \"actual-message-id\"", errorInfo.contains("actual-message-id"));
376 assertTrue("Error info contains \"expected-message-id\"", errorInfo.contains("expected-message-id"));
380 public void testConcurrentMessageLimit() throws Exception {
382 ArrayList<String> messageID = new ArrayList<>();
384 for (int i = 0; i < 10; i++) {
385 messageID.add(UUID.randomUUID().toString());
386 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID.get(i), false);
387 assertEquals("ListenableFuture is null", true, resultFuture instanceof UncancellableFuture);
390 final String notWorkingMessageID = UUID.randomUUID().toString();
391 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(notWorkingMessageID, false);
392 assertEquals("ListenableFuture is null", false, resultFuture instanceof UncancellableFuture);
394 communicator.onMessage(spySession, createSuccessResponseMessage(messageID.get(0)));
396 resultFuture = sendRequest(messageID.get(0), false);
397 assertNotNull("ListenableFuture is null", resultFuture);
400 private static NetconfMessage createMultiErrorResponseMessage(final String messageID) throws Exception {
401 // multiple rpc-errors which simulate actual response like in NETCONF-666
402 String xmlStr = "<nc:rpc-reply xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\" "
403 + "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" "
404 + "xmlns:junos=\"http://xml.juniper.net/junos/18.4R1/junos\" message-id=\"" + messageID + "\">"
406 + "<nc:error-type>protocol</nc:error-type>\n"
407 + "<nc:error-tag>operation-failed</nc:error-tag>\n"
408 + "<nc:error-severity>error</nc:error-severity>\n"
409 + "<source-daemon>\n"
411 + "</source-daemon>\n"
412 + "<nc:error-message>\n"
413 + "Number of member links configured, i.e [1], "
414 + "for interface [ae0]is lesser than the required minimum [2].\n"
415 + "</nc:error-message>\n"
416 + "</nc:rpc-error>\n"
418 + "<nc:error-type>protocol</nc:error-type>\n"
419 + "<nc:error-tag>operation-failed</nc:error-tag>\n"
420 + "<nc:error-severity>error</nc:error-severity>\n"
421 + "<nc:error-message>\n"
422 + "configuration check-out failed\n"
423 + "</nc:error-message>\n"
424 + "</nc:rpc-error>\n"
427 ByteArrayInputStream bis = new ByteArrayInputStream(xmlStr.getBytes());
428 Document doc = UntrustedXML.newDocumentBuilder().parse(bis);
429 return new NetconfMessage(doc);
432 private static NetconfMessage createErrorResponseMessage(final String messageID) throws Exception {
433 String xmlStr = "<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\""
434 + " message-id=\"" + messageID + "\">"
436 + " <error-type>rpc</error-type>"
437 + " <error-tag>missing-attribute</error-tag>"
438 + " <error-severity>error</error-severity>"
439 + " <error-message>Missing attribute</error-message>"
441 + " <bad-attribute>foo</bad-attribute>"
442 + " <bad-element>bar</bad-element>"
447 ByteArrayInputStream bis = new ByteArrayInputStream(xmlStr.getBytes());
448 Document doc = UntrustedXML.newDocumentBuilder().parse(bis);
449 return new NetconfMessage(doc);
452 private static void verifyResponseMessage(final RpcResult<NetconfMessage> rpcResult, final String dataText) {
453 assertNotNull("RpcResult is null", rpcResult);
454 assertTrue("isSuccessful", rpcResult.isSuccessful());
455 NetconfMessage messageResult = rpcResult.getResult();
456 assertNotNull("getResult", messageResult);
457 // List<SimpleNode<?>> nodes = messageResult.getSimpleNodesByName(
458 // QName.create( URI.create( "ns" ), null, "data" ) );
459 // assertNotNull( "getSimpleNodesByName", nodes );
460 // assertEquals( "List<SimpleNode<?>> size", 1, nodes.size() );
461 // assertEquals( "SimpleNode value", dataText, nodes.iterator().next().getValue() );
464 private static RpcError verifyErrorRpcResult(final RpcResult<NetconfMessage> rpcResult,
465 final ErrorType expErrorType, final ErrorTag expErrorTag) {
466 assertNotNull("RpcResult is null", rpcResult);
467 assertFalse("isSuccessful", rpcResult.isSuccessful());
468 assertNotNull("RpcResult errors is null", rpcResult.getErrors());
469 assertEquals("Errors size", 1, rpcResult.getErrors().size());
470 RpcError rpcError = rpcResult.getErrors().iterator().next();
471 assertEquals("getErrorSeverity", ErrorSeverity.ERROR, rpcError.getSeverity());
472 assertEquals("getErrorType", expErrorType, rpcError.getErrorType());
473 assertEquals("getErrorTag", expErrorTag, rpcError.getTag());
475 final String msg = rpcError.getMessage();
476 assertNotNull("getMessage is null", msg);
477 assertFalse("getMessage is empty", msg.isEmpty());
478 assertFalse("getMessage is blank", CharMatcher.whitespace().matchesAllOf(msg));