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
27 import org.junit.Ignore
29 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceInfo
30 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfReceivedEvent
31 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSession
32 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfSessionListener
33 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.RpcMessageUtils
34 import java.io.ByteArrayInputStream
35 import java.io.IOException
36 import java.io.InputStream
37 import java.io.OutputStream
40 import java.nio.charset.StandardCharsets
41 import java.util.concurrent.CompletableFuture
42 import java.util.concurrent.ConcurrentHashMap
43 import java.util.regex.Pattern
44 import kotlin.test.assertEquals
45 import kotlin.test.assertFalse
46 import kotlin.test.assertTrue
48 class NetconfDeviceCommunicatorTest {
49 private lateinit var netconfSession: NetconfSession
50 private lateinit var netconfSessionListener: NetconfSessionListener
51 private lateinit var mockInputStream: InputStream
52 private lateinit var mockOutputStream: OutputStream
53 private lateinit var stubInputStream: InputStream
54 private lateinit var replies: MutableMap<String, CompletableFuture<String>>
55 private val endPatternCharArray: List<Int> = stringToCharArray(RpcMessageUtils.END_PATTERN)
59 private val chunkedEnding = "\n##\n"
60 //using example from section 4.2 of RFC6242 (https://tools.ietf.org/html/rfc6242#section-4.2)
61 private val validChunkedEncodedMsg = """
70 | xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
77 private fun stringToCharArray(str: String): List<Int> {
78 return str.toCharArray().map(Char::toInt)
83 netconfSession = mockk()
84 netconfSessionListener = mockk()
85 mockInputStream = mockk()
86 mockOutputStream = mockk()
87 replies = ConcurrentHashMap()
91 fun `NetconfDeviceCommunicator should read from supplied reader`() {
92 every { mockInputStream.read() } returns -1
93 every { mockInputStream.read(any(), any(), any()) } returns -1
94 val communicator: NetconfDeviceCommunicator =
95 NetconfDeviceCommunicator(mockInputStream, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies)
98 verify { mockInputStream.read(any(), any(), any()) }
102 fun `NetconfDeviceCommunicator unregisters device on END_PATTERN`() {
103 //The reader will generate RpcMessageUtils.END_PATTERN "]]>]]>" which tells Netconf
104 //to unregister the device.
105 //we want to capture the slot to return the value as inputStreamReader will pass a char array
106 //create a slot where NetconfReceivedEvent will be placed to further verify Type.DEVICE_UNREGISTERED
107 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
108 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
109 stubInputStream = RpcMessageUtils.END_PATTERN.byteInputStream(StandardCharsets.UTF_8)
110 val inputStreamSpy = spyk(stubInputStream)
112 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
113 genDeviceInfo(), netconfSessionListener, replies)
116 verify { inputStreamSpy.close() }
117 assertTrue { eventSlot.isCaptured }
118 assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
119 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
123 fun `NetconfDeviceCommunicator on IOException generated DEVICE_ERROR event`() {
124 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
125 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
126 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8)
127 val inputStreamSpy = spyk(stubInputStream)
128 every { inputStreamSpy.read(any(), any(), any()) } returns 1 andThenThrows IOException("Fake IO Exception")
130 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
131 genDeviceInfo(), netconfSessionListener, replies)
134 assertTrue { eventSlot.isCaptured }
135 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
136 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
140 fun `NetconfDeviceCommunicator in END_PATTERN state but fails RpcMessageUtils end pattern validation`() {
141 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
142 val payload = "<rpc-reply>blah</rpc-reply>"
143 stubInputStream = "$payload${RpcMessageUtils.END_PATTERN}".byteInputStream(StandardCharsets.UTF_8)
144 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
146 val communicator = NetconfDeviceCommunicator(stubInputStream, mockOutputStream,
147 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(inputStreamSpy, mockOutputStream, genDeviceInfo(),
169 netconfSessionListener, replies)
172 verify(exactly = 0) { inputStreamSpy.close() } //make sure the reader is not closed as this could cause problems
173 assertTrue { eventSlot.isCaptured }
174 //eventually, sessionListener is called with message type DEVICE_REPLY
175 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
176 assertEquals("", eventSlot.captured.messagePayload)
179 @Ignore //TODO: Not clear on validateChunkedFraming, the size validation part could be discarding valid msg..
181 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN passing validation generates DEVICE_REPLY`() {
182 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
183 stubInputStream = validChunkedEncodedMsg.byteInputStream(StandardCharsets.UTF_8)
184 val inputStreamSpy = spyk(stubInputStream)
185 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
187 NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
189 verify(exactly = 0) { inputStreamSpy.close() } //make sure the reader is not closed as this could cause problems
190 assertTrue { eventSlot.isCaptured }
191 //eventually, sessionListener is called with message type DEVICE_REPLY
192 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
193 assertEquals("", eventSlot.captured.messagePayload)
197 //test to ensure that we have a valid test message to be then used in the case of chunked message
198 // validation code path
199 fun `chunked sample is validated by the chunked response regex`() {
200 val test1 = "\n#10\nblah\n##\n"
201 val chunkedFramingPattern = Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
202 val matcher = chunkedFramingPattern.matcher(validChunkedEncodedMsg)
203 assertTrue { matcher.matches() }
207 //Verify that our test sample passes the second pattern for chunked size
208 fun `chunkSizeMatcher pattern finds matches in chunkedMessageSample`() {
209 val sizePattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
210 val matcher = sizePattern.matcher(validChunkedEncodedMsg)
211 assertTrue { matcher.find() }
215 fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
216 val msgPayload = "some text"
218 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) //no data available in the stream...
219 every { mockOutputStream.write(any(), any(), any()) } just Runs
220 every { mockOutputStream.write(msgPayload.toByteArray(Charsets.UTF_8)) } just Runs
221 every { mockOutputStream.flush() } just Runs
223 val communicator = NetconfDeviceCommunicator(
224 stubInputStream, mockOutputStream,
225 genDeviceInfo(), netconfSessionListener, replies)
226 val completableFuture = communicator.sendMessage(msgPayload, msgId)
229 verify { mockOutputStream.write(any(), any(), any()) }
230 verify { mockOutputStream.flush() }
231 assertFalse { completableFuture.isCompletedExceptionally }
235 fun `sendMessage on IOError returns completed exceptionally future`() {
236 val msgPayload = "some text"
238 every { mockOutputStream.write(any(), any(), any()) } throws IOException("Some IO error occurred!")
239 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) //no data available in the stream...
241 val communicator = NetconfDeviceCommunicator(
242 stubInputStream, mockOutputStream,
243 genDeviceInfo(), netconfSessionListener, replies)
244 val completableFuture = communicator.sendMessage(msgPayload, msgId)
246 verify { mockOutputStream.write(any(), any(), any()) }
247 verify(exactly = 0) { mockOutputStream.flush() }
248 assertTrue { completableFuture.isCompletedExceptionally }
251 private fun genDeviceInfo(): DeviceInfo {
252 return DeviceInfo().apply {
255 ipAddress = "localhost"