c70a43e4938f9f37944b67de3d239e7a4b71493a
[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.CapturingSlot
20 import io.mockk.Runs
21 import io.mockk.every
22 import io.mockk.just
23 import io.mockk.mockk
24 import io.mockk.spyk
25 import io.mockk.verify
26 import org.junit.Before
27 import org.junit.Test
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
43
44 class NetconfDeviceCommunicatorTest {
45
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)
53
54     companion object {
55
56         private val chunkedEnding = "\n##\n"
57
58         // using example from section 4.2 of RFC6242 (https://tools.ietf.org/html/rfc6242#section-4.2)
59         private val validChunkedEncodedMsg = """
60             |
61             |#4
62             |<rpc
63             |#18
64             | message-id="102"
65             |
66             |#79
67             |     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
68             |  <close-session/>
69             |</rpc>
70             |##
71             |""".trimMargin()
72     }
73
74     private fun stringToCharArray(str: String): List<Int> {
75         return str.toCharArray().map(Char::toInt)
76     }
77
78     @Before
79     fun setup() {
80         netconfSession = mockk()
81         netconfSessionListener = mockk()
82         mockInputStream = mockk()
83         mockOutputStream = mockk()
84         replies = ConcurrentHashMap()
85     }
86
87     @Test
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)
93         communicator.join()
94         // verify
95         verify { mockInputStream.read(any(), any(), any()) }
96     }
97
98     @Test
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)
108         // RUN the test
109         val communicator = NetconfDeviceCommunicator(
110             inputStreamSpy, mockOutputStream,
111             genDeviceInfo(), netconfSessionListener, replies
112         )
113         communicator.join()
114         // Verify
115         verify { inputStreamSpy.close() }
116         assertTrue { eventSlot.isCaptured }
117         assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
118         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
119     }
120
121     @Test
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")
128         // RUN THE TEST
129         val communicator = NetconfDeviceCommunicator(
130             inputStreamSpy, mockOutputStream,
131             genDeviceInfo(), netconfSessionListener, replies
132         )
133         communicator.join()
134         // Verify
135         assertTrue { eventSlot.isCaptured }
136         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
137         assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
138     }
139
140     @Test
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
146         // RUN the test
147         val communicator = NetconfDeviceCommunicator(
148             stubInputStream, mockOutputStream,
149             genDeviceInfo(), netconfSessionListener, replies
150         )
151         communicator.join()
152         // Verify
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)
158     }
159
160     @Test
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
166
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)
170         // RUN the test
171         val communicator = NetconfDeviceCommunicator(
172             inputStreamSpy, mockOutputStream, genDeviceInfo(),
173             netconfSessionListener, replies
174         )
175         communicator.join()
176         // Verify
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)
182     }
183
184     @Test
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
190         // RUN the test
191         NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
192         // Verify
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)
197         assertEquals(
198             """
199         <rpc message-id="102"
200              xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
201           <close-session/>
202         </rpc>
203             """.trimIndent(),
204             eventSlot.captured.messagePayload
205         )
206     }
207
208     @Test
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() }
216     }
217
218     @Test
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() }
224     }
225
226     @Test
227     fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
228         val msgPayload = "some text"
229         val msgId = "100"
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
234         // Run the command
235         val communicator = NetconfDeviceCommunicator(
236             stubInputStream, mockOutputStream,
237             genDeviceInfo(), netconfSessionListener, replies
238         )
239         val completableFuture = communicator.sendMessage(msgPayload, msgId)
240         communicator.join()
241         // verify
242         verify { mockOutputStream.write(any(), any(), any()) }
243         verify { mockOutputStream.flush() }
244         assertFalse { completableFuture.isCompletedExceptionally }
245     }
246
247     @Test
248     fun `sendMessage on IOError returns completed exceptionally future`() {
249         val msgPayload = "some text"
250         val msgId = "100"
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...
253         // Run the command
254         val communicator = NetconfDeviceCommunicator(
255             stubInputStream, mockOutputStream,
256             genDeviceInfo(), netconfSessionListener, replies
257         )
258         val completableFuture = communicator.sendMessage(msgPayload, msgId)
259         // verify
260         verify { mockOutputStream.write(any(), any(), any()) }
261         verify(exactly = 0) { mockOutputStream.flush() }
262         assertTrue { completableFuture.isCompletedExceptionally }
263     }
264
265     private fun genDeviceInfo(): DeviceInfo {
266         return DeviceInfo().apply {
267             username = "user"
268             password = "pass"
269             ipAddress = "localhost"
270             port = 4567
271         }
272     }
273 }