2 * Copyright © 2019 Bell Canada
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.core
19 import io.mockk.CapturingSlot
25 import io.mockk.verify
26 import org.junit.Before
28 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceInfo
29 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfReceivedEvent
30 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSession
31 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSessionListener
32 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.RpcMessageUtils
33 import java.io.IOException
34 import java.io.InputStream
35 import java.io.OutputStream
36 import java.nio.charset.StandardCharsets
37 import java.util.concurrent.CompletableFuture
38 import java.util.concurrent.ConcurrentHashMap
39 import java.util.regex.Pattern
40 import kotlin.test.assertEquals
41 import kotlin.test.assertFalse
42 import kotlin.test.assertTrue
44 class NetconfDeviceCommunicatorTest {
45 private lateinit var netconfSession: NetconfSession
46 private lateinit var netconfSessionListener: NetconfSessionListener
47 private lateinit var mockInputStream: InputStream
48 private lateinit var mockOutputStream: OutputStream
49 private lateinit var stubInputStream: InputStream
50 private lateinit var replies: MutableMap<String, CompletableFuture<String>>
51 private val endPatternCharArray: List<Int> = stringToCharArray(RpcMessageUtils.END_PATTERN)
54 private val chunkedEnding = "\n##\n"
55 // using example from section 4.2 of RFC6242 (https://tools.ietf.org/html/rfc6242#section-4.2)
56 private val validChunkedEncodedMsg = """
64 | xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
71 private fun stringToCharArray(str: String): List<Int> {
72 return str.toCharArray().map(Char::toInt)
77 netconfSession = mockk()
78 netconfSessionListener = mockk()
79 mockInputStream = mockk()
80 mockOutputStream = mockk()
81 replies = ConcurrentHashMap()
85 fun `NetconfDeviceCommunicator should read from supplied reader`() {
86 every { mockInputStream.read() } returns -1
87 every { mockInputStream.read(any(), any(), any()) } returns -1
88 val communicator: NetconfDeviceCommunicator =
89 NetconfDeviceCommunicator(mockInputStream, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies)
92 verify { mockInputStream.read(any(), any(), any()) }
96 fun `NetconfDeviceCommunicator unregisters device on END_PATTERN`() {
97 // The reader will generate RpcMessageUtils.END_PATTERN "]]>]]>" which tells Netconf
98 // to unregister the device.
99 // we want to capture the slot to return the value as inputStreamReader will pass a char array
100 // create a slot where NetconfReceivedEvent will be placed to further verify Type.DEVICE_UNREGISTERED
101 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
102 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
103 stubInputStream = RpcMessageUtils.END_PATTERN.byteInputStream(StandardCharsets.UTF_8)
104 val inputStreamSpy = spyk(stubInputStream)
106 val communicator = NetconfDeviceCommunicator(
107 inputStreamSpy, mockOutputStream,
108 genDeviceInfo(), netconfSessionListener, replies
112 verify { inputStreamSpy.close() }
113 assertTrue { eventSlot.isCaptured }
114 assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
115 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
119 fun `NetconfDeviceCommunicator on IOException generated DEVICE_ERROR event`() {
120 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
121 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
122 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8)
123 val inputStreamSpy = spyk(stubInputStream)
124 every { inputStreamSpy.read(any(), any(), any()) } returns 1 andThenThrows IOException("Fake IO Exception")
126 val communicator = NetconfDeviceCommunicator(
127 inputStreamSpy, mockOutputStream,
128 genDeviceInfo(), netconfSessionListener, replies
132 assertTrue { eventSlot.isCaptured }
133 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
134 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
138 fun `NetconfDeviceCommunicator in END_PATTERN state but fails RpcMessageUtils end pattern validation`() {
139 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
140 val payload = "<rpc-reply>blah</rpc-reply>"
141 stubInputStream = "$payload${RpcMessageUtils.END_PATTERN}".byteInputStream(StandardCharsets.UTF_8)
142 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
144 val communicator = NetconfDeviceCommunicator(
145 stubInputStream, mockOutputStream,
146 genDeviceInfo(), netconfSessionListener, replies
150 verify(exactly = 0) { mockInputStream.close() } // make sure the reader is not closed as this could cause problems
151 assertTrue { eventSlot.isCaptured }
152 // eventually, sessionListener is called with message type DEVICE_REPLY
153 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
154 assertEquals(payload, eventSlot.captured.messagePayload)
158 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN but validation failing produces DEVICE_ERROR`() {
159 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
160 val payload = "<rpc-reply>blah</rpc-reply>"
161 val payloadWithChunkedEnding = "$payload$chunkedEnding"
162 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
164 stubInputStream = payloadWithChunkedEnding.byteInputStream(StandardCharsets.UTF_8)
165 // we have to ensure that the input stream is processed, so need to create a spy object.
166 val inputStreamSpy = spyk(stubInputStream)
168 val communicator = NetconfDeviceCommunicator(
169 inputStreamSpy, mockOutputStream, genDeviceInfo(),
170 netconfSessionListener, replies
174 verify(exactly = 0) { inputStreamSpy.close() } // make sure the reader is not closed as this could cause problems
175 assertTrue { eventSlot.isCaptured }
176 // eventually, sessionListener is called with message type DEVICE_REPLY
177 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
178 assertEquals("", eventSlot.captured.messagePayload)
182 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN passing validation generates DEVICE_REPLY`() {
183 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
184 stubInputStream = validChunkedEncodedMsg.byteInputStream(StandardCharsets.UTF_8)
185 val inputStreamSpy = spyk(stubInputStream)
186 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
188 NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
190 verify(exactly = 0) { inputStreamSpy.close() } // make sure the reader is not closed as this could cause problems
191 assertTrue { eventSlot.isCaptured }
192 // eventually, sessionListener is called with message type DEVICE_REPLY
193 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
196 <rpc message-id="102"
197 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
200 """.trimIndent(), eventSlot.captured.messagePayload
205 // test to ensure that we have a valid test message to be then used in the case of chunked message
206 // validation code path
207 fun `chunked sample is validated by the chunked response regex`() {
208 val test1 = "\n#10\nblah\n##\n"
209 val chunkedFramingPattern = Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
210 val matcher = chunkedFramingPattern.matcher(test1)
211 assertTrue { matcher.matches() }
215 // Verify that our test sample passes the second pattern for chunked size
216 fun `chunkSizeMatcher pattern finds matches in chunkedMessageSample`() {
217 val sizePattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
218 val matcher = sizePattern.matcher(validChunkedEncodedMsg)
219 assertTrue { matcher.find() }
223 fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
224 val msgPayload = "some text"
226 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) // no data available in the stream...
227 every { mockOutputStream.write(any(), any(), any()) } just Runs
228 every { mockOutputStream.write(msgPayload.toByteArray(Charsets.UTF_8)) } just Runs
229 every { mockOutputStream.flush() } just Runs
231 val communicator = NetconfDeviceCommunicator(
232 stubInputStream, mockOutputStream,
233 genDeviceInfo(), netconfSessionListener, replies
235 val completableFuture = communicator.sendMessage(msgPayload, msgId)
238 verify { mockOutputStream.write(any(), any(), any()) }
239 verify { mockOutputStream.flush() }
240 assertFalse { completableFuture.isCompletedExceptionally }
244 fun `sendMessage on IOError returns completed exceptionally future`() {
245 val msgPayload = "some text"
247 every { mockOutputStream.write(any(), any(), any()) } throws IOException("Some IO error occurred!")
248 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) // no data available in the stream...
250 val communicator = NetconfDeviceCommunicator(
251 stubInputStream, mockOutputStream,
252 genDeviceInfo(), netconfSessionListener, replies
254 val completableFuture = communicator.sendMessage(msgPayload, msgId)
256 verify { mockOutputStream.write(any(), any(), any()) }
257 verify(exactly = 0) { mockOutputStream.flush() }
258 assertTrue { completableFuture.isCompletedExceptionally }
261 private fun genDeviceInfo(): DeviceInfo {
262 return DeviceInfo().apply {
265 ipAddress = "localhost"