be8b2a2da0aa7d3b84a37ad15c37d952018298d5
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / netconf-executor / src / test / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / functions / netconf / executor / core / NetconfDeviceCommunicatorTest.kt
1 /*
2  * Copyright © 2019 Bell Canada
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.core
18
19 import org.mockito.Mockito
20 import org.mockito.kotlin.any
21 import org.junit.Before
22 import org.junit.Test
23 import org.mockito.kotlin.never
24 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceInfo
25 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfReceivedEvent
26 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSession
27 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSessionListener
28 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.RpcMessageUtils
29 import java.io.IOException
30 import java.io.InputStream
31 import java.io.OutputStream
32 import java.nio.charset.StandardCharsets
33 import java.util.concurrent.CompletableFuture
34 import java.util.concurrent.ConcurrentHashMap
35 import java.util.regex.Pattern
36 import kotlin.test.assertEquals
37 import kotlin.test.assertFalse
38 import kotlin.test.assertTrue
39
40 class NetconfDeviceCommunicatorTest {
41
42     private lateinit var netconfSession: NetconfSession
43     private lateinit var netconfSessionListener: NetconfSessionListener
44     private lateinit var mockInputStream: InputStream
45     private lateinit var mockOutputStream: OutputStream
46     private lateinit var stubInputStream: InputStream
47     private lateinit var replies: MutableMap<String, CompletableFuture<String>>
48     private val endPatternCharArray: List<Int> = stringToCharArray(RpcMessageUtils.END_PATTERN)
49
50     companion object {
51
52         private val chunkedEnding = "\n##\n"
53
54         // using example from section 4.2 of RFC6242 (https://tools.ietf.org/html/rfc6242#section-4.2)
55         private val validChunkedEncodedMsg = """
56             |
57             |#4
58             |<rpc
59             |#18
60             | message-id="102"
61             |
62             |#79
63             |     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
64             |  <close-session/>
65             |</rpc>
66             |##
67             |""".trimMargin()
68     }
69
70     private fun stringToCharArray(str: String): List<Int> {
71         return str.toCharArray().map(Char::toInt)
72     }
73
74     @Before
75     fun setup() {
76         netconfSession = Mockito.mock(NetconfSession::class.java)
77         netconfSessionListener = Mockito.mock(NetconfSessionListener::class.java)
78         mockInputStream = Mockito.mock(InputStream::class.java)
79         mockOutputStream = Mockito.mock(OutputStream::class.java)
80         replies = ConcurrentHashMap()
81     }
82
83     @Test
84     fun `NetconfDeviceCommunicator should read from supplied reader`() {
85         Mockito.`when`(mockInputStream.read()).thenReturn(-1)
86         Mockito.`when`(mockInputStream.read(any(), any(), any())).thenReturn(-1)
87         val communicator: NetconfDeviceCommunicator =
88             NetconfDeviceCommunicator(mockInputStream, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies)
89         communicator.join()
90         // verify
91         Mockito.verify(mockInputStream).read(any(), any(), any())
92     }
93
94     @Test
95     fun `NetconfDeviceCommunicator unregisters device on END_PATTERN`() {
96         // The reader will generate RpcMessageUtils.END_PATTERN "]]>]]>" which tells Netconf
97         // to unregister the device.
98         // we want to capture the slot to return the value as inputStreamReader will pass a char array
99         // create a slot where NetconfReceivedEvent will be placed to further verify Type.DEVICE_UNREGISTERED
100         val captured = mutableListOf<NetconfReceivedEvent>()
101         Mockito.doAnswer {
102             captured.add(it.getArgument(0))
103         }.`when`(netconfSessionListener).accept(any())
104         stubInputStream = RpcMessageUtils.END_PATTERN.byteInputStream(StandardCharsets.UTF_8)
105         // RUN the test
106         val communicator = NetconfDeviceCommunicator(
107             stubInputStream, mockOutputStream,
108             genDeviceInfo(), netconfSessionListener, replies
109         )
110         communicator.join()
111         // Verify
112         assertTrue(captured.size == 1)
113         assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, captured[0].type)
114         assertEquals(genDeviceInfo(), captured[0].deviceInfo)
115     }
116
117     @Test
118     fun `NetconfDeviceCommunicator on IOException generated DEVICE_ERROR event`() {
119         val captured = mutableListOf<NetconfReceivedEvent>()
120         Mockito.doAnswer {
121             captured.add(it.getArgument(0))
122         }.`when`(netconfSessionListener).accept(any())
123         stubInputStream = "".byteInputStream(StandardCharsets.UTF_8)
124         Mockito.`when`(mockInputStream.read(any(), any(), any()))
125             .thenReturn(1).thenThrow(IOException("Fake IO"))
126         // RUN THE TEST
127         val communicator = NetconfDeviceCommunicator(
128             mockInputStream, mockOutputStream,
129             genDeviceInfo(), netconfSessionListener, replies
130         )
131         communicator.join()
132         // Verify
133         assertTrue(captured.size == 1)
134         assertEquals(genDeviceInfo(), captured[0].deviceInfo)
135         assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, captured[0].type)
136     }
137
138     @Test
139     fun `NetconfDeviceCommunicator in END_PATTERN state but fails RpcMessageUtils end pattern validation`() {
140         val captured = mutableListOf<NetconfReceivedEvent>()
141         Mockito.doAnswer {
142             captured.add(it.getArgument(0))
143         }.`when`(netconfSessionListener).accept(any())
144         val payload = "<rpc-reply>blah</rpc-reply>"
145         stubInputStream = "$payload${RpcMessageUtils.END_PATTERN}".byteInputStream(StandardCharsets.UTF_8)
146         Mockito.doAnswer {
147             val bytes = stubInputStream.readAllBytes()
148             bytes.forEachIndexed { index, byte ->
149                 (it.getArgument(0) as ByteArray)[index] = byte
150             }
151             bytes.size
152         }.doReturn(-1).`when`(mockInputStream).read(any(), any(), any())
153         // RUN the test
154         val communicator = NetconfDeviceCommunicator(
155             mockInputStream, mockOutputStream,
156             genDeviceInfo(), netconfSessionListener, replies
157         )
158         communicator.join()
159         // Verify
160         Mockito.verify(mockInputStream, never()).close() // make sure the reader is not closed as this could cause problems
161         assertTrue(captured.size == 1)
162         // eventually, sessionListener is called with message type DEVICE_REPLY
163         assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, captured[0].type)
164         assertEquals(payload, captured[0].messagePayload)
165     }
166
167     @Test
168     fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN but validation failing produces DEVICE_ERROR`() {
169         val captured = mutableListOf<NetconfReceivedEvent>()
170         Mockito.doAnswer {
171             captured.add(it.getArgument(0))
172         }.`when`(netconfSessionListener).accept(any())
173         val payload = "<rpc-reply>blah</rpc-reply>"
174         val payloadWithChunkedEnding = "$payload$chunkedEnding"
175
176         stubInputStream = payloadWithChunkedEnding.byteInputStream(StandardCharsets.UTF_8)
177         Mockito.doAnswer {
178             val bytes = stubInputStream.readAllBytes()
179             bytes.forEachIndexed { index, byte ->
180                 (it.getArgument(0) as ByteArray)[index] = byte
181             }
182             bytes.size
183         }.doReturn(-1).`when`(mockInputStream).read(any(), any(), any())
184         // RUN the test
185         val communicator = NetconfDeviceCommunicator(
186             mockInputStream, mockOutputStream, genDeviceInfo(),
187             netconfSessionListener, replies
188         )
189         communicator.join()
190         // Verify
191         Mockito.verify(mockInputStream, never()).close() // make sure the reader is not closed as this could cause problems
192         assertTrue(captured.size == 1)
193         // eventually, sessionListener is called with message type DEVICE_REPLY
194         assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, captured[0].type)
195         assertEquals("", captured[0].messagePayload)
196     }
197
198     @Test
199     fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN passing validation generates DEVICE_REPLY`() {
200         val captured = mutableListOf<NetconfReceivedEvent>()
201         Mockito.doAnswer {
202             captured.add(it.getArgument(0))
203         }.`when`(netconfSessionListener).accept(any())
204         stubInputStream = validChunkedEncodedMsg.byteInputStream(StandardCharsets.UTF_8)
205         Mockito.doAnswer {
206             val bytes = stubInputStream.readAllBytes()
207             bytes.forEachIndexed { index, byte ->
208                 (it.getArgument(0) as ByteArray)[index] = byte
209             }
210             bytes.size
211         }.doReturn(-1).`when`(mockInputStream).read(any(), any(), any())
212         // RUN the test
213         NetconfDeviceCommunicator(mockInputStream, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
214         // Verify
215         Mockito.verify(mockOutputStream, never()).close() // make sure the reader is not closed as this could cause problems
216         assertTrue(captured.size == 1)
217         // eventually, sessionListener is called with message type DEVICE_REPLY
218         assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, captured[0].type)
219         assertEquals(
220             """
221         <rpc message-id="102"
222              xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
223           <close-session/>
224         </rpc>
225             """.trimIndent(),
226             captured[0].messagePayload
227         )
228     }
229
230     @Test
231     // test to ensure that we have a valid test message to be then used in the case of chunked message
232     // validation code path
233     fun `chunked sample is validated by the chunked response regex`() {
234         val test1 = "\n#10\nblah\n##\n"
235         val chunkedFramingPattern = Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
236         val matcher = chunkedFramingPattern.matcher(test1)
237         assertTrue { matcher.matches() }
238     }
239
240     @Test
241     // Verify that our test sample passes the second pattern for chunked size
242     fun `chunkSizeMatcher pattern finds matches in chunkedMessageSample`() {
243         val sizePattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
244         val matcher = sizePattern.matcher(validChunkedEncodedMsg)
245         assertTrue { matcher.find() }
246     }
247
248     @Test
249     fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
250         val msgPayload = "some text"
251         val msgId = "100"
252         stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) // no data available in the stream...
253         // Run the command
254         val communicator = NetconfDeviceCommunicator(
255             stubInputStream, mockOutputStream,
256             genDeviceInfo(), netconfSessionListener, replies
257         )
258         val completableFuture = communicator.sendMessage(msgPayload, msgId)
259         communicator.join()
260         // verify
261         Mockito.verify(mockOutputStream).write(any(), any(), any())
262         Mockito.verify(mockOutputStream).flush()
263         assertFalse { completableFuture.isCompletedExceptionally }
264     }
265
266     @Test
267     fun `sendMessage on IOError returns completed exceptionally future`() {
268         val msgPayload = "some text"
269         val msgId = "100"
270         Mockito.`when`(mockOutputStream.write(any(), any(), any()))
271             .thenThrow(IOException("Some IO error occurred!"))
272         stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) // no data available in the stream...
273         // Run the command
274         val communicator = NetconfDeviceCommunicator(
275             stubInputStream, mockOutputStream,
276             genDeviceInfo(), netconfSessionListener, replies
277         )
278         val completableFuture = communicator.sendMessage(msgPayload, msgId)
279         // verify
280         Mockito.verify(mockOutputStream).write(any(), any(), any())
281         Mockito.verify(mockOutputStream, never()).flush()
282         assertTrue { completableFuture.isCompletedExceptionally }
283     }
284
285     private fun genDeviceInfo(): DeviceInfo {
286         return DeviceInfo().apply {
287             username = "user"
288             password = "pass"
289             ipAddress = "localhost"
290             port = 4567
291         }
292     }
293 }