47c729187d96153164c962fe15a643abf5149fe6
[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     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)
52
53
54     companion object {
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 = """
58             |
59             |#4
60             |<rpc
61             |#18
62             | message-id="102"
63             |
64             |#79
65             |     xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
66             |  <close-session/>
67             |</rpc>
68             |##
69             |""".trimMargin()
70     }
71
72     private fun stringToCharArray(str: String): List<Int> {
73         return str.toCharArray().map(Char::toInt)
74     }
75
76     @Before
77     fun setup() {
78         netconfSession = mockk()
79         netconfSessionListener = mockk()
80         mockInputStream = mockk()
81         mockOutputStream = mockk()
82         replies = ConcurrentHashMap()
83     }
84
85     @Test
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)
91         communicator.join()
92         //verify
93         verify { mockInputStream.read(any(), any(), any()) }
94     }
95
96     @Test
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)
106         //RUN the test
107         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
108             genDeviceInfo(), netconfSessionListener, replies)
109         communicator.join()
110         //Verify
111         verify { inputStreamSpy.close() }
112         assertTrue { eventSlot.isCaptured }
113         assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
114         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
115     }
116
117     @Test
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")
124         //RUN THE TEST
125         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
126             genDeviceInfo(), netconfSessionListener, replies)
127         communicator.join()
128         //Verify
129         assertTrue { eventSlot.isCaptured }
130         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
131         assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
132     }
133
134     @Test
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
140         //RUN the test
141         val communicator = NetconfDeviceCommunicator(stubInputStream, mockOutputStream,
142             genDeviceInfo(), netconfSessionListener, replies)
143         communicator.join()
144         //Verify
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)
150     }
151
152     @Test
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
158
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)
162         //RUN the test
163         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(),
164             netconfSessionListener, replies)
165         communicator.join()
166         //Verify
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)
172     }
173
174     @Test
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
180         //RUN the test
181         NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
182         //Verify
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)
187         assertEquals("""
188 <rpc message-id="102"
189      xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
190   <close-session/>
191 </rpc>
192         """.trimIndent(), eventSlot.captured.messagePayload)
193     }
194
195     @Test
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() }
203     }
204
205     @Test
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() }
211     }
212
213     @Test
214     fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
215         val msgPayload = "some text"
216         val msgId = "100"
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
221         //Run the command
222         val communicator = NetconfDeviceCommunicator(
223             stubInputStream, mockOutputStream,
224             genDeviceInfo(), netconfSessionListener, replies)
225         val completableFuture = communicator.sendMessage(msgPayload, msgId)
226         communicator.join()
227         //verify
228         verify { mockOutputStream.write(any(), any(), any()) }
229         verify { mockOutputStream.flush() }
230         assertFalse { completableFuture.isCompletedExceptionally }
231     }
232
233     @Test
234     fun `sendMessage on IOError returns completed exceptionally future`() {
235         val msgPayload = "some text"
236         val msgId = "100"
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...
239         //Run the command
240         val communicator = NetconfDeviceCommunicator(
241             stubInputStream, mockOutputStream,
242             genDeviceInfo(), netconfSessionListener, replies)
243         val completableFuture = communicator.sendMessage(msgPayload, msgId)
244         //verify
245         verify { mockOutputStream.write(any(), any(), any()) }
246         verify(exactly = 0) { mockOutputStream.flush() }
247         assertTrue { completableFuture.isCompletedExceptionally }
248     }
249
250     private fun genDeviceInfo(): DeviceInfo {
251         return DeviceInfo().apply {
252             username = "user"
253             password = "pass"
254             ipAddress = "localhost"
255             port = 4567
256         }
257     }
258
259 }