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

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

1
 package com.dmdirc.ktirc
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
 import com.dmdirc.ktirc.model.*
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
  * Primary interface for interacting with KtIrc.
9
  * Primary interface for interacting with KtIrc.
16
  */
10
  */
17
 interface IrcClient {
11
 interface IrcClient {
18
 
12
 
13
+    /**
14
+     * Holds state relating to the current server, its features, and capabilities.
15
+     */
19
     val serverState: ServerState
16
     val serverState: ServerState
17
+
18
+    /**
19
+     * Holds the state for each channel we are currently joined to.
20
+     */
20
     val channelState: ChannelStateMap
21
     val channelState: ChannelStateMap
22
+
23
+    /**
24
+     * Holds the state for all known users (those in common channels).
25
+     */
21
     val userState: UserState
26
     val userState: UserState
22
 
27
 
23
     val caseMapping: CaseMapping
28
     val caseMapping: CaseMapping
86
 @Suppress("FunctionName")
91
 @Suppress("FunctionName")
87
 fun IrcClient(block: IrcClientConfigBuilder.() -> Unit): IrcClient =
92
 fun IrcClient(block: IrcClientConfigBuilder.() -> Unit): IrcClient =
88
         IrcClientImpl(IrcClientConfigBuilder().apply(block).build())
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

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
             enabledCapabilities.putAll(capabilities)
54
             enabledCapabilities.putAll(capabilities)
55
 
55
 
56
             if (client.serverState.sasl.mechanisms.isNotEmpty()) {
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
                         return
59
                         return
60
                     }
60
                     }
61
                 }
61
                 }

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

1
 package com.dmdirc.ktirc.events
1
 package com.dmdirc.ktirc.events
2
 
2
 
3
 import com.dmdirc.ktirc.model.Capability
3
 import com.dmdirc.ktirc.model.Capability
4
+import com.dmdirc.ktirc.model.ConnectionError
4
 import com.dmdirc.ktirc.model.ServerFeatureMap
5
 import com.dmdirc.ktirc.model.ServerFeatureMap
5
 import com.dmdirc.ktirc.model.User
6
 import com.dmdirc.ktirc.model.User
6
 import java.time.LocalDateTime
7
 import java.time.LocalDateTime
17
 /** Raised when the connection to the server has ended. */
18
 /** Raised when the connection to the server has ended. */
18
 class ServerDisconnected(time: LocalDateTime) : IrcEvent(time)
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
 /** Raised when the server is ready for use. */
24
 /** Raised when the server is ready for use. */
21
 class ServerReady(time: LocalDateTime) : IrcEvent(time)
25
 class ServerReady(time: LocalDateTime) : IrcEvent(time)
22
 
26
 

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

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

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

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
 package com.dmdirc.ktirc
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
 import com.dmdirc.ktirc.io.CaseMapping
4
 import com.dmdirc.ktirc.io.CaseMapping
8
 import com.dmdirc.ktirc.io.LineBufferedSocket
5
 import com.dmdirc.ktirc.io.LineBufferedSocket
9
 import com.dmdirc.ktirc.model.ChannelState
6
 import com.dmdirc.ktirc.model.ChannelState
7
+import com.dmdirc.ktirc.model.ConnectionError
10
 import com.dmdirc.ktirc.model.ServerFeature
8
 import com.dmdirc.ktirc.model.ServerFeature
11
 import com.dmdirc.ktirc.model.User
9
 import com.dmdirc.ktirc.model.User
12
 import com.dmdirc.ktirc.util.currentTimeProvider
10
 import com.dmdirc.ktirc.util.currentTimeProvider
16
 import kotlinx.coroutines.channels.Channel
14
 import kotlinx.coroutines.channels.Channel
17
 import kotlinx.coroutines.channels.filter
15
 import kotlinx.coroutines.channels.filter
18
 import kotlinx.coroutines.channels.map
16
 import kotlinx.coroutines.channels.map
17
+import kotlinx.coroutines.sync.Mutex
19
 import org.junit.jupiter.api.Assertions.*
18
 import org.junit.jupiter.api.Assertions.*
20
 import org.junit.jupiter.api.BeforeEach
19
 import org.junit.jupiter.api.BeforeEach
21
 import org.junit.jupiter.api.Test
20
 import org.junit.jupiter.api.Test
22
 import org.junit.jupiter.api.assertThrows
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
 @KtorExperimentalAPI
26
 @KtorExperimentalAPI
25
 @ExperimentalCoroutinesApi
27
 @ExperimentalCoroutinesApi
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
     }
81
     }
82
 
82
 
83
     @Test
83
     @Test
84
-    fun `sends END when capabilities acknowledged and no profile`() {
84
+    fun `sends END when capabilities acknowledged and no enabled mechanisms`() {
85
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
85
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
86
                 Capability.EchoMessages to "",
86
                 Capability.EchoMessages to "",
87
-                Capability.HostsInNamesReply to "123"
87
+                Capability.HostsInNamesReply to ""
88
         )))
88
         )))
89
 
89
 
90
         verify(ircClient).send("CAP END")
90
         verify(ircClient).send("CAP END")
95
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
95
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
96
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
96
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
97
                 Capability.EchoMessages to "",
97
                 Capability.EchoMessages to "",
98
-                Capability.HostsInNamesReply to "123"
98
+                Capability.HostsInNamesReply to ""
99
         )))
99
         )))
100
 
100
 
101
         verify(ircClient).send("CAP END")
101
         verify(ircClient).send("CAP END")
104
     @Test
104
     @Test
105
     fun `sends END when capabilities acknowledged and no shared mechanism`() {
105
     fun `sends END when capabilities acknowledged and no shared mechanism`() {
106
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
106
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
107
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "fake1,fake2"
107
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
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
         verify(ircClient).send("CAP END")
113
         verify(ircClient).send("CAP END")
113
     }
114
     }
114
 
115
 
115
     @Test
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
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
118
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
119
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "mech1,fake2"
118
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
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
     @Test
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
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
130
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
131
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = ""
129
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
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
     @Test
140
     @Test
139
     fun `sends authenticate when capabilities acknowledged with shared mechanism`() {
141
     fun `sends authenticate when capabilities acknowledged with shared mechanism`() {
140
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
142
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
143
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "mech1,fake2"
141
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
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
         verify(ircClient).send("AUTHENTICATE mech1")
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
     @Test
164
     @Test
150
     fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
165
     fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
151
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
166
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
167
+        serverState.capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "mech1,fake2"
152
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
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
         assertEquals(CapabilitiesNegotiationState.AUTHENTICATING, serverState.capabilities.negotiationState)
173
         assertEquals(CapabilitiesNegotiationState.AUTHENTICATING, serverState.capabilities.negotiationState)

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

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