Improve NetconfNodeHandlerTest
[netconf.git] / apps / netconf-topology / src / test / java / org / opendaylight / netconf / topology / spi / NetconfNodeHandlerTest.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.topology.spi;
9
10 import static org.hamcrest.CoreMatchers.instanceOf;
11 import static org.hamcrest.MatcherAssert.assertThat;
12 import static org.junit.Assert.assertEquals;
13 import static org.junit.Assert.assertTrue;
14 import static org.mockito.ArgumentMatchers.any;
15 import static org.mockito.ArgumentMatchers.anyLong;
16 import static org.mockito.ArgumentMatchers.eq;
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.times;
21 import static org.mockito.Mockito.verify;
22 import static org.mockito.Mockito.verifyNoInteractions;
23
24 import com.google.common.net.InetAddresses;
25 import com.google.common.util.concurrent.Futures;
26 import com.google.common.util.concurrent.SettableFuture;
27 import io.netty.util.Timeout;
28 import io.netty.util.TimerTask;
29 import java.net.InetSocketAddress;
30 import java.util.List;
31 import java.util.concurrent.TimeUnit;
32 import org.junit.After;
33 import org.junit.Before;
34 import org.junit.BeforeClass;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 import org.mockito.ArgumentCaptor;
38 import org.mockito.Captor;
39 import org.mockito.Mock;
40 import org.mockito.junit.MockitoJUnitRunner;
41 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
42 import org.opendaylight.netconf.api.CapabilityURN;
43 import org.opendaylight.netconf.client.NetconfClientFactory;
44 import org.opendaylight.netconf.client.NetconfClientFactoryImpl;
45 import org.opendaylight.netconf.client.NetconfClientSession;
46 import org.opendaylight.netconf.client.mdsal.NetconfDeviceCapabilities;
47 import org.opendaylight.netconf.client.mdsal.NetconfDeviceSchema;
48 import org.opendaylight.netconf.client.mdsal.api.BaseNetconfSchemaProvider;
49 import org.opendaylight.netconf.client.mdsal.api.CredentialProvider;
50 import org.opendaylight.netconf.client.mdsal.api.DeviceActionFactory;
51 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
52 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceHandler;
53 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
54 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices;
55 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceServices.Rpcs;
56 import org.opendaylight.netconf.client.mdsal.api.SchemaResourceManager;
57 import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider;
58 import org.opendaylight.netconf.client.mdsal.impl.DefaultBaseNetconfSchemaProvider;
59 import org.opendaylight.netconf.common.NetconfTimer;
60 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv4Address;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
65 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.KeyAuthBuilder;
66 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.LoginPwUnencryptedBuilder;
67 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.key.auth.KeyBasedBuilder;
68 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.credentials.credentials.login.pw.unencrypted.LoginPasswordUnencryptedBuilder;
69 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev231121.NetconfNodeBuilder;
70 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
71 import org.opendaylight.yangtools.yang.common.Decimal64;
72 import org.opendaylight.yangtools.yang.common.Uint16;
73 import org.opendaylight.yangtools.yang.common.Uint32;
74 import org.opendaylight.yangtools.yang.data.api.schema.MountPointContext;
75 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
76 import org.opendaylight.yangtools.yang.parser.impl.DefaultYangParserFactory;
77
78 @RunWith(MockitoJUnitRunner.StrictStubs.class)
79 public class NetconfNodeHandlerTest {
80     private static final RemoteDeviceId DEVICE_ID = new RemoteDeviceId("netconf-topology",
81         new InetSocketAddress(InetAddresses.forString("127.0.0.1"), 9999));
82     private static final NodeId NODE_ID = new NodeId("testing-node");
83
84     private static BaseNetconfSchemaProvider BASE_SCHEMAS;
85
86     // Core setup
87     @Mock
88     private NetconfTimer timer;
89     @Mock
90     private SchemaResourceManager schemaManager;
91     @Mock
92     private DeviceActionFactory deviceActionFactory;
93     @Mock
94     private RemoteDeviceHandler delegate;
95
96     // DefaultNetconfClientConfigurationBuilderFactory setup
97     @Mock
98     private SslContextFactoryProvider sslContextFactoryProvider;
99     @Mock
100     private AAAEncryptionService encryptionService;
101     @Mock
102     private CredentialProvider credentialProvider;
103
104     // Mock client dispatcher-related things
105     @Mock
106     private NetconfClientFactory clientFactory;
107     @Mock
108     private NetconfClientSession clientSession;
109     @Captor
110     private ArgumentCaptor<NetconfDeviceSchema> schemaCaptor;
111     @Captor
112     private ArgumentCaptor<NetconfSessionPreferences> prefsCaptor;
113     @Captor
114     private ArgumentCaptor<RemoteDeviceServices> servicesCaptor;
115
116     // Mock Timer-related things
117     @Mock
118     private Timeout timeout;
119     @Captor
120     private ArgumentCaptor<TimerTask> timerCaptor;
121     @Mock
122     private EffectiveModelContext schemaContext;
123
124     private NetconfTopologySchemaAssembler schemaAssembler;
125     private NetconfNodeHandler handler;
126
127     @BeforeClass
128     public static void beforeClass() throws Exception {
129         BASE_SCHEMAS = new DefaultBaseNetconfSchemaProvider(new DefaultYangParserFactory());
130     }
131
132     @BeforeClass
133     public static void afterClass() throws Exception {
134         BASE_SCHEMAS = null;
135     }
136
137     @Before
138     public void before() {
139         schemaAssembler = new NetconfTopologySchemaAssembler(1, 1, 0, TimeUnit.SECONDS);
140
141         // Instantiate the handler
142         handler = new NetconfNodeHandler(clientFactory, timer, BASE_SCHEMAS, schemaManager, schemaAssembler,
143             new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider,
144                 sslContextFactoryProvider),
145             deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
146                 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
147                 .setPort(new PortNumber(Uint16.valueOf(9999)))
148                 .setReconnectOnChangedSchema(true)
149                 .setSchemaless(true)
150                 .setTcpOnly(true)
151                 .setBackoffMultiplier(Decimal64.valueOf("1.5"))
152                 .setConcurrentRpcLimit(Uint16.ONE)
153                 // One reconnection attempt
154                 .setMaxConnectionAttempts(Uint32.TWO)
155                 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
156                 .setMinBackoffMillis(Uint16.valueOf(100))
157                 .setKeepaliveDelay(Uint32.valueOf(1000))
158                 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
159                 .setMaxBackoffMillis(Uint32.valueOf(1000))
160                 .setBackoffJitter(Decimal64.valueOf("0.0"))
161                 .setCredentials(new LoginPwUnencryptedBuilder()
162                     .setLoginPasswordUnencrypted(new LoginPasswordUnencryptedBuilder()
163                         .setUsername("testuser")
164                         .setPassword("testpassword")
165                         .build())
166                     .build())
167                 .build(), null);
168     }
169
170     @After
171     public void after() {
172         schemaAssembler.close();
173     }
174
175     @Test
176     public void successfulOnDeviceConnectedPropagates() throws Exception {
177         assertSuccessfulConnect();
178         assertEquals(1, handler.attempts());
179
180         final var schema = new NetconfDeviceSchema(NetconfDeviceCapabilities.empty(),
181             MountPointContext.of(schemaContext));
182         final var netconfSessionPreferences = NetconfSessionPreferences.fromStrings(List.of(CapabilityURN.CANDIDATE));
183         final var deviceServices = new RemoteDeviceServices(mock(Rpcs.Normalized.class), null);
184
185         // when the device is connected, we propagate the information
186         doNothing().when(delegate).onDeviceConnected(schemaCaptor.capture(), prefsCaptor.capture(),
187             servicesCaptor.capture());
188         handler.onDeviceConnected(schema, netconfSessionPreferences, deviceServices);
189
190         assertEquals(schema, schemaCaptor.getValue());
191         assertEquals(netconfSessionPreferences, prefsCaptor.getValue());
192         assertEquals(deviceServices, servicesCaptor.getValue());
193         assertEquals(0, handler.attempts());
194     }
195
196     @Test
197     public void failedSchemaCausesReconnect() throws Exception {
198         assertSuccessfulConnect();
199         assertEquals(1, handler.attempts());
200
201         // Note: this will count as a second attempt
202         doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), anyLong(), any());
203
204         handler.onDeviceFailed(new AssertionError("schema failure"));
205
206         assertEquals(2, handler.attempts());
207
208         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
209         timerCaptor.getValue().run(timeout);
210         verify(clientFactory, times(2)).createClient(any());
211         assertEquals(2, handler.attempts());
212     }
213
214     @Test
215     public void downAfterUpCausesReconnect() throws Exception {
216         // Let's borrow common bits
217         successfulOnDeviceConnectedPropagates();
218
219         // when the device is connected, we propagate the information and initiate reconnect
220         doNothing().when(delegate).onDeviceDisconnected();
221         doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(100L), eq(TimeUnit.MILLISECONDS));
222         handler.onDeviceDisconnected();
223
224         assertEquals(1, handler.attempts());
225
226         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
227         timerCaptor.getValue().run(timeout);
228         verify(clientFactory, times(2)).createClient(any());
229         assertEquals(1, handler.attempts());
230     }
231
232     @Test
233     public void socketFailuresAreRetried() throws Exception {
234         final var firstFuture = SettableFuture.create();
235         final var secondFuture = SettableFuture.create();
236         doReturn(firstFuture, secondFuture).when(clientFactory).createClient(any());
237         handler.connect();
238         assertEquals(1, handler.attempts());
239
240         doReturn(timeout).when(timer).newTimeout(timerCaptor.capture(), eq(150L), eq(TimeUnit.MILLISECONDS));
241         firstFuture.setException(new AssertionError("first"));
242
243         assertEquals(2, handler.attempts());
244
245         // and when we run the task, we get a clientDispatcher invocation, but attempts are still the same
246         timerCaptor.getValue().run(timeout);
247         verify(clientFactory, times(2)).createClient(any());
248         assertEquals(2, handler.attempts());
249
250         // now report the second failure
251         final var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
252         doNothing().when(delegate).onDeviceFailed(throwableCaptor.capture());
253         secondFuture.setException(new AssertionError("second"));
254         assertThat(throwableCaptor.getValue(), instanceOf(ConnectGivenUpException.class));
255
256         // but nothing else happens
257         assertEquals(2, handler.attempts());
258     }
259
260     // Initiate connect() which results in immediate clientDispatcher report. No interactions with delegate may occur,
261     // as this is just a prelude to a follow-up callback
262     private void assertSuccessfulConnect() throws Exception {
263         doReturn(Futures.immediateFuture(clientSession)).when(clientFactory).createClient(any());
264         handler.connect();
265         verify(clientFactory).createClient(any());
266         verifyNoInteractions(delegate);
267     }
268
269     @Test
270     public void failToConnectOnUnsupportedConfiguration() {
271         final var defaultTimer = new DefaultNetconfTimer();
272         final var factory = new NetconfClientFactoryImpl(defaultTimer);
273
274         final var keyId = "keyId";
275         final var keyAuthHandler = new NetconfNodeHandler(factory, defaultTimer, BASE_SCHEMAS, schemaManager,
276             schemaAssembler, new NetconfClientConfigurationBuilderFactoryImpl(encryptionService, credentialProvider,
277                 sslContextFactoryProvider),
278             deviceActionFactory, delegate, DEVICE_ID, NODE_ID, new NetconfNodeBuilder()
279                 .setHost(new Host(new IpAddress(new Ipv4Address("127.0.0.1"))))
280                 .setPort(new PortNumber(Uint16.valueOf(9999)))
281                 .setReconnectOnChangedSchema(true)
282                 .setSchemaless(true)
283                 .setTcpOnly(false)
284                 .setProtocol(null)
285                 .setBackoffMultiplier(Decimal64.valueOf("1.5"))
286                 .setConcurrentRpcLimit(Uint16.ONE)
287                 // One reconnection attempt
288                 .setMaxConnectionAttempts(Uint32.ONE)
289                 .setDefaultRequestTimeoutMillis(Uint32.valueOf(1000))
290                 .setMinBackoffMillis(Uint16.valueOf(100))
291                 .setKeepaliveDelay(Uint32.valueOf(1000))
292                 .setConnectionTimeoutMillis(Uint32.valueOf(1000))
293                 .setMaxBackoffMillis(Uint32.valueOf(1000))
294                 .setBackoffJitter(Decimal64.valueOf("0.0"))
295                 .setCredentials(new KeyAuthBuilder()
296                     .setKeyBased(new KeyBasedBuilder()
297                         .setUsername("testuser")
298                         .setKeyId(keyId)
299                         .build())
300                     .build())
301                 .build(), null);
302
303         // return null when attempt to load credentials fot key id
304         doReturn(null).when(credentialProvider).credentialForId(any());
305         doNothing().when(delegate).onDeviceFailed(any());
306         keyAuthHandler.connect();
307         verify(credentialProvider).credentialForId(eq(keyId));
308         // attempt to connect fails due to unsupported configuration, and there is attempt to reconnect
309         final var captor = ArgumentCaptor.forClass(Throwable.class);
310         verify(delegate).onDeviceFailed(captor.capture());
311         assertTrue(captor.getValue() instanceof ConnectGivenUpException);
312         assertEquals(1, keyAuthHandler.attempts());
313     }
314 }