cb023fd9a7dd3e0385543725b17d148e97adcaa3
[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.Ignore
28 import org.junit.Test
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
38 import java.io.Reader
39 import java.io.Writer
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
47
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)
56
57
58     companion object {
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 = """
62             |
63             |#4
64             |<rpc
65             |
66             |#18
67             | message-id="102"
68             |
69             |#79
70             |  xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
71             | <close-session/>
72             |</rpc>
73             |##
74             |""".trimMargin()
75     }
76
77     private fun stringToCharArray(str: String): List<Int> {
78         return str.toCharArray().map(Char::toInt)
79     }
80
81     @Before
82     fun setup() {
83         netconfSession = mockk()
84         netconfSessionListener = mockk()
85         mockInputStream = mockk()
86         mockOutputStream = mockk()
87         replies = ConcurrentHashMap()
88     }
89
90     @Test
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)
96         communicator.join()
97         //verify
98         verify { mockInputStream.read(any(), any(), any()) }
99     }
100
101     @Test
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)
111         //RUN the test
112         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
113             genDeviceInfo(), netconfSessionListener, replies)
114         communicator.join()
115         //Verify
116         verify { inputStreamSpy.close() }
117         assertTrue { eventSlot.isCaptured }
118         assertEquals(NetconfReceivedEvent.Type.DEVICE_UNREGISTERED, eventSlot.captured.type)
119         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
120     }
121
122     @Test
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")
129         //RUN THE TEST
130         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream,
131             genDeviceInfo(), netconfSessionListener, replies)
132         communicator.join()
133         //Verify
134         assertTrue { eventSlot.isCaptured }
135         assertEquals(genDeviceInfo(), eventSlot.captured.deviceInfo)
136         assertEquals(NetconfReceivedEvent.Type.DEVICE_ERROR, eventSlot.captured.type)
137     }
138
139     @Test
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
145         //RUN the test
146         val communicator = NetconfDeviceCommunicator(stubInputStream, mockOutputStream,
147             genDeviceInfo(), netconfSessionListener, replies)
148         communicator.join()
149         //Verify
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)
155     }
156
157     @Test
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
163
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)
167         //RUN the test
168         val communicator = NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(),
169             netconfSessionListener, replies)
170         communicator.join()
171         //Verify
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)
177     }
178
179     @Ignore //TODO: Not clear on validateChunkedFraming, the size validation part could be discarding valid msg..
180     @Test
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
186         //RUN the test
187         NetconfDeviceCommunicator(inputStreamSpy, mockOutputStream, genDeviceInfo(), netconfSessionListener, replies).join()
188         //Verify
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)
194     }
195
196     @Test
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() }
204     }
205
206     @Test
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() }
212     }
213
214     @Test
215     fun `sendMessage writes the request to NetconfDeviceCommunicator Writer`() {
216         val msgPayload = "some text"
217         val msgId = "100"
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
222         //Run the command
223         val communicator = NetconfDeviceCommunicator(
224             stubInputStream, mockOutputStream,
225             genDeviceInfo(), netconfSessionListener, replies)
226         val completableFuture = communicator.sendMessage(msgPayload, msgId)
227         communicator.join()
228         //verify
229         verify { mockOutputStream.write(any(), any(), any()) }
230         verify { mockOutputStream.flush() }
231         assertFalse { completableFuture.isCompletedExceptionally }
232     }
233
234     @Test
235     fun `sendMessage on IOError returns completed exceptionally future`() {
236         val msgPayload = "some text"
237         val msgId = "100"
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...
240         //Run the command
241         val communicator = NetconfDeviceCommunicator(
242             stubInputStream, mockOutputStream,
243             genDeviceInfo(), netconfSessionListener, replies)
244         val completableFuture = communicator.sendMessage(msgPayload, msgId)
245         //verify
246         verify { mockOutputStream.write(any(), any(), any()) }
247         verify(exactly = 0) { mockOutputStream.flush() }
248         assertTrue { completableFuture.isCompletedExceptionally }
249     }
250
251     private fun genDeviceInfo(): DeviceInfo {
252         return DeviceInfo().apply {
253             username = "user"
254             password = "pass"
255             ipAddress = "localhost"
256             port = 4567
257         }
258     }
259
260 }