netconf-executor: NetconfSessionImplTest
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / netconf-executor / src / test / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / functions / netconf / executor / core / NetconfSessionImplTest.kt
diff --git a/ms/blueprintsprocessor/functions/netconf-executor/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/netconf/executor/core/NetconfSessionImplTest.kt b/ms/blueprintsprocessor/functions/netconf-executor/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/netconf/executor/core/NetconfSessionImplTest.kt
new file mode 100644 (file)
index 0000000..1f526f4
--- /dev/null
@@ -0,0 +1,543 @@
+/*
+ * Copyright © 2019 Bell Canada
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.core
+
+import io.mockk.CapturingSlot
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import org.apache.sshd.client.SshClient
+import org.apache.sshd.client.channel.ChannelSubsystem
+import org.apache.sshd.client.channel.ClientChannel
+import org.apache.sshd.client.future.DefaultAuthFuture
+import org.apache.sshd.client.future.DefaultConnectFuture
+import org.apache.sshd.client.future.DefaultOpenFuture
+import org.apache.sshd.client.session.ClientSession
+import org.apache.sshd.common.FactoryManager
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceInfo
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.DeviceResponse
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfException
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfRpcService
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.NetconfMessageUtils
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils.RpcStatus
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeoutException
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+class NetconfSessionImplTest {
+    companion object {
+        val SUCCESSFUL_DEVICE_RESPONSE = DeviceResponse().apply {
+            status = RpcStatus.SUCCESS
+            errorMessage = ""
+            responseMessage = ""
+            requestMessage = ""
+        }
+        val FAILED_DEVICE_RESPONSE = DeviceResponse().apply {
+            status = RpcStatus.FAILURE
+            errorMessage = ""
+            responseMessage = ""
+            requestMessage = ""
+        }
+        val deviceInfo: DeviceInfo = DeviceInfo().apply {
+            username = "username"
+            password = "password"
+            ipAddress = "localhost"
+            port = 2224
+            connectTimeout = 10
+        }
+        private const val someString = "Some string"
+    }
+
+    private lateinit var netconfSession: NetconfSessionImpl
+    private lateinit var netconfCommunicator: NetconfDeviceCommunicator
+    private lateinit var rpcService: NetconfRpcService
+    private lateinit var mockSshClient: SshClient
+    private lateinit var mockClientSession: ClientSession
+    private lateinit var mockClientChannel: ClientChannel
+    private lateinit var mockSubsystem: ChannelSubsystem
+
+    private val futureMsg = "blahblahblah"
+    private val request = "0"
+    private val sessionId = "0"
+    private val messageId = "asdfasdfadf"
+    private val deviceCapabilities = setOf("capability1", "capability2")
+    private val formattedRequest = NetconfMessageUtils.formatRPCRequest(request, messageId, deviceCapabilities)
+    private lateinit var sampleInputStream: InputStream
+    private lateinit var sampleOutputStream: ByteArrayOutputStream
+
+    @Before
+    fun setup() {
+        netconfCommunicator = mockk()
+        rpcService = mockk()
+        netconfSession = NetconfSessionImpl(deviceInfo, rpcService)
+        netconfSession.setStreamHandler(netconfCommunicator)
+        mockSshClient = mockk()
+        mockClientSession = mockk()
+        mockClientChannel = mockk()
+        mockSubsystem = mockk()
+        sampleInputStream = ByteArrayInputStream(someString.toByteArray(StandardCharsets.UTF_8))
+        sampleOutputStream = ByteArrayOutputStream()
+    }
+
+    @Test
+    fun `connect calls appropriate methods`() {
+        val session = spyk(netconfSession, recordPrivateCalls = true)
+        every { session["startClient"]() as Unit } just Runs
+        session.connect()
+        verify { session["startClient"]() }
+    }
+
+    //look for NetconfException being thrown when cannot connect
+    @Test
+    fun `connect throws NetconfException on error`() {
+        val errMsg = "$deviceInfo: Failed to establish SSH session"
+        assertFailsWith(exceptionClass = NetconfException::class, message = errMsg) {
+            val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+            every { netconfSessionSpy["startClient"]() as Unit } throws NetconfException(errMsg)
+            netconfSessionSpy.connect()
+        }
+    }
+
+    @Test
+    fun `disconnect without force option for rpcService succeeds`() {
+        //rpcService.closeSession succeeds with status not RpcStatus.FAILURE
+        every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
+        every { mockClientSession.close() } just Runs
+        every { mockSshClient.close() } just Runs
+        every { mockClientChannel.close() } just Runs
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        netconfSessionSpy.setSession(mockClientSession)
+        netconfSessionSpy.setClient(mockSshClient)
+        netconfSessionSpy.setChannel(mockClientChannel)
+        //RUN
+        netconfSessionSpy.disconnect()
+        //make sure that rpcService.close session is not called again.
+        verify(exactly = 0) { rpcService.closeSession(true) }
+        verify { mockClientSession.close() }
+        verify { mockSshClient.close() }
+        verify { mockClientChannel.close() }
+    }
+
+    @Test
+    fun `disconnect with force option for rpcService succeeds`() {
+        //rpcService.closeSession succeeds with status not RpcStatus.FAILURE
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { rpcService.closeSession(any()) } returns
+            FAILED_DEVICE_RESPONSE andThen SUCCESSFUL_DEVICE_RESPONSE
+        every { mockClientSession.close() } just Runs
+        every { mockSshClient.close() } just Runs
+        every { mockClientChannel.close() } just Runs
+        netconfSessionSpy.setSession(mockClientSession)
+        netconfSessionSpy.setClient(mockSshClient)
+        netconfSessionSpy.setChannel(mockClientChannel)
+        //RUN
+        netconfSessionSpy.disconnect()
+        //VERIFY
+        verify(exactly = 2) { rpcService.closeSession(any()) }
+        verify { mockClientSession.close() }
+        verify { mockSshClient.close() }
+        verify { mockClientChannel.close() }
+
+    }
+
+    @Ignore //TODO undo close method removal
+    @Test
+    fun `disconnect wraps exception from ssh closing error`() {
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { netconfSessionSpy["close"]() as Unit } throws IOException("Some IOException occurred!")
+        every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
+        every { netconfSessionSpy.checkAndReestablish() } just Runs
+        netconfSessionSpy.disconnect()
+        verify { netconfSessionSpy["close"]() }
+    }
+
+    @Test
+    fun `reconnect calls disconnect and connect`() {
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { netconfSessionSpy.disconnect() } just Runs
+        every { netconfSessionSpy.connect() } just Runs
+        netconfSessionSpy.reconnect()
+        verify { netconfSessionSpy.disconnect() }
+        verify { netconfSessionSpy.connect() }
+    }
+
+    @Test
+    fun `checkAndReestablish restarts connection and clears replies on sshClient disconnection`() {
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { mockSshClient.isClosed } returns true
+        netconfSessionSpy.setClient(mockSshClient)
+        every { netconfSessionSpy["startConnection"]() as Unit } just Runs
+        //Call method
+        netconfSessionSpy.checkAndReestablish()
+        //Verify
+        verify { netconfSessionSpy.clearReplies() }
+        verify { netconfSessionSpy["startConnection"]() }
+    }
+
+    @Test
+    fun `checkAndReestablish restarts session and clears replies on clientSession closing`() {
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { mockClientSession.isClosed } returns true
+        every { mockSshClient.isClosed } returns false
+        every { netconfSessionSpy["startSession"]() as Unit } just Runs
+        netconfSessionSpy.setClient(mockSshClient)
+        netconfSessionSpy.setSession(mockClientSession)
+        //Call method
+        netconfSessionSpy.checkAndReestablish()
+        //Verify
+        verify { netconfSessionSpy.clearReplies() }
+        verify { netconfSessionSpy["startSession"]() }
+    }
+
+    @Test
+    fun `checkAndReestablish reopens channel and clears replies on channel closing`() {
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { mockClientSession.isClosed } returns false
+        every { mockSshClient.isClosed } returns false
+        every { mockClientChannel.isClosed } returns true
+        every { netconfSessionSpy["openChannel"]() as Unit } just Runs
+        netconfSessionSpy.setClient(mockSshClient)
+        netconfSessionSpy.setSession(mockClientSession)
+        netconfSessionSpy.setChannel(mockClientChannel)
+        //Call method
+        netconfSessionSpy.checkAndReestablish()
+        //Verify
+        verify { netconfSessionSpy.clearReplies() }
+        verify { netconfSessionSpy["openChannel"]() }
+    }
+
+
+    @Test
+    fun `syncRpc runs normally`() {
+        val netconfSessionSpy = spyk(netconfSession)
+        val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
+
+        //test the case where SSH connection did not need to be re-established.
+        //put an existing item into the replies
+        netconfSessionSpy.getReplies()["somekey"] = CompletableFuture.completedFuture("${futureMsg}2")
+        every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+        every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } returns futureRet.get()
+        every { netconfSessionSpy.checkAndReestablish() } just Runs
+        //call the method
+        assertEquals(futureMsg, netconfSessionSpy.syncRpc("0", "0"))
+        //make sure the replies didn't change
+        assertTrue {
+            netconfSessionSpy.getReplies().size == 1 &&
+                netconfSessionSpy.getReplies().containsKey("somekey")
+        }
+        verify(exactly = 0) { netconfSessionSpy.clearReplies() }
+    }
+
+
+    @Test
+    fun `syncRpc still succeeds and replies are cleared on client disconnect`() {
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
+
+        //put an item into the replies
+        netconfSessionSpy.getReplies()["somekey"] = CompletableFuture.completedFuture("${futureMsg}2")
+
+        //tests the case where SSH session needs to be re-established.
+        every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+        every { netconfSessionSpy["startClient"]() as Unit } just Runs
+        every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } returns futureRet.get()
+        every { mockSshClient.isClosed } returns true
+        netconfSessionSpy.setClient(mockSshClient)
+
+        //call the method
+        assertEquals(futureMsg, netconfSessionSpy.syncRpc("0", "0"))
+        //make sure the replies got cleared out
+        assertTrue { netconfSessionSpy.getReplies().isEmpty() }
+        verify(exactly = 1) { netconfSessionSpy.clearReplies() }
+    }
+
+    @Ignore //TODO
+    //Test for handling CompletableFuture.get returns InterruptedException inside NetconfDeviceCommunicator
+    @Test
+    fun `syncRpc throws NetconfException if InterruptedException is caught`() {
+        val expectedExceptionMsg = "$deviceInfo: Interrupted while waiting for reply for request: $formattedRequest"
+        assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
+            val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+            val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
+            every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+            every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws InterruptedException("interrupted")
+            every { netconfSessionSpy.checkAndReestablish() } just Runs
+            //call the method
+            netconfSessionSpy.syncRpc("0", "0")
+        }
+    }
+
+    @Ignore //TODO revert back on getFutureFromSendMessage
+    @Test
+    fun `syncRpc throws NetconfException if TimeoutException is caught`() {
+        val expectedExceptionMsg = "$deviceInfo: Timed out while waiting for reply for request $formattedRequest after ${deviceInfo.replyTimeout} sec."
+        assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
+            val netconfSessionSpy = spyk(netconfSession)
+            val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
+            every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+            every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws TimeoutException("timed out")
+            every { netconfSessionSpy.checkAndReestablish() } just Runs
+            //call the method
+            netconfSessionSpy.syncRpc("0", "0")
+        }
+    }
+
+    @Ignore
+    @Test
+    fun `syncRpc throws NetconfException if ExecutionException is caught`() {
+        val expectedExceptionMsg = "$deviceInfo: Closing session $sessionId for request $formattedRequest"
+        assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
+            val netconfSessionSpy = spyk(netconfSession)
+            val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
+            every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+            every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws
+                ExecutionException("exec exception", Exception("nested exception")) //TODO revert getFutureFromSendMessage back
+            every { netconfSessionSpy.checkAndReestablish() } just Runs
+            //call the method
+            netconfSessionSpy.syncRpc("0", "0")
+        }
+    }
+
+    @Ignore //TODO revert back on getFutureFromSendMessage
+    @Test
+    fun `syncRpc throws NetconfException if caught ExecutionException and failed to close SSH session`() {
+        val expectedExceptionMsg = "$deviceInfo: Closing session $sessionId for request $formattedRequest"
+        assertFailsWith(exceptionClass = NetconfException::class, message = expectedExceptionMsg) {
+            val netconfSessionSpy = spyk(netconfSession)
+            val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
+            every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+            every { netconfCommunicator.getFutureFromSendMessage(any(), any(), any()) } throws
+                ExecutionException("exec exception", Exception("nested exception"))
+            every { netconfSessionSpy["close"]() as Unit } throws IOException("got an IO exception")
+            every { netconfSessionSpy.checkAndReestablish() } just Runs
+            //call the method
+            netconfSessionSpy.syncRpc("0", "0")
+            //make sure replies are cleared...
+            verify(exactly = 1) { netconfSessionSpy.clearReplies() }
+            verify(exactly = 1) { netconfSessionSpy.clearErrorReplies() }
+        }
+    }
+
+    @Test
+    fun `asyncRpc runs normally`() {
+        val netconfSessionSpy = spyk(netconfSession)
+        every { netconfSessionSpy.checkAndReestablish() } just Runs
+        val futureRet: CompletableFuture<String> = CompletableFuture.completedFuture(futureMsg)
+        every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+        //run the method
+        val rpcResultFuture = netconfSessionSpy.asyncRpc("0", "0")
+        every { netconfSessionSpy.checkAndReestablish() } just Runs
+        //make sure the future gets resolved
+        assertTrue { rpcResultFuture.get() == futureMsg }
+        //make sure that clearReplies wasn't called (reestablishConnection check)
+        verify(exactly = 0) { netconfSessionSpy.clearReplies() }
+    }
+
+    @Test
+    @Ignore
+    //TODO: get 't' inside asyncRpc to be a Throwable
+    fun `asyncRpc wraps exception`() {
+        assertFailsWith(exceptionClass = NetconfException::class, message = futureMsg) {
+            val netconfSessionSpy = spyk(netconfSession)
+            val futureRet: CompletableFuture<String> = CompletableFuture.supplyAsync {
+                throw Exception("blah")
+            }
+            futureRet.completeExceptionally(IOException("something is wrong"))
+            every { netconfCommunicator.sendMessage(any(), any()) } returns futureRet
+            //RUN
+            val rpcResultFuture = netconfSessionSpy.asyncRpc("0", "0")
+        }
+    }
+
+    @Test
+    fun `connect starts underlying client`() {
+        val propertiesMap = hashMapOf<String, Any>()
+        every { mockSshClient.start() } just Runs
+        every { mockSshClient.properties } returns propertiesMap
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
+        every { netconfSessionSpy["startSession"]() as Unit } just Runs
+        netconfSessionSpy.setClient(mockSshClient)
+        netconfSessionSpy.connect()
+        verify { mockSshClient.start() }
+        assertTrue { propertiesMap.containsKey(FactoryManager.IDLE_TIMEOUT) }
+        assertTrue { propertiesMap.containsKey(FactoryManager.NIO2_READ_TIMEOUT) }
+    }
+
+    @Test
+    fun `startSession tries to connect to user supplied device`() {
+        every { mockSshClient.start() } just Runs
+        every { mockSshClient.properties } returns hashMapOf<String, Any>()
+        //setup slots to capture values from the invocations
+        val userSlot = CapturingSlot<String>()
+        val ipSlot = CapturingSlot<String>()
+        val portSlot = CapturingSlot<Int>()
+        //create a future that succeeded
+        val succeededFuture = DefaultConnectFuture(Any(), Any())
+        succeededFuture.value = mockClientSession
+        every { mockSshClient.connect(capture(userSlot), capture(ipSlot), capture(portSlot)) } returns succeededFuture
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { netconfSessionSpy["authSession"]() as Unit } just Runs
+        every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
+        netconfSessionSpy.setClient(mockSshClient)
+        //RUN
+        netconfSessionSpy.connect()
+        //Verify
+        verify { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) }
+        assertEquals(deviceInfo.username, userSlot.captured)
+        assertEquals(deviceInfo.ipAddress, ipSlot.captured)
+        assertEquals(deviceInfo.port, portSlot.captured)
+        verify { netconfSessionSpy["authSession"]() }
+    }
+
+    @Test
+    fun `authSession throws exception if ClientSession is not AUTHED`() {
+        assertFailsWith(exceptionClass = NetconfException::class) {
+            //after client session connects,
+            every { mockSshClient.start() } just Runs
+            every { mockSshClient.properties } returns hashMapOf<String, Any>()
+            val succeededAuthFuture = DefaultAuthFuture(Any(), Any())
+            succeededAuthFuture.value = true //AuthFuture's value is Boolean
+            val passSlot = CapturingSlot<String>()
+            every { mockClientSession.addPasswordIdentity(capture(passSlot)) } just Runs
+            every { mockClientSession.auth() } returns succeededAuthFuture
+            val succeededSessionFuture = DefaultConnectFuture(Any(), Any())
+            succeededSessionFuture.value = mockClientSession
+            every { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) } returns succeededSessionFuture
+            every { mockClientSession.waitFor(any(), any()) } returns
+                setOf(ClientSession.ClientSessionEvent.WAIT_AUTH, ClientSession.ClientSessionEvent.CLOSED)
+            val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+            every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
+            netconfSessionSpy.setClient(mockSshClient)
+            //RUN
+            netconfSessionSpy.connect()
+        }
+    }
+
+    //common mock initializer for more weird tests.
+    private fun setupOpenChannelMocks(): Unit {
+        every { mockSshClient.start() } just Runs
+        every { mockSshClient.properties } returns hashMapOf<String, Any>()
+        val succeededAuthFuture = DefaultAuthFuture(Any(), Any())
+        succeededAuthFuture.value = true //AuthFuture's value is Boolean
+        val passSlot = CapturingSlot<String>()
+        every { mockClientSession.addPasswordIdentity(capture(passSlot)) } just Runs
+        every { mockClientSession.auth() } returns succeededAuthFuture
+        val succeededSessionFuture = DefaultConnectFuture(Any(), Any())
+        succeededSessionFuture.value = mockClientSession
+        every { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) } returns succeededSessionFuture
+        every { mockClientSession.waitFor(any(), any()) } returns
+            setOf(ClientSession.ClientSessionEvent.WAIT_AUTH,
+                ClientSession.ClientSessionEvent.CLOSED,
+                ClientSession.ClientSessionEvent.AUTHED)
+
+        every { mockClientSession.createSubsystemChannel(any()) } returns mockSubsystem
+        every { mockClientChannel.invertedOut } returns sampleInputStream
+        every { mockClientChannel.invertedIn } returns sampleOutputStream
+    }
+
+    @Test
+    fun `authSession opensChannel if ClientSession is AUTHED and session can be opened`() {
+        //after client session connects, make sure the client receives authentication
+        setupOpenChannelMocks()
+        val channelFuture = DefaultOpenFuture(Any(), Any())
+        channelFuture.value = true
+        channelFuture.setOpened()
+        val connectFuture = DefaultConnectFuture(Any(), Any())
+        connectFuture.value = mockClientSession
+        connectFuture.session = mockClientSession
+        every { mockSubsystem.open() } returns channelFuture
+        every { mockSshClient.connect(deviceInfo.username, deviceInfo.ipAddress, deviceInfo.port) } returns connectFuture
+
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
+        every { netconfSessionSpy["setupHandler"]() as Unit } just Runs
+        netconfSessionSpy.setClient(mockSshClient)
+        //Run
+        netconfSessionSpy.connect()
+        //Verify
+        verify { mockSubsystem.open() }
+    }
+
+
+    @Test
+    fun `authSession throws NetconfException if ClientSession is AUTHED but channelFuture timed out or not open`() {
+        assertFailsWith(exceptionClass = NetconfException::class) {
+            //after client session connects, make sure the client receives authentication
+            setupOpenChannelMocks()
+            val channelFuture = DefaultOpenFuture(Any(), Any())
+            every { mockSubsystem.open() } returns channelFuture
+            val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+            every { netconfSessionSpy["setupNewSSHClient"]() as Unit } just Runs
+            every { netconfSessionSpy["setupHandler"]() as Unit } just Runs
+            netconfSessionSpy.setClient(mockSshClient)
+            //Run
+            netconfSessionSpy.connect()
+            //Verify
+            verify { mockSubsystem.open() }
+        }
+    }
+
+
+    @Test
+    fun `disconnect closes session, channel, and client`() {
+        every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
+        every { mockClientSession.close() } just Runs
+        every { mockClientChannel.close() } just Runs
+        every { mockSshClient.close() } just Runs
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        netconfSessionSpy.setChannel(mockClientChannel)
+        netconfSessionSpy.setClient(mockSshClient)
+        netconfSessionSpy.setSession(mockClientSession)
+        //RUN
+        netconfSessionSpy.disconnect()
+        //VERIFY
+        verify { mockClientSession.close() }
+        verify { mockClientChannel.close() }
+        verify { mockSshClient.close() }
+    }
+
+    @Ignore
+    @Test
+    fun `disconnect wraps IOException if channel doesn't close`() { //this test is equivalent to others
+        every { rpcService.closeSession(false) } returns SUCCESSFUL_DEVICE_RESPONSE
+        every { mockClientSession.close() } just Runs
+        every { mockClientChannel.close() } throws IOException("channel doesn't want to close!")
+        val netconfSessionSpy = spyk(netconfSession, recordPrivateCalls = true)
+        netconfSessionSpy.setChannel(mockClientChannel)
+        netconfSessionSpy.setClient(mockSshClient)
+        netconfSessionSpy.setSession(mockClientSession)
+        //RUN
+        netconfSessionSpy.disconnect()
+        //VERIFY
+        verify { mockClientSession.close() }
+    }
+}