2240f48c275131a865cd17eab3d7fb63fe469436
[ccsdk/cds.git] /
1 /*
2  * Copyright © 2019 Bell Canada
3  *
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
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.core
18
19 import io.mockk.*
20 import org.junit.Before
21 import org.junit.Test
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
36
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)
45
46
47     companion object {
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 = """
51             |
52             |#4
53             |<rpc
54             |#18
55             | message-id="102"
56             |
57             |#79
58             |     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
59             |  <close-session/>
60             |</rpc>
61             |##
62             |""".trimMargin()
63     }
64
65     private fun stringToCharArray(str: String): List<Int> {
66         return str.toCharArray().map(Char::toInt)
67     }
68
69     @Before
70     fun setup() {
71         netconfSession = mockk()
72         netconfSessionListener = mockk()
73         mockInputStream = mockk()
74         mockOutputStream = mockk()
75         replies = ConcurrentHashMap()
76     }
77
78     @Test
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)
84         communicator.join()
85         //verify
86         verify { mockInputStream.read(any(), any(), any()) }
87     }
88
89     @Test
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)
99         //RUN the test
100         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
101             genDeviceInfo(), netconfSessionListener, replies)
102         communicator.join()
103         //Verify
104         verify { inputStreamSpy.close() }
105         assertTrue { eventSlot.isCaptured }
106         assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
107         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
108     }
109
110     @Test
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")
117         //RUN THE TEST
118         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
119             genDeviceInfo(), netconfSessionListener, replies)
120         communicator.join()
121         //Verify
122         assertTrue { eventSlot.isCaptured }
123         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
124         assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
125     }
126
127     @Test
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
133         //RUN the test
134         val communicator = NetconfDeviceCommunicator(stubInputStream, mockOutputStream,
135             genDeviceInfo(), netconfSessionListener, replies)
136         communicator.join()
137         //Verify
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)
143     }
144
145     @Test
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
151
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)
155         //RUN the test
156         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(),
157             netconfSessionListener, replies)
158         communicator.join()
159         //Verify
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)
165     }
166
167     @Test
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
173         //RUN the test
174         NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
175         //Verify
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)
180         assertEquals("""
181         <rpc message-id="102"
182              xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
183           <close-session/>
184         </rpc>
185         """.trimIndent(), eventSlot.captured.messagePayload)
186     }
187
188     @Test
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() }
196     }
197
198     @Test
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() }
204     }
205
206     @Test
207     fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
208         val msgPayload = "some text"
209         val msgId = "100"
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
214         //Run the command
215         val communicator = NetconfDeviceCommunicator(
216             stubInputStream, mockOutputStream,
217             genDeviceInfo(), netconfSessionListener, replies)
218         val completableFuture = communicator.sendMessage(msgPayload, msgId)
219         communicator.join()
220         //verify
221         verify { mockOutputStream.write(any(), any(), any()) }
222         verify { mockOutputStream.flush() }
223         assertFalse { completableFuture.isCompletedExceptionally }
224     }
225
226     @Test
227     fun `sendMessage on IOError returns completed exceptionally future`() {
228         val msgPayload = "some text"
229         val msgId = "100"
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...
232         //Run the command
233         val communicator = NetconfDeviceCommunicator(
234             stubInputStream, mockOutputStream,
235             genDeviceInfo(), netconfSessionListener, replies)
236         val completableFuture = communicator.sendMessage(msgPayload, msgId)
237         //verify
238         verify { mockOutputStream.write(any(), any(), any()) }
239         verify(exactly = 0) { mockOutputStream.flush() }
240         assertTrue { completableFuture.isCompletedExceptionally }
241     }
242
243     private fun genDeviceInfo(): DeviceInfo {
244         return DeviceInfo().apply {
245             username = "user"
246             password = "pass"
247             ipAddress = "localhost"
248             port = 4567
249         }
250     }
251
252 }