Browse Source

Handle server connection errors

Fix regression with capability negotiation
tags/v0.7.0
Chris Smith 5 years ago
parent
commit
dd87752527

+ 1
- 0
CHANGELOG View File

@@ -9,6 +9,7 @@ vNEXT (in development)
9 9
    * Added support for EXTERNAL mechanism, disabled by default
10 10
    * Now attempts to renegotiate if the server doesn't recognise the SASL mechanism that was tried
11 11
  * Added UserNickChanged and corresponding ChannelNickChanged events
12
+ * Added ServerConnectionError, raised when connecting to the server fails
12 13
  * (Internal) Minor version updates for Gradle, Kotlin and JUnit
13 14
 
14 15
 v0.6.0

+ 14
- 91
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt View File

@@ -1,23 +1,28 @@
1 1
 package com.dmdirc.ktirc
2 2
 
3
-import com.dmdirc.ktirc.events.*
4
-import com.dmdirc.ktirc.io.*
5
-import com.dmdirc.ktirc.messages.*
3
+import com.dmdirc.ktirc.events.IrcEvent
4
+import com.dmdirc.ktirc.io.CaseMapping
5
+import com.dmdirc.ktirc.messages.sendJoin
6 6
 import com.dmdirc.ktirc.model.*
7
-import com.dmdirc.ktirc.util.currentTimeProvider
8
-import com.dmdirc.ktirc.util.logger
9
-import io.ktor.util.KtorExperimentalAPI
10
-import kotlinx.coroutines.*
11
-import kotlinx.coroutines.channels.map
12
-import java.util.concurrent.atomic.AtomicBoolean
13 7
 
14 8
 /**
15 9
  * Primary interface for interacting with KtIrc.
16 10
  */
17 11
 interface IrcClient {
18 12
 
13
+    /**
14
+     * Holds state relating to the current server, its features, and capabilities.
15
+     */
19 16
     val serverState: ServerState
17
+
18
+    /**
19
+     * Holds the state for each channel we are currently joined to.
20
+     */
20 21
     val channelState: ChannelStateMap
22
+
23
+    /**
24
+     * Holds the state for all known users (those in common channels).
25
+     */
21 26
     val userState: UserState
22 27
 
23 28
     val caseMapping: CaseMapping
@@ -86,85 +91,3 @@ interface IrcClient {
86 91
 @Suppress("FunctionName")
87 92
 fun IrcClient(block: IrcClientConfigBuilder.() -> Unit): IrcClient =
88 93
         IrcClientImpl(IrcClientConfigBuilder().apply(block).build())
89
-
90
-/**
91
- * Concrete implementation of an [IrcClient].
92
- */
93
-// TODO: How should alternative nicknames work?
94
-// TODO: Should IRC Client take a pool of servers and rotate through, or make the caller do that?
95
-// TODO: Should there be a default profile?
96
-internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, CoroutineScope {
97
-
98
-    private val log by logger()
99
-
100
-    @ExperimentalCoroutinesApi
101
-    override val coroutineContext = GlobalScope.newCoroutineContext(Dispatchers.IO)
102
-
103
-    @ExperimentalCoroutinesApi
104
-    @KtorExperimentalAPI
105
-    internal var socketFactory: (CoroutineScope, String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
106
-
107
-    override val serverState = ServerState(config.profile.nickname, config.server.host, config.sasl)
108
-    override val channelState = ChannelStateMap { caseMapping }
109
-    override val userState = UserState { caseMapping }
110
-
111
-    private val messageHandler = MessageHandler(messageProcessors.toList(), eventHandlers.toMutableList())
112
-
113
-    private val parser = MessageParser()
114
-    private var socket: LineBufferedSocket? = null
115
-
116
-    private val connecting = AtomicBoolean(false)
117
-
118
-    override fun send(message: String) {
119
-        socket?.sendChannel?.offer(message.toByteArray()) ?: log.warning { "No send channel for message: $message" }
120
-    }
121
-
122
-    override fun connect() {
123
-        check(!connecting.getAndSet(true))
124
-
125
-        @Suppress("EXPERIMENTAL_API_USAGE")
126
-        with(socketFactory(this, config.server.host, config.server.port, config.server.useTls)) {
127
-            // TODO: Proper error handling - what if connect() fails?
128
-            socket = this
129
-
130
-            emitEvent(ServerConnecting(currentTimeProvider()))
131
-
132
-            launch {
133
-                connect()
134
-                emitEvent(ServerConnected(currentTimeProvider()))
135
-                sendCapabilityList()
136
-                sendPasswordIfPresent()
137
-                sendNickChange(config.profile.nickname)
138
-                sendUser(config.profile.username, config.profile.realName)
139
-                messageHandler.processMessages(this@IrcClientImpl, receiveChannel.map { parser.parse(it) })
140
-                reset()
141
-                emitEvent(ServerDisconnected(currentTimeProvider()))
142
-            }
143
-        }
144
-    }
145
-
146
-    override fun disconnect() {
147
-        socket?.disconnect()
148
-    }
149
-
150
-    override fun onEvent(handler: (IrcEvent) -> Unit) {
151
-        messageHandler.handlers.add(object : EventHandler {
152
-            override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
153
-                handler(event)
154
-                return emptyList()
155
-            }
156
-        })
157
-    }
158
-
159
-    private fun emitEvent(event: IrcEvent) = messageHandler.emitEvent(this, event)
160
-    private fun sendPasswordIfPresent() = config.server.password?.let(this::sendPassword)
161
-
162
-    internal fun reset() {
163
-        serverState.reset()
164
-        channelState.clear()
165
-        userState.reset()
166
-        socket = null
167
-        connecting.set(false)
168
-    }
169
-
170
-}

+ 104
- 0
src/main/kotlin/com/dmdirc/ktirc/IrcClientImpl.kt View File

@@ -0,0 +1,104 @@
1
+package com.dmdirc.ktirc
2
+
3
+import com.dmdirc.ktirc.events.*
4
+import com.dmdirc.ktirc.io.KtorLineBufferedSocket
5
+import com.dmdirc.ktirc.io.LineBufferedSocket
6
+import com.dmdirc.ktirc.io.MessageHandler
7
+import com.dmdirc.ktirc.io.MessageParser
8
+import com.dmdirc.ktirc.messages.*
9
+import com.dmdirc.ktirc.model.ChannelStateMap
10
+import com.dmdirc.ktirc.model.ServerState
11
+import com.dmdirc.ktirc.model.UserState
12
+import com.dmdirc.ktirc.model.toConnectionError
13
+import com.dmdirc.ktirc.util.currentTimeProvider
14
+import com.dmdirc.ktirc.util.logger
15
+import io.ktor.util.KtorExperimentalAPI
16
+import kotlinx.coroutines.*
17
+import kotlinx.coroutines.channels.map
18
+import java.util.concurrent.atomic.AtomicBoolean
19
+
20
+/**
21
+ * Concrete implementation of an [IrcClient].
22
+ */
23
+// TODO: How should alternative nicknames work?
24
+// TODO: Should IRC Client take a pool of servers and rotate through, or make the caller do that?
25
+// TODO: Should there be a default profile?
26
+internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, CoroutineScope {
27
+
28
+    private val log by logger()
29
+
30
+    @ExperimentalCoroutinesApi
31
+    override val coroutineContext = GlobalScope.newCoroutineContext(Dispatchers.IO)
32
+
33
+    @ExperimentalCoroutinesApi
34
+    @KtorExperimentalAPI
35
+    internal var socketFactory: (CoroutineScope, String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
36
+
37
+    override val serverState = ServerState(config.profile.nickname, config.server.host, config.sasl)
38
+    override val channelState = ChannelStateMap { caseMapping }
39
+    override val userState = UserState { caseMapping }
40
+
41
+    private val messageHandler = MessageHandler(messageProcessors.toList(), eventHandlers.toMutableList())
42
+
43
+    private val parser = MessageParser()
44
+    private var socket: LineBufferedSocket? = null
45
+
46
+    private val connecting = AtomicBoolean(false)
47
+
48
+    override fun send(message: String) {
49
+        socket?.sendChannel?.offer(message.toByteArray()) ?: log.warning { "No send channel for message: $message" }
50
+    }
51
+
52
+    override fun connect() {
53
+        check(!connecting.getAndSet(true))
54
+
55
+        @Suppress("EXPERIMENTAL_API_USAGE")
56
+        with(socketFactory(this, config.server.host, config.server.port, config.server.useTls)) {
57
+            socket = this
58
+
59
+            emitEvent(ServerConnecting(currentTimeProvider()))
60
+
61
+            launch {
62
+                try {
63
+                    connect()
64
+                    emitEvent(ServerConnected(currentTimeProvider()))
65
+                    sendCapabilityList()
66
+                    sendPasswordIfPresent()
67
+                    sendNickChange(config.profile.nickname)
68
+                    sendUser(config.profile.username, config.profile.realName)
69
+                    messageHandler.processMessages(this@IrcClientImpl, receiveChannel.map { parser.parse(it) })
70
+                } catch (ex : Exception) {
71
+                    emitEvent(ServerConnectionError(currentTimeProvider(), ex.toConnectionError(), ex.localizedMessage))
72
+                }
73
+
74
+                reset()
75
+                emitEvent(ServerDisconnected(currentTimeProvider()))
76
+            }
77
+        }
78
+    }
79
+
80
+    override fun disconnect() {
81
+        socket?.disconnect()
82
+    }
83
+
84
+    override fun onEvent(handler: (IrcEvent) -> Unit) {
85
+        messageHandler.handlers.add(object : EventHandler {
86
+            override fun processEvent(client: IrcClient, event: IrcEvent): List<IrcEvent> {
87
+                handler(event)
88
+                return emptyList()
89
+            }
90
+        })
91
+    }
92
+
93
+    private fun emitEvent(event: IrcEvent) = messageHandler.emitEvent(this, event)
94
+    private fun sendPasswordIfPresent() = config.server.password?.let(this::sendPassword)
95
+
96
+    internal fun reset() {
97
+        serverState.reset()
98
+        channelState.clear()
99
+        userState.reset()
100
+        socket = null
101
+        connecting.set(false)
102
+    }
103
+
104
+}

+ 2
- 2
src/main/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandler.kt View File

@@ -54,8 +54,8 @@ internal class CapabilitiesHandler : EventHandler {
54 54
             enabledCapabilities.putAll(capabilities)
55 55
 
56 56
             if (client.serverState.sasl.mechanisms.isNotEmpty()) {
57
-                enabledCapabilities[Capability.SaslAuthentication]?.let { serverCaps ->
58
-                    if (startSaslAuth(client, serverCaps.split(','))) {
57
+                advertisedCapabilities[Capability.SaslAuthentication]?.let { serverCaps ->
58
+                    if (startSaslAuth(client, if (serverCaps.isEmpty()) emptyList() else serverCaps.split(','))) {
59 59
                         return
60 60
                     }
61 61
                 }

+ 4
- 0
src/main/kotlin/com/dmdirc/ktirc/events/Events.kt View File

@@ -1,6 +1,7 @@
1 1
 package com.dmdirc.ktirc.events
2 2
 
3 3
 import com.dmdirc.ktirc.model.Capability
4
+import com.dmdirc.ktirc.model.ConnectionError
4 5
 import com.dmdirc.ktirc.model.ServerFeatureMap
5 6
 import com.dmdirc.ktirc.model.User
6 7
 import java.time.LocalDateTime
@@ -17,6 +18,9 @@ class ServerConnected(time: LocalDateTime) : IrcEvent(time)
17 18
 /** Raised when the connection to the server has ended. */
18 19
 class ServerDisconnected(time: LocalDateTime) : IrcEvent(time)
19 20
 
21
+/** Raised when an error occurred trying to connect. */
22
+class ServerConnectionError(time: LocalDateTime, val error: ConnectionError, val details: String?) : IrcEvent(time)
23
+
20 24
 /** Raised when the server is ready for use. */
21 25
 class ServerReady(time: LocalDateTime) : IrcEvent(time)
22 26
 

+ 3
- 0
src/main/kotlin/com/dmdirc/ktirc/io/LineBufferedSocket.kt View File

@@ -17,11 +17,14 @@ import kotlinx.coroutines.io.ByteReadChannel
17 17
 import kotlinx.coroutines.io.ByteWriteChannel
18 18
 import java.net.InetSocketAddress
19 19
 import java.security.SecureRandom
20
+import java.security.cert.CertificateException
20 21
 import javax.net.ssl.X509TrustManager
21 22
 
22 23
 internal interface LineBufferedSocket {
23 24
 
25
+    @Throws(CertificateException::class)
24 26
     fun connect()
27
+
25 28
     fun disconnect()
26 29
 
27 30
     val sendChannel: SendChannel<ByteArray>

+ 27
- 0
src/main/kotlin/com/dmdirc/ktirc/model/ConnectionError.kt View File

@@ -0,0 +1,27 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+import java.net.ConnectException
4
+import java.nio.channels.UnresolvedAddressException
5
+import java.security.cert.CertificateException
6
+
7
+/**
8
+ * Possible types of errors that occur whilst connecting.
9
+ */
10
+enum class ConnectionError {
11
+    /** An error occurred, but we don't really know what. */
12
+    Unknown,
13
+    /** The hostname did not resolve to an IP address. */
14
+    UnresolvableAddress,
15
+    /** A connection couldn't be established to the given host/port. */
16
+    ConnectionRefused,
17
+    /** There was an issue with the TLS certificate the server presented. */
18
+    BadTlsCertificate,
19
+}
20
+
21
+internal fun Exception.toConnectionError() =
22
+        when (this) {
23
+            is UnresolvedAddressException -> ConnectionError.UnresolvableAddress
24
+            is CertificateException -> ConnectionError.BadTlsCertificate
25
+            is ConnectException -> ConnectionError.ConnectionRefused
26
+            else -> ConnectionError.Unknown
27
+        }

src/test/kotlin/com/dmdirc/ktirc/IrcClientTest.kt → src/test/kotlin/com/dmdirc/ktirc/IrcClientImplTest.kt View File

@@ -1,12 +1,10 @@
1 1
 package com.dmdirc.ktirc
2 2
 
3
-import com.dmdirc.ktirc.events.IrcEvent
4
-import com.dmdirc.ktirc.events.ServerConnected
5
-import com.dmdirc.ktirc.events.ServerConnecting
6
-import com.dmdirc.ktirc.events.ServerWelcome
3
+import com.dmdirc.ktirc.events.*
7 4
 import com.dmdirc.ktirc.io.CaseMapping
8 5
 import com.dmdirc.ktirc.io.LineBufferedSocket
9 6
 import com.dmdirc.ktirc.model.ChannelState
7
+import com.dmdirc.ktirc.model.ConnectionError
10 8
 import com.dmdirc.ktirc.model.ServerFeature
11 9
 import com.dmdirc.ktirc.model.User
12 10
 import com.dmdirc.ktirc.util.currentTimeProvider
@@ -16,10 +14,14 @@ import kotlinx.coroutines.*
16 14
 import kotlinx.coroutines.channels.Channel
17 15
 import kotlinx.coroutines.channels.filter
18 16
 import kotlinx.coroutines.channels.map
17
+import kotlinx.coroutines.sync.Mutex
19 18
 import org.junit.jupiter.api.Assertions.*
20 19
 import org.junit.jupiter.api.BeforeEach
21 20
 import org.junit.jupiter.api.Test
22 21
 import org.junit.jupiter.api.assertThrows
22
+import java.nio.channels.UnresolvedAddressException
23
+import java.security.cert.CertificateException
24
+import java.util.concurrent.atomic.AtomicReference
23 25
 
24 26
 @KtorExperimentalAPI
25 27
 @ExperimentalCoroutinesApi
@@ -265,5 +267,51 @@ internal class IrcClientImplTest {
265 267
         }
266 268
     }
267 269
 
270
+    @Test
271
+    fun `sends connect error when host is unresolvable`() = runBlocking {
272
+        whenever(mockSocket.connect()).doThrow(UnresolvedAddressException())
273
+        with(IrcClientImpl(normalConfig)) {
274
+            socketFactory = mockSocketFactory
275
+            withTimeout(500) {
276
+                launch {
277
+                    delay(50)
278
+                    connect()
279
+                }
280
+                val event = waitForEvent<ServerConnectionError>()
281
+                assertEquals(ConnectionError.UnresolvableAddress, event.error)
282
+            }
283
+        }
284
+    }
285
+
286
+    @Test
287
+    fun `sends connect error when tls certificate is bad`() = runBlocking {
288
+        whenever(mockSocket.connect()).doThrow(CertificateException("Boooo"))
289
+        with(IrcClientImpl(normalConfig)) {
290
+            socketFactory = mockSocketFactory
291
+            withTimeout(500) {
292
+                launch {
293
+                    delay(50)
294
+                    connect()
295
+                }
296
+                val event = waitForEvent<ServerConnectionError>()
297
+                assertEquals(ConnectionError.BadTlsCertificate, event.error)
298
+                assertEquals("Boooo", event.details)
299
+            }
300
+        }
301
+    }
302
+
303
+    private suspend inline fun <reified T : IrcEvent> IrcClient.waitForEvent(): T {
304
+        val mutex = Mutex(true)
305
+        val value = AtomicReference<T>()
306
+        onEvent {
307
+            if (it is T) {
308
+                value.set(it)
309
+                mutex.unlock()
310
+            }
311
+        }
312
+        mutex.lock()
313
+        return value.get()
314
+    }
315
+
268 316
 
269 317
 }

+ 34
- 18
src/test/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandlerTest.kt View File

@@ -81,10 +81,10 @@ internal class CapabilitiesHandlerTest {
81 81
     }
82 82
 
83 83
     @Test
84
-    fun `sends END when capabilities acknowledged and no profile`() {
84
+    fun `sends END when capabilities acknowledged and no enabled mechanisms`() {
85 85
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
86 86
                 Capability.EchoMessages to "",
87
-                Capability.HostsInNamesReply to "123"
87
+                Capability.HostsInNamesReply to ""
88 88
         )))
89 89
 
90 90
         verify(ircClient).send("CAP END")
@@ -95,7 +95,7 @@ internal class CapabilitiesHandlerTest {
95 95
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
96 96
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
97 97
                 Capability.EchoMessages to "",
98
-                Capability.HostsInNamesReply to "123"
98
+                Capability.HostsInNamesReply to ""
99 99
         )))
100 100
 
101 101
         verify(ircClient).send("CAP END")
@@ -104,54 +104,70 @@ internal class CapabilitiesHandlerTest {
104 104
     @Test
105 105
     fun `sends END when capabilities acknowledged and no shared mechanism`() {
106 106
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
107
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "fake1,fake2"
107 108
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
108
-                Capability.SaslAuthentication to "fake1,fake2",
109
-                Capability.HostsInNamesReply to "123"
109
+                Capability.SaslAuthentication to "",
110
+                Capability.HostsInNamesReply to ""
110 111
         )))
111 112
 
112 113
         verify(ircClient).send("CAP END")
113 114
     }
114 115
 
115 116
     @Test
116
-    fun `sends AUTHENTICATE when capabilities acknowledged with shared mechanism`() {
117
+    fun `sets current SASL mechanism when capabilities acknowledged with shared mechanism`() {
117 118
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
119
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "mech1,fake2"
118 120
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
119
-                Capability.SaslAuthentication to "mech1,fake2",
120
-                Capability.HostsInNamesReply to "123"
121
+                Capability.SaslAuthentication to "",
122
+                Capability.HostsInNamesReply to ""
121 123
         )))
122 124
 
123
-        verify(ircClient).send("AUTHENTICATE mech1")
125
+        assertSame(saslMech1, serverState.sasl.currentMechanism)
124 126
     }
125 127
 
126 128
     @Test
127
-    fun `sets current SASL mechanism when capabilities acknowledged with shared mechanism`() {
129
+    fun `sets current SASL mechanism when capabilities acknowledged with no declared mechanisms`() {
128 130
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
131
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = ""
129 132
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
130
-                Capability.SaslAuthentication to "mech1,fake2",
131
-                Capability.HostsInNamesReply to "123"
133
+                Capability.SaslAuthentication to "",
134
+                Capability.HostsInNamesReply to ""
132 135
         )))
133 136
 
134
-        assertSame(saslMech1, serverState.sasl.currentMechanism)
137
+        assertSame(saslMech3, serverState.sasl.currentMechanism)
135 138
     }
136 139
 
137
-
138 140
     @Test
139 141
     fun `sends authenticate when capabilities acknowledged with shared mechanism`() {
140 142
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
143
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "mech1,fake2"
141 144
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
142
-                Capability.SaslAuthentication to "mech1,fake2",
143
-                Capability.HostsInNamesReply to "123"
145
+                Capability.SaslAuthentication to "",
146
+                Capability.HostsInNamesReply to ""
144 147
         )))
145 148
 
146 149
         verify(ircClient).send("AUTHENTICATE mech1")
147 150
     }
148 151
 
152
+    @Test
153
+    fun `sends authenticate when capabilities acknowledged with no declared mechanisms`() {
154
+        serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
155
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = ""
156
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
157
+                Capability.SaslAuthentication to "",
158
+                Capability.HostsInNamesReply to ""
159
+        )))
160
+
161
+        verify(ircClient).send("AUTHENTICATE mech3")
162
+    }
163
+
149 164
     @Test
150 165
     fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
151 166
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
167
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "mech1,fake2"
152 168
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
153
-                Capability.SaslAuthentication to "mech1,fake2",
154
-                Capability.HostsInNamesReply to "123"
169
+                Capability.SaslAuthentication to "",
170
+                Capability.HostsInNamesReply to ""
155 171
         )))
156 172
 
157 173
         assertEquals(CapabilitiesNegotiationState.AUTHENTICATING, serverState.capabilities.negotiationState)

+ 19
- 0
src/test/kotlin/com/dmdirc/ktirc/model/ConnectionErrorTest.kt View File

@@ -0,0 +1,19 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+import org.junit.jupiter.api.Assertions.assertEquals
4
+import org.junit.jupiter.api.Test
5
+import java.net.ConnectException
6
+import java.nio.channels.UnresolvedAddressException
7
+import java.security.cert.CertificateException
8
+
9
+internal class ConnectionErrorTest {
10
+
11
+    @Test
12
+    fun `maps exceptions to ConnectionError types`() {
13
+        assertEquals(ConnectionError.ConnectionRefused, ConnectException().toConnectionError())
14
+        assertEquals(ConnectionError.BadTlsCertificate, CertificateException().toConnectionError())
15
+        assertEquals(ConnectionError.UnresolvableAddress, UnresolvedAddressException().toConnectionError())
16
+        assertEquals(ConnectionError.Unknown, IllegalArgumentException().toConnectionError())
17
+    }
18
+
19
+}

Loading…
Cancel
Save