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 {
46 private lateinit var netconfSession: NetconfSession
47 private lateinit var netconfSessionListener: NetconfSessionListener
48 private lateinit var mockInputStream: InputStream
49 private lateinit var mockOutputStream: OutputStream
50 private lateinit var stubInputStream: InputStream
51 private lateinit var replies: MutableMap<String, CompletableFuture<String>>
52 private val endPatternCharArray: List<Int> = stringToCharArray(RpcMessageUtils.END_PATTERN)
56 private val chunkedEnding = "\n##\n"
58 // using example from section 4.2 of RFC6242 (https://tools.ietf.org/html/rfc6242#section-4.2)
59 private val validChunkedEncodedMsg = """
67 | xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
74 private fun stringToCharArray(str: String): List<Int> {
75 return str.toCharArray().map(Char::toInt)
80 netconfSession = mockk()
81 netconfSessionListener = mockk()
82 mockInputStream = mockk()
83 mockOutputStream = mockk()
84 replies = ConcurrentHashMap()
88 fun `NetconfDeviceCommunicator should read from supplied reader`() {
89 every { mockInputStream.read() } returns -1
90 every { mockInputStream.read(any(), any(), any()) } returns -1
91 val communicator: NetconfDeviceCommunicator =
92 NetconfDeviceCommunicator(mockInputStream, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies)
95 verify { mockInputStream.read(any(), any(), any()) }
99 fun `NetconfDeviceCommunicator unregisters device on END_PATTERN`() {
100 // The reader will generate RpcMessageUtils.END_PATTERN "]]>]]>" which tells Netconf
101 // to unregister the device.
102 // we want to capture the slot to return the value as inputStreamReader will pass a char array
103 // create a slot where NetconfReceivedEvent will be placed to further verify Type.DEVICE_UNREGISTERED
104 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
105 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
106 stubInputStream = RpcMessageUtils.END_PATTERN.byteInputStream(StandardCharsets.UTF_8)
107 val inputStreamSpy = spyk(stubInputStream)
109 val communicator = NetconfDeviceCommunicator(
110 inputStreamSpy, mockOutputStream,
111 genDeviceInfo(), netconfSessionListener, replies
115 verify { inputStreamSpy.close() }
116 assertTrue { eventSlot.isCaptured }
117 assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
118 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
122 fun `NetconfDeviceCommunicator on IOException generated DEVICE_ERROR event`() {
123 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
124 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
125 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8)
126 val inputStreamSpy = spyk(stubInputStream)
127 every { inputStreamSpy.read(any(), any(), any()) } returns 1 andThenThrows IOException("Fake IO Exception")
129 val communicator = NetconfDeviceCommunicator(
130 inputStreamSpy, mockOutputStream,
131 genDeviceInfo(), netconfSessionListener, replies
135 assertTrue { eventSlot.isCaptured }
136 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
137 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
141 fun `NetconfDeviceCommunicator in END_PATTERN state but fails RpcMessageUtils end pattern validation`() {
142 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
143 val payload = "<rpc-reply>blah</rpc-reply>"
144 stubInputStream = "$payload${RpcMessageUtils.END_PATTERN}".byteInputStream(StandardCharsets.UTF_8)
145 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
147 val communicator = NetconfDeviceCommunicator(
148 stubInputStream, mockOutputStream,
149 genDeviceInfo(), netconfSessionListener, replies
153 verify(exactly = 0) { mockInputStream.close() } // make sure the reader is not closed as this could cause problems
154 assertTrue { eventSlot.isCaptured }
155 // eventually, sessionListener is called with message type DEVICE_REPLY
156 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
157 assertEquals(payload, eventSlot.captured.messagePayload)
161 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN but validation failing produces DEVICE_ERROR`() {
162 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
163 val payload = "<rpc-reply>blah</rpc-reply>"
164 val payloadWithChunkedEnding = "$payload$chunkedEnding"
165 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
167 stubInputStream = payloadWithChunkedEnding.byteInputStream(StandardCharsets.UTF_8)
168 // we have to ensure that the input stream is processed, so need to create a spy object.
169 val inputStreamSpy = spyk(stubInputStream)
171 val communicator = NetconfDeviceCommunicator(
172 inputStreamSpy, mockOutputStream, genDeviceInfo(),
173 netconfSessionListener, replies
177 verify(exactly = 0) { inputStreamSpy.close() } // make sure the reader is not closed as this could cause problems
178 assertTrue { eventSlot.isCaptured }
179 // eventually, sessionListener is called with message type DEVICE_REPLY
180 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
181 assertEquals("", eventSlot.captured.messagePayload)
185 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN passing validation generates DEVICE_REPLY`() {
186 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
187 stubInputStream = validChunkedEncodedMsg.byteInputStream(StandardCharsets.UTF_8)
188 val inputStreamSpy = spyk(stubInputStream)
189 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
191 NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
193 verify(exactly = 0) { inputStreamSpy.close() } // make sure the reader is not closed as this could cause problems
194 assertTrue { eventSlot.isCaptured }
195 // eventually, sessionListener is called with message type DEVICE_REPLY
196 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
199 <rpc message-id="102"
200 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
204 eventSlot.captured.messagePayload
209 // test to ensure that we have a valid test message to be then used in the case of chunked message
210 // validation code path
211 fun `chunked sample is validated by the chunked response regex`() {
212 val test1 = "\n#10\nblah\n##\n"
213 val chunkedFramingPattern = Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
214 val matcher = chunkedFramingPattern.matcher(test1)
215 assertTrue { matcher.matches() }
219 // Verify that our test sample passes the second pattern for chunked size
220 fun `chunkSizeMatcher pattern finds matches in chunkedMessageSample`() {
221 val sizePattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
222 val matcher = sizePattern.matcher(validChunkedEncodedMsg)
223 assertTrue { matcher.find() }
227 fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
228 val msgPayload = "some text"
230 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) // no data available in the stream...
231 every { mockOutputStream.write(any(), any(), any()) } just Runs
232 every { mockOutputStream.write(msgPayload.toByteArray(Charsets.UTF_8)) } just Runs
233 every { mockOutputStream.flush() } just Runs
235 val communicator = NetconfDeviceCommunicator(
236 stubInputStream, mockOutputStream,
237 genDeviceInfo(), netconfSessionListener, replies
239 val completableFuture = communicator.sendMessage(msgPayload, msgId)
242 verify { mockOutputStream.write(any(), any(), any()) }
243 verify { mockOutputStream.flush() }
244 assertFalse { completableFuture.isCompletedExceptionally }
248 fun `sendMessage on IOError returns completed exceptionally future`() {
249 val msgPayload = "some text"
251 every { mockOutputStream.write(any(), any(), any()) } throws IOException("Some IO error occurred!")
252 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) // no data available in the stream...
254 val communicator = NetconfDeviceCommunicator(
255 stubInputStream, mockOutputStream,
256 genDeviceInfo(), netconfSessionListener, replies
258 val completableFuture = communicator.sendMessage(msgPayload, msgId)
260 verify { mockOutputStream.write(any(), any(), any()) }
261 verify(exactly = 0) { mockOutputStream.flush() }
262 assertTrue { completableFuture.isCompletedExceptionally }
265 private fun genDeviceInfo(): DeviceInfo {
266 return DeviceInfo().apply {
269 ipAddress = "localhost"