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
20 import org.junit.Before
22 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceInfo
23 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfReceivedEvent
24 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSession
25 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSessionListener
26 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.RpcMessageUtils
27 import java.io.IOException
28 import java.io.InputStream
29 import java.io.OutputStream
30 import java.nio.charset.*
31 import java.util.concurrent.*
32 import java.util.regex.*
33 import kotlin.test.assertEquals
34 import kotlin.test.assertFalse
35 import kotlin.test.assertTrue
37 class NetconfDeviceCommunicatorTest {
38 private lateinit var netconfSession: NetconfSession
39 private lateinit var netconfSessionListener: NetconfSessionListener
40 private lateinit var mockInputStream: InputStream
41 private lateinit var mockOutputStream: OutputStream
42 private lateinit var stubInputStream: InputStream
43 private lateinit var replies: MutableMap<String, CompletableFuture<String>>
44 private val endPatternCharArray: List<Int> = stringToCharArray(RpcMessageUtils.END_PATTERN)
48 private val chunkedEnding = "\n##\n"
49 //using example from section 4.2 of RFC6242 (https://tools.ietf.org/html/rfc6242#section-4.2)
50 private val validChunkedEncodedMsg = """
58 | xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
65 private fun stringToCharArray(str: String): List<Int> {
66 return str.toCharArray().map(Char::toInt)
71 netconfSession = mockk()
72 netconfSessionListener = mockk()
73 mockInputStream = mockk()
74 mockOutputStream = mockk()
75 replies = ConcurrentHashMap()
79 fun `NetconfDeviceCommunicator should read from supplied reader`() {
80 every { mockInputStream.read() } returns -1
81 every { mockInputStream.read(any(), any(), any()) } returns -1
82 val communicator: NetconfDeviceCommunicator =
83 NetconfDeviceCommunicator(mockInputStream, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies)
86 verify { mockInputStream.read(any(), any(), any()) }
90 fun `NetconfDeviceCommunicator unregisters device on END_PATTERN`() {
91 //The reader will generate RpcMessageUtils.END_PATTERN "]]>]]>" which tells Netconf
92 //to unregister the device.
93 //we want to capture the slot to return the value as inputStreamReader will pass a char array
94 //create a slot where NetconfReceivedEvent will be placed to further verify Type.DEVICE_UNREGISTERED
95 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
96 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
97 stubInputStream = RpcMessageUtils.END_PATTERN.byteInputStream(StandardCharsets.UTF_8)
98 val inputStreamSpy = spyk(stubInputStream)
100 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
101 genDeviceInfo(), netconfSessionListener, replies)
104 verify { inputStreamSpy.close() }
105 assertTrue { eventSlot.isCaptured }
106 assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
107 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
111 fun `NetconfDeviceCommunicator on IOException generated DEVICE_ERROR event`() {
112 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
113 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
114 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8)
115 val inputStreamSpy = spyk(stubInputStream)
116 every { inputStreamSpy.read(any(), any(), any()) } returns 1 andThenThrows IOException("Fake IO Exception")
118 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
119 genDeviceInfo(), netconfSessionListener, replies)
122 assertTrue { eventSlot.isCaptured }
123 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
124 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
128 fun `NetconfDeviceCommunicator in END_PATTERN state but fails RpcMessageUtils end pattern validation`() {
129 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
130 val payload = "<rpc-reply>blah</rpc-reply>"
131 stubInputStream = "$payload${RpcMessageUtils.END_PATTERN}".byteInputStream(StandardCharsets.UTF_8)
132 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
134 val communicator = NetconfDeviceCommunicator(stubInputStream, mockOutputStream,
135 genDeviceInfo(), netconfSessionListener, replies)
138 verify(exactly = 0) { mockInputStream.close() } //make sure the reader is not closed as this could cause problems
139 assertTrue { eventSlot.isCaptured }
140 //eventually, sessionListener is called with message type DEVICE_REPLY
141 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
142 assertEquals(payload, eventSlot.captured.messagePayload)
146 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN but validation failing produces DEVICE_ERROR`() {
147 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
148 val payload = "<rpc-reply>blah</rpc-reply>"
149 val payloadWithChunkedEnding = "$payload$chunkedEnding"
150 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
152 stubInputStream = payloadWithChunkedEnding.byteInputStream(StandardCharsets.UTF_8)
153 //we have to ensure that the input stream is processed, so need to create a spy object.
154 val inputStreamSpy = spyk(stubInputStream)
156 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(),
157 netconfSessionListener, replies)
160 verify(exactly = 0) { inputStreamSpy.close() } //make sure the reader is not closed as this could cause problems
161 assertTrue { eventSlot.isCaptured }
162 //eventually, sessionListener is called with message type DEVICE_REPLY
163 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
164 assertEquals("", eventSlot.captured.messagePayload)
168 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN passing validation generates DEVICE_REPLY`() {
169 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
170 stubInputStream = validChunkedEncodedMsg.byteInputStream(StandardCharsets.UTF_8)
171 val inputStreamSpy = spyk(stubInputStream)
172 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
174 NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
176 verify(exactly = 0) { inputStreamSpy.close() } //make sure the reader is not closed as this could cause problems
177 assertTrue { eventSlot.isCaptured }
178 //eventually, sessionListener is called with message type DEVICE_REPLY
179 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
181 <rpc message-id="102"
182 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
185 """.trimIndent(), eventSlot.captured.messagePayload)
189 //test to ensure that we have a valid test message to be then used in the case of chunked message
190 // validation code path
191 fun `chunked sample is validated by the chunked response regex`() {
192 val test1 = "\n#10\nblah\n##\n"
193 val chunkedFramingPattern = Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
194 val matcher = chunkedFramingPattern.matcher(test1)
195 assertTrue { matcher.matches() }
199 //Verify that our test sample passes the second pattern for chunked size
200 fun `chunkSizeMatcher pattern finds matches in chunkedMessageSample`() {
201 val sizePattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
202 val matcher = sizePattern.matcher(validChunkedEncodedMsg)
203 assertTrue { matcher.find() }
207 fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
208 val msgPayload = "some text"
210 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) //no data available in the stream...
211 every { mockOutputStream.write(any(), any(), any()) } just Runs
212 every { mockOutputStream.write(msgPayload.toByteArray(Charsets.UTF_8)) } just Runs
213 every { mockOutputStream.flush() } just Runs
215 val communicator = NetconfDeviceCommunicator(
216 stubInputStream, mockOutputStream,
217 genDeviceInfo(), netconfSessionListener, replies)
218 val completableFuture = communicator.sendMessage(msgPayload, msgId)
221 verify { mockOutputStream.write(any(), any(), any()) }
222 verify { mockOutputStream.flush() }
223 assertFalse { completableFuture.isCompletedExceptionally }
227 fun `sendMessage on IOError returns completed exceptionally future`() {
228 val msgPayload = "some text"
230 every { mockOutputStream.write(any(), any(), any()) } throws IOException("Some IO error occurred!")
231 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) //no data available in the stream...
233 val communicator = NetconfDeviceCommunicator(
234 stubInputStream, mockOutputStream,
235 genDeviceInfo(), netconfSessionListener, replies)
236 val completableFuture = communicator.sendMessage(msgPayload, msgId)
238 verify { mockOutputStream.write(any(), any(), any()) }
239 verify(exactly = 0) { mockOutputStream.flush() }
240 assertTrue { completableFuture.isCompletedExceptionally }
243 private fun genDeviceInfo(): DeviceInfo {
244 return DeviceInfo().apply {
247 ipAddress = "localhost"