1f526f4451bfa7d1211551568f4af8b424f3ee2b
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / netconf-executor / src / test / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / functions / netconf / executor / core / NetconfSessionImplTest.kt
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.apache.sshd.client.SshClient
27 import org.apache.sshd.client.channel.ChannelSubsystem
28 import org.apache.sshd.client.channel.ClientChannel
29 import org.apache.sshd.client.future.DefaultAuthFuture
30 import org.apache.sshd.client.future.DefaultConnectFuture
31 import org.apache.sshd.client.future.DefaultOpenFuture
32 import org.apache.sshd.client.session.ClientSession
33 import org.apache.sshd.common.FactoryManager
34 import org.junit.Before
35 import org.junit.Ignore
36 import org.junit.Test
37 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceInfo
38 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceResponse
39 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfException
40 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfRpcService
41 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.NetconfMessageUtils
42 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.RpcStatus
43 import java.io.ByteArrayInputStream
44 import java.io.ByteArrayOutputStream
45 import java.io.IOException
46 import java.io.InputStream
47 import java.nio.charset.StandardCharsets
48 import java.util.concurrent.CompletableFuture
49 import java.util.concurrent.ExecutionException
50 import java.util.concurrent.TimeoutException
51 import kotlin.test.assertEquals
52 import kotlin.test.assertFailsWith
53 import kotlin.test.assertTrue
54
55 class NetconfSessionImplTest {
56     companion object {
57         val SUCCESSFUL_DEVICE_RESPONSE = DeviceResponse().apply {
58             status = RpcStatus.SUCCESS
59             errorMessage = ""
60             responseMessage = ""
61             requestMessage = ""
62         }
63         val FAILED_DEVICE_RESPONSE = DeviceResponse().apply {
64             status = RpcStatus.FAILURE
65             errorMessage = ""
66             responseMessage = ""
67             requestMessage = ""
68         }
69         val deviceInfo: DeviceInfo = DeviceInfo().apply {
70             username = "username"
71             password = "password"
72             ipAddress = "localhost"
73             port = 2224
74             connectTimeout = 10
75         }
76         private const val someString = "Some string"
77     }
78
79     private lateinit var netconfSession: NetconfSessionImpl
80     private lateinit var netconfCommunicator: NetconfDeviceCommunicator
81     private lateinit var rpcService: NetconfRpcService
82     private lateinit var mockSshClient: SshClient
83     private lateinit var mockClientSession: ClientSession
84     private lateinit var mockClientChannel: ClientChannel
85     private lateinit var mockSubsystem: ChannelSubsystem
86
87     private val futureMsg = "blahblahblah"
88     private val request = "0"
89     private val sessionId = "0"
90     private val messageId = "asdfasdfadf"
91     private val deviceCapabilities = setOf("capability1", "capability2")
92     private val formattedRequest = NetconfMessageUtils.formatRPCRequest(request, messageId, deviceCapabilities)
93     private lateinit var sampleInputStream: InputStream
94     private lateinit var sampleOutputStream: ByteArrayOutputStream
95
96     @Before
97     fun setup() {
98         netconfCommunicator = mockk()
99         rpcService = mockk()
100         netconfSession = NetconfSessionImpl(deviceInfo, rpcService)
101         netconfSession.setStreamHandler(netconfCommunicator)
102         mockSshClient = mockk()
103         mockClientSession = mockk()
104         mockClientChannel = mockk()
105         mockSubsystem = mockk()
106         sampleInputStream = ByteArrayInputStream(someString.toByteArray(StandardCharsets.UTF_8))
107         sampleOutputStream = ByteArrayOutputStream()
108     }
109
110     @Test
111     fun `connect calls appropriate methods`() {
112         val session = spyk(netconfSession, recordPrivateCalls = true)
113         every { session["startClient"]() as Unit } just Runs
114         session.connect()
115         verify { session["startClient"]() }
116     }
117
118     //look for NetconfException being thrown when cannot connect
119     @Test
120     fun `connect throws NetconfException on error`() {
121         val errMsg = "$deviceInfo: Failed to establish SSH session"
122         assertFailsWith(exceptionClass = NetconfException::class, message = errMsg) {
123             val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
124             every { netconfSessionSpy["startClient"]() as Unit } throws NetconfException(errMsg)
125             netconfSessionSpy.connect()
126         }
127     }
128
129     @Test
130     fun `disconnect without force option for rpcService succeeds`() {
131         //rpcService.closeSession succeeds with status not RpcStatus.FAILURE
132         every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
133         every { mockClientSession.close() } just Runs
134         every { mockSshClient.close() } just Runs
135         every { mockClientChannel.close() } just Runs
136         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
137         netconfSessionSpy.setSession(mockClientSession)
138         netconfSessionSpy.setClient(mockSshClient)
139         netconfSessionSpy.setChannel(mockClientChannel)
140         //RUN
141         netconfSessionSpy.disconnect()
142         //make sure that rpcService.close session is not called again.
143         verify(exactly = 0) { rpcService.closeSession(true) }
144         verify { mockClientSession.close() }
145         verify { mockSshClient.close() }
146         verify { mockClientChannel.close() }
147     }
148
149     @Test
150     fun `disconnect with force option for rpcService succeeds`() {
151         //rpcService.closeSession succeeds with status not RpcStatus.FAILURE
152         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
153         every { rpcService.closeSession(any()) } returns
154             FAILED_DEVICE_RESPONSE andThen SUCCESSFUL_DEVICE_RESPONSE
155         every { mockClientSession.close() } just Runs
156         every { mockSshClient.close() } just Runs
157         every { mockClientChannel.close() } just Runs
158         netconfSessionSpy.setSession(mockClientSession)
159         netconfSessionSpy.setClient(mockSshClient)
160         netconfSessionSpy.setChannel(mockClientChannel)
161         //RUN
162         netconfSessionSpy.disconnect()
163         //VERIFY
164         verify(exactly = 2) { rpcService.closeSession(any()) }
165         verify { mockClientSession.close() }
166         verify { mockSshClient.close() }
167         verify { mockClientChannel.close() }
168
169     }
170
171     @Ignore //TODO undo close method removal
172     @Test
173     fun `disconnect wraps exception from ssh closing error`() {
174         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
175         every { netconfSessionSpy["close"]() as Unit } throws IOException("Some IOException occurred!")
176         every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
177         every { netconfSessionSpy.checkAndReestablish() } just Runs
178         netconfSessionSpy.disconnect()
179         verify { netconfSessionSpy["close"]() }
180     }
181
182     @Test
183     fun `reconnect calls disconnect and connect`() {
184         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
185         every { netconfSessionSpy.disconnect() } just Runs
186         every { netconfSessionSpy.connect() } just Runs
187         netconfSessionSpy.reconnect()
188         verify { netconfSessionSpy.disconnect() }
189         verify { netconfSessionSpy.connect() }
190     }
191
192     @Test
193     fun `checkAndReestablish restarts connection and clears replies on sshClient disconnection`() {
194         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
195         every { mockSshClient.isClosed } returns true
196         netconfSessionSpy.setClient(mockSshClient)
197         every { netconfSessionSpy["startConnection"]() as Unit } just Runs
198         //Call method
199         netconfSessionSpy.checkAndReestablish()
200         //Verify
201         verify { netconfSessionSpy.clearReplies() }
202         verify { netconfSessionSpy["startConnection"]() }
203     }
204
205     @Test
206     fun `checkAndReestablish restarts session and clears replies on clientSession closing`() {
207         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
208         every { mockClientSession.isClosed } returns true
209         every { mockSshClient.isClosed } returns false
210         every { netconfSessionSpy["startSession"]() as Unit } just Runs
211         netconfSessionSpy.setClient(mockSshClient)
212         netconfSessionSpy.setSession(mockClientSession)
213         //Call method
214         netconfSessionSpy.checkAndReestablish()
215         //Verify
216         verify { netconfSessionSpy.clearReplies() }
217         verify { netconfSessionSpy["startSession"]() }
218     }
219
220     @Test
221     fun `checkAndReestablish reopens channel and clears replies on channel closing`() {
222         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
223         every { mockClientSession.isClosed } returns false
224         every { mockSshClient.isClosed } returns false
225         every { mockClientChannel.isClosed } returns true
226         every { netconfSessionSpy["openChannel"]() as Unit } just Runs
227         netconfSessionSpy.setClient(mockSshClient)
228         netconfSessionSpy.setSession(mockClientSession)
229         netconfSessionSpy.setChannel(mockClientChannel)
230         //Call method
231         netconfSessionSpy.checkAndReestablish()
232         //Verify
233         verify { netconfSessionSpy.clearReplies() }
234         verify { netconfSessionSpy["openChannel"]() }
235     }
236
237
238     @Test
239     fun `syncRpc runs normally`() {
240         val netconfSessionSpy = spyk(netconfSession)
241         val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
242
243         //test the case where SSH connection did not need to be re-established.
244         //put an existing item into the replies
245         netconfSessionSpy.getReplies()["somekey"] = CompletableFuture.completedFuture("${futureMsg}2")
246         every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
247         every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } returns futureRet.get()
248         every { netconfSessionSpy.checkAndReestablish() } just Runs
249         //call the method
250         assertEquals(futureMsg, netconfSessionSpy.syncRpc("0", "0"))
251         //make sure the replies didn't change
252         assertTrue {
253             netconfSessionSpy.getReplies().size == 1 &&
254                 netconfSessionSpy.getReplies().containsKey("somekey")
255         }
256         verify(exactly = 0) { netconfSessionSpy.clearReplies() }
257     }
258
259
260     @Test
261     fun `syncRpc still succeeds and replies are cleared on client disconnect`() {
262         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
263         val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
264
265         //put an item into the replies
266         netconfSessionSpy.getReplies()["somekey"] = CompletableFuture.completedFuture("${futureMsg}2")
267
268         //tests the case where SSH session needs to be re-established.
269         every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
270         every { netconfSessionSpy["startClient"]() as Unit } just Runs
271         every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } returns futureRet.get()
272         every { mockSshClient.isClosed } returns true
273         netconfSessionSpy.setClient(mockSshClient)
274
275         //call the method
276         assertEquals(futureMsg, netconfSessionSpy.syncRpc("0", "0"))
277         //make sure the replies got cleared out
278         assertTrue { netconfSessionSpy.getReplies().isEmpty() }
279         verify(exactly = 1) { netconfSessionSpy.clearReplies() }
280     }
281
282     @Ignore //TODO
283     //Test for handling CompletableFuture.get returns InterruptedException inside NetconfDeviceCommunicator
284     @Test
285     fun `syncRpc throws NetconfException if InterruptedException is caught`() {
286         val expectedExceptionMsg = "$deviceInfo: Interrupted while waiting for reply for request: $formattedRequest"
287         assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
288             val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
289             val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
290             every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
291             every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws InterruptedException("interrupted")
292             every { netconfSessionSpy.checkAndReestablish() } just Runs
293             //call the method
294             netconfSessionSpy.syncRpc("0", "0")
295         }
296     }
297
298     @Ignore //TODO revert back on getFutureFromSendMessage
299     @Test
300     fun `syncRpc throws NetconfException if TimeoutException is caught`() {
301         val expectedExceptionMsg = "$deviceInfo: Timed out while waiting for reply for request $formattedRequest after ${deviceInfo.replyTimeout} sec."
302         assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
303             val netconfSessionSpy = spyk(netconfSession)
304             val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
305             every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
306             every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws TimeoutException("timed out")
307             every { netconfSessionSpy.checkAndReestablish() } just Runs
308             //call the method
309             netconfSessionSpy.syncRpc("0", "0")
310         }
311     }
312
313     @Ignore
314     @Test
315     fun `syncRpc throws NetconfException if ExecutionException is caught`() {
316         val expectedExceptionMsg = "$deviceInfo: Closing session $sessionId for request $formattedRequest"
317         assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
318             val netconfSessionSpy = spyk(netconfSession)
319             val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
320             every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
321             every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws
322                 ExecutionException("exec exception", Exception("nested exception")) //TODO revert getFutureFromSendMessage back
323             every { netconfSessionSpy.checkAndReestablish() } just Runs
324             //call the method
325             netconfSessionSpy.syncRpc("0", "0")
326         }
327     }
328
329     @Ignore //TODO revert back on getFutureFromSendMessage
330     @Test
331     fun `syncRpc throws NetconfException if caught ExecutionException and failed to close SSH session`() {
332         val expectedExceptionMsg = "$deviceInfo: Closing session $sessionId for request $formattedRequest"
333         assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
334             val netconfSessionSpy = spyk(netconfSession)
335             val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
336             every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
337             every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws
338                 ExecutionException("exec exception", Exception("nested exception"))
339             every { netconfSessionSpy["close"]() as Unit } throws IOException("got an IO exception")
340             every { netconfSessionSpy.checkAndReestablish() } just Runs
341             //call the method
342             netconfSessionSpy.syncRpc("0", "0")
343             //make sure replies are cleared...
344             verify(exactly = 1) { netconfSessionSpy.clearReplies() }
345             verify(exactly = 1) { netconfSessionSpy.clearErrorReplies() }
346         }
347     }
348
349     @Test
350     fun `asyncRpc runs normally`() {
351         val netconfSessionSpy = spyk(netconfSession)
352         every { netconfSessionSpy.checkAndReestablish() } just Runs
353         val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
354         every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
355         //run the method
356         val rpcResultFuture = netconfSessionSpy.asyncRpc("0", "0")
357         every { netconfSessionSpy.checkAndReestablish() } just Runs
358         //make sure the future gets resolved
359         assertTrue { rpcResultFuture.get() == futureMsg }
360         //make sure that clearReplies wasn't called (reestablishConnection check)
361         verify(exactly = 0) { netconfSessionSpy.clearReplies() }
362     }
363
364     @Test
365     @Ignore
366     //TODO: get 't' inside asyncRpc to be a Throwable
367     fun `asyncRpc wraps exception`() {
368         assertFailsWith(exceptionClass = NetconfException::class, message = futureMsg) {
369             val netconfSessionSpy = spyk(netconfSession)
370             val futureRet: CompletableFuture<String> = CompletableFuture.supplyAsync {
371                 throw Exception("blah")
372             }
373             futureRet.completeExceptionally(IOException("something is wrong"))
374             every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
375             //RUN
376             val rpcResultFuture = netconfSessionSpy.asyncRpc("0", "0")
377         }
378     }
379
380     @Test
381     fun `connect starts underlying client`() {
382         val propertiesMap = hashMapOf<String, Any>()
383         every { mockSshClient.start() } just Runs
384         every { mockSshClient.properties } returns propertiesMap
385         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
386         every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
387         every { netconfSessionSpy["startSession"]() as Unit } just Runs
388         netconfSessionSpy.setClient(mockSshClient)
389         netconfSessionSpy.connect()
390         verify { mockSshClient.start() }
391         assertTrue { propertiesMap.containsKey(FactoryManager.IDLE_TIMEOUT) }
392         assertTrue { propertiesMap.containsKey(FactoryManager.NIO2_READ_TIMEOUT) }
393     }
394
395     @Test
396     fun `startSession tries to connect to user supplied device`() {
397         every { mockSshClient.start() } just Runs
398         every { mockSshClient.properties } returns hashMapOf<String, Any>()
399         //setup slots to capture values from the invocations
400         val userSlot = CapturingSlot<String>()
401         val ipSlot = CapturingSlot<String>()
402         val portSlot = CapturingSlot<Int>()
403         //create a future that succeeded
404         val succeededFuture = DefaultConnectFuture(Any(), Any())
405         succeededFuture.value = mockClientSession
406         every { mockSshClient.connect(capture(userSlot), capture(ipSlot), capture(portSlot)) } returns succeededFuture
407         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
408         every { netconfSessionSpy["authSession"]() as Unit } just Runs
409         every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
410         netconfSessionSpy.setClient(mockSshClient)
411         //RUN
412         netconfSessionSpy.connect()
413         //Verify
414         verify { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) }
415         assertEquals(deviceInfo.username, userSlot.captured)
416         assertEquals(deviceInfo.ipAddress, ipSlot.captured)
417         assertEquals(deviceInfo.port, portSlot.captured)
418         verify { netconfSessionSpy["authSession"]() }
419     }
420
421     @Test
422     fun `authSession throws exception if ClientSession is not AUTHED`() {
423         assertFailsWith(exceptionClass = NetconfException::class) {
424             //after client session connects,
425             every { mockSshClient.start() } just Runs
426             every { mockSshClient.properties } returns hashMapOf<String, Any>()
427             val succeededAuthFuture = DefaultAuthFuture(Any(), Any())
428             succeededAuthFuture.value = true //AuthFuture's value is Boolean
429             val passSlot = CapturingSlot<String>()
430             every { mockClientSession.addPasswordIdentity(capture(passSlot)) } just Runs
431             every { mockClientSession.auth() } returns succeededAuthFuture
432             val succeededSessionFuture = DefaultConnectFuture(Any(), Any())
433             succeededSessionFuture.value = mockClientSession
434             every { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) } returns succeededSessionFuture
435             every { mockClientSession.waitFor(any(), any()) } returns
436                 setOf(ClientSession.ClientSessionEvent.WAIT_AUTH, ClientSession.ClientSessionEvent.CLOSED)
437             val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
438             every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
439             netconfSessionSpy.setClient(mockSshClient)
440             //RUN
441             netconfSessionSpy.connect()
442         }
443     }
444
445     //common mock initializer for more weird tests.
446     private fun setupOpenChannelMocks(): Unit {
447         every { mockSshClient.start() } just Runs
448         every { mockSshClient.properties } returns hashMapOf<String, Any>()
449         val succeededAuthFuture = DefaultAuthFuture(Any(), Any())
450         succeededAuthFuture.value = true //AuthFuture's value is Boolean
451         val passSlot = CapturingSlot<String>()
452         every { mockClientSession.addPasswordIdentity(capture(passSlot)) } just Runs
453         every { mockClientSession.auth() } returns succeededAuthFuture
454         val succeededSessionFuture = DefaultConnectFuture(Any(), Any())
455         succeededSessionFuture.value = mockClientSession
456         every { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) } returns succeededSessionFuture
457         every { mockClientSession.waitFor(any(), any()) } returns
458             setOf(ClientSession.ClientSessionEvent.WAIT_AUTH,
459                 ClientSession.ClientSessionEvent.CLOSED,
460                 ClientSession.ClientSessionEvent.AUTHED)
461
462         every { mockClientSession.createSubsystemChannel(any()) } returns mockSubsystem
463         every { mockClientChannel.invertedOut } returns sampleInputStream
464         every { mockClientChannel.invertedIn } returns sampleOutputStream
465     }
466
467     @Test
468     fun `authSession opensChannel if ClientSession is AUTHED and session can be opened`() {
469         //after client session connects, make sure the client receives authentication
470         setupOpenChannelMocks()
471         val channelFuture = DefaultOpenFuture(Any(), Any())
472         channelFuture.value = true
473         channelFuture.setOpened()
474         val connectFuture = DefaultConnectFuture(Any(), Any())
475         connectFuture.value = mockClientSession
476         connectFuture.session = mockClientSession
477         every { mockSubsystem.open() } returns channelFuture
478         every { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) } returns connectFuture
479
480         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
481         every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
482         every { netconfSessionSpy["setupHandler"]() as Unit } just Runs
483         netconfSessionSpy.setClient(mockSshClient)
484         //Run
485         netconfSessionSpy.connect()
486         //Verify
487         verify { mockSubsystem.open() }
488     }
489
490
491     @Test
492     fun `authSession throws NetconfException if ClientSession is AUTHED but channelFuture timed out or not open`() {
493         assertFailsWith(exceptionClass = NetconfException::class) {
494             //after client session connects, make sure the client receives authentication
495             setupOpenChannelMocks()
496             val channelFuture = DefaultOpenFuture(Any(), Any())
497             every { mockSubsystem.open() } returns channelFuture
498             val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
499             every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
500             every { netconfSessionSpy["setupHandler"]() as Unit } just Runs
501             netconfSessionSpy.setClient(mockSshClient)
502             //Run
503             netconfSessionSpy.connect()
504             //Verify
505             verify { mockSubsystem.open() }
506         }
507     }
508
509
510     @Test
511     fun `disconnect closes session, channel, and client`() {
512         every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
513         every { mockClientSession.close() } just Runs
514         every { mockClientChannel.close() } just Runs
515         every { mockSshClient.close() } just Runs
516         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
517         netconfSessionSpy.setChannel(mockClientChannel)
518         netconfSessionSpy.setClient(mockSshClient)
519         netconfSessionSpy.setSession(mockClientSession)
520         //RUN
521         netconfSessionSpy.disconnect()
522         //VERIFY
523         verify { mockClientSession.close() }
524         verify { mockClientChannel.close() }
525         verify { mockSshClient.close() }
526     }
527
528     @Ignore
529     @Test
530     fun `disconnect wraps IOException if channel doesn't close`() { //this test is equivalent to others
531         every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
532         every { mockClientSession.close() } just Runs
533         every { mockClientChannel.close() } throws IOException("channel doesn't want to close!")
534         val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
535         netconfSessionSpy.setChannel(mockClientChannel)
536         netconfSessionSpy.setClient(mockSshClient)
537         netconfSessionSpy.setSession(mockClientSession)
538         //RUN
539         netconfSessionSpy.disconnect()
540         //VERIFY
541         verify { mockClientSession.close() }
542     }
543 }