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)
55 private val chunkedEnding = "\n##\n"
56 //using example from section 4.2 of RFC6242 (https://tools.ietf.org/html/rfc6242#section-4.2)
57 private val validChunkedEncodedMsg = """
65 | xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
72 private fun stringToCharArray(str: String): List<Int> {
73 return str.toCharArray().map(Char::toInt)
78 netconfSession = mockk()
79 netconfSessionListener = mockk()
80 mockInputStream = mockk()
81 mockOutputStream = mockk()
82 replies = ConcurrentHashMap()
86 fun `NetconfDeviceCommunicator should read from supplied reader`() {
87 every { mockInputStream.read() } returns -1
88 every { mockInputStream.read(any(), any(), any()) } returns -1
89 val communicator: NetconfDeviceCommunicator =
90 NetconfDeviceCommunicator(mockInputStream, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies)
93 verify { mockInputStream.read(any(), any(), any()) }
97 fun `NetconfDeviceCommunicator unregisters device on END_PATTERN`() {
98 //The reader will generate RpcMessageUtils.END_PATTERN "]]>]]>" which tells Netconf
99 //to unregister the device.
100 //we want to capture the slot to return the value as inputStreamReader will pass a char array
101 //create a slot where NetconfReceivedEvent will be placed to further verify Type.DEVICE_UNREGISTERED
102 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
103 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
104 stubInputStream = RpcMessageUtils.END_PATTERN.byteInputStream(StandardCharsets.UTF_8)
105 val inputStreamSpy = spyk(stubInputStream)
107 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
108 genDeviceInfo(), netconfSessionListener, replies)
111 verify { inputStreamSpy.close() }
112 assertTrue { eventSlot.isCaptured }
113 assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
114 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
118 fun `NetconfDeviceCommunicator on IOException generated DEVICE_ERROR event`() {
119 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
120 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
121 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8)
122 val inputStreamSpy = spyk(stubInputStream)
123 every { inputStreamSpy.read(any(), any(), any()) } returns 1 andThenThrows IOException("Fake IO Exception")
125 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
126 genDeviceInfo(), netconfSessionListener, replies)
129 assertTrue { eventSlot.isCaptured }
130 assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
131 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
135 fun `NetconfDeviceCommunicator in END_PATTERN state but fails RpcMessageUtils end pattern validation`() {
136 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
137 val payload = "<rpc-reply>blah</rpc-reply>"
138 stubInputStream = "$payload${RpcMessageUtils.END_PATTERN}".byteInputStream(StandardCharsets.UTF_8)
139 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
141 val communicator = NetconfDeviceCommunicator(stubInputStream, mockOutputStream,
142 genDeviceInfo(), netconfSessionListener, replies)
145 verify(exactly = 0) { mockInputStream.close() } //make sure the reader is not closed as this could cause problems
146 assertTrue { eventSlot.isCaptured }
147 //eventually, sessionListener is called with message type DEVICE_REPLY
148 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
149 assertEquals(payload, eventSlot.captured.messagePayload)
153 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN but validation failing produces DEVICE_ERROR`() {
154 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
155 val payload = "<rpc-reply>blah</rpc-reply>"
156 val payloadWithChunkedEnding = "$payload$chunkedEnding"
157 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
159 stubInputStream = payloadWithChunkedEnding.byteInputStream(StandardCharsets.UTF_8)
160 //we have to ensure that the input stream is processed, so need to create a spy object.
161 val inputStreamSpy = spyk(stubInputStream)
163 val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(),
164 netconfSessionListener, replies)
167 verify(exactly = 0) { inputStreamSpy.close() } //make sure the reader is not closed as this could cause problems
168 assertTrue { eventSlot.isCaptured }
169 //eventually, sessionListener is called with message type DEVICE_REPLY
170 assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
171 assertEquals("", eventSlot.captured.messagePayload)
175 fun `NetconfDeviceCommunicator in END_CHUNKED_PATTERN passing validation generates DEVICE_REPLY`() {
176 val eventSlot = CapturingSlot<NetconfReceivedEvent>()
177 stubInputStream = validChunkedEncodedMsg.byteInputStream(StandardCharsets.UTF_8)
178 val inputStreamSpy = spyk(stubInputStream)
179 every { netconfSessionListener.accept(event = capture(eventSlot)) } just Runs
181 NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
183 verify(exactly = 0) { inputStreamSpy.close() } //make sure the reader is not closed as this could cause problems
184 assertTrue { eventSlot.isCaptured }
185 //eventually, sessionListener is called with message type DEVICE_REPLY
186 assertEquals(NetconfReceivedEvent.Type.DEVICE_REPLY, eventSlot.captured.type)
188 <rpc message-id="102"
189 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
192 """.trimIndent(), eventSlot.captured.messagePayload)
196 //test to ensure that we have a valid test message to be then used in the case of chunked message
197 // validation code path
198 fun `chunked sample is validated by the chunked response regex`() {
199 val test1 = "\n#10\nblah\n##\n"
200 val chunkedFramingPattern = Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
201 val matcher = chunkedFramingPattern.matcher(test1)
202 assertTrue { matcher.matches() }
206 //Verify that our test sample passes the second pattern for chunked size
207 fun `chunkSizeMatcher pattern finds matches in chunkedMessageSample`() {
208 val sizePattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
209 val matcher = sizePattern.matcher(validChunkedEncodedMsg)
210 assertTrue { matcher.find() }
214 fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
215 val msgPayload = "some text"
217 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) //no data available in the stream...
218 every { mockOutputStream.write(any(), any(), any()) } just Runs
219 every { mockOutputStream.write(msgPayload.toByteArray(Charsets.UTF_8)) } just Runs
220 every { mockOutputStream.flush() } just Runs
222 val communicator = NetconfDeviceCommunicator(
223 stubInputStream, mockOutputStream,
224 genDeviceInfo(), netconfSessionListener, replies)
225 val completableFuture = communicator.sendMessage(msgPayload, msgId)
228 verify { mockOutputStream.write(any(), any(), any()) }
229 verify { mockOutputStream.flush() }
230 assertFalse { completableFuture.isCompletedExceptionally }
234 fun `sendMessage on IOError returns completed exceptionally future`() {
235 val msgPayload = "some text"
237 every { mockOutputStream.write(any(), any(), any()) } throws IOException("Some IO error occurred!")
238 stubInputStream = "".byteInputStream(StandardCharsets.UTF_8) //no data available in the stream...
240 val communicator = NetconfDeviceCommunicator(
241 stubInputStream, mockOutputStream,
242 genDeviceInfo(), netconfSessionListener, replies)
243 val completableFuture = communicator.sendMessage(msgPayload, msgId)
245 verify { mockOutputStream.write(any(), any(), any()) }
246 verify(exactly = 0) { mockOutputStream.flush() }
247 assertTrue { completableFuture.isCompletedExceptionally }
250 private fun genDeviceInfo(): DeviceInfo {
251 return DeviceInfo().apply {
254 ipAddress = "localhost"