Browse Source

Construct IrcClient with a DSL.

This allows more options to be added nicely in the future,
and hides the implementation details from library users.
tags/v0.7.0
Chris Smith 5 years ago
parent
commit
d76c60a47c

+ 3
- 0
CHANGELOG View File

@@ -1,6 +1,9 @@
1 1
 vNEXT (in development)
2 2
 
3 3
  * Fixed experimental API warnings when using IrcClient
4
+ * IrcClients are now constructed using a DSL
5
+   * Users of the library no longer need to care about the implementing class
6
+   * Facilitates adding more options in the future without breaking existing implementations
4 7
  * (Internal) Minor version updates for Gradle, Kotlin and JUnit
5 8
 
6 9
 v0.6.0

+ 49
- 14
README.md View File

@@ -8,6 +8,27 @@
8 8
 KtIrc is a Kotlin JVM library for connecting to and interacting with IRC servers.
9 9
 It is still in an early stage of development.
10 10
 
11
+## Features
12
+
13
+#### Built for Kotlin
14
+
15
+KtIrc is written in and designed for use in Kotlin; it uses extension methods,
16
+DSLs, sealed classes, and so on, to make it much easier to use than an
17
+equivalent Java library.
18
+
19
+#### Coroutine-powered
20
+
21
+KtIrc uses co-routines for all of its input/output which lets it deal with
22
+IRC messages in the background while your app does other things, without
23
+the overhead of creating a new thread per IRC client.
24
+
25
+#### Modern IRC standards
26
+
27
+KtIrc supports many IRCv3 features such as SASL authentication, message IDs,
28
+server timestamps, replies, reactions, account tags, and more. These features
29
+(where server support is available) make it easier to develop bots and
30
+clients, and enhance IRC with new user-facing functionality.
31
+
11 32
 ## Setup
12 33
 
13 34
 KtIrc is published to JCenter, so adding it to a gradle build is as simple as:
@@ -24,21 +45,34 @@ dependencies {
24 45
 
25 46
 ## Usage
26 47
 
27
-The main interface for interacting with KtIrc is the `IrcClientImpl` class. A
28
-simple bot might look like:
48
+Clients are created using a DSL and the `IrcClient` function. At a minimum
49
+you must specify a server and a profile. A simple bot might look like:
29 50
 
30 51
 ```kotlin
31
-with(IrcClientImpl(Server("my.server.com", 6667), Profile("nick", "realName", "userName"))) {
32
-    onEvent { event ->
33
-        when (event) {
34
-            is ServerReady -> sendJoin("#ktirc")
35
-            is MessageReceived ->
36
-                if (event.message == "!test")
37
-                    reply(event, "Test successful!")
38
-        }
52
+val client = IrcClient {
53
+    server {
54
+        host = "my.server.com"
55
+    } 
56
+    profile {
57
+        nickname = "nick"
58
+        username = "username"
59
+        realName = "Hi there"
39 60
     }
40
-    connect()
41 61
 }
62
+
63
+client.onEvent { event ->
64
+    when (event) {
65
+        is ServerReady ->
66
+            client.sendJoin("#ktirc")
67
+        is ServerDisconnected ->
68
+            client.connect()
69
+        is MessageReceived ->
70
+            if (event.message == "!test")
71
+                client.reply(event, "Test successful!")
72
+    }
73
+}
74
+
75
+client.connect()
42 76
 ```
43 77
 
44 78
 ## Known issues / FAQ
@@ -47,9 +81,10 @@ with(IrcClientImpl(Server("my.server.com", 6667), Profile("nick", "realName", "u
47 81
 
48 82
 This happens when the IRC server requests an optional client certificate (for use
49 83
 in SASL auth, usually). At present there is no support for client certificates in
50
-the networking library used by KtIrc. This is tracked upstream in
51
-[ktor#641](https://github.com/ktorio/ktor/issues/641). There is no workaround
52
-other than using an insecure connection.
84
+the networking library used by KtIrc. This is fixed in the
85
+[upstream library](https://github.com/ktorio/ktor/issues/641) and will be included
86
+as soon as snapshot builds are available. There is no workaround other than using
87
+an insecure connection.
53 88
 
54 89
 ### KtIrc connects over IPv4 even when host has IPv6
55 90
 

+ 13
- 4
src/itest/kotlin/com/dmdirc/ktirc/KtIrcIntegrationTest.kt View File

@@ -1,8 +1,6 @@
1 1
 package com.dmdirc.ktirc
2 2
 
3 3
 import com.dmdirc.irctest.IrcLibraryTests
4
-import com.dmdirc.ktirc.model.Profile
5
-import com.dmdirc.ktirc.model.Server
6 4
 import kotlinx.coroutines.runBlocking
7 5
 import org.junit.jupiter.api.TestFactory
8 6
 
@@ -11,10 +9,21 @@ class KtIrcIntegrationTest {
11 9
     @TestFactory
12 10
     fun dynamicTests() = IrcLibraryTests().getTests(object : IrcLibraryTests.IrcLibrary {
13 11
 
14
-        private lateinit var ircClient : IrcClientImpl
12
+        private lateinit var ircClient : IrcClient
15 13
 
16 14
         override fun connect(nick: String, ident: String, realName: String, password: String?) {
17
-            ircClient = IrcClientImpl(Server("localhost", 12321, password = password), Profile(nick, ident, realName))
15
+            ircClient = IrcClient {
16
+                server {
17
+                    host = "localhost"
18
+                    port = 12321
19
+                    this.password = password
20
+                }
21
+                profile {
22
+                    nickname = nick
23
+                    username = ident
24
+                    this.realName = realName
25
+                }
26
+            }
18 27
             ircClient.connect()
19 28
         }
20 29
 

+ 119
- 0
src/main/kotlin/com/dmdirc/ktirc/Dsl.kt View File

@@ -0,0 +1,119 @@
1
+package com.dmdirc.ktirc
2
+
3
+/**
4
+ * Dsl marker for [IrcClient] dsl.
5
+ */
6
+@DslMarker
7
+annotation class IrcClientDsl
8
+
9
+internal data class IrcClientConfig(val server: ServerConfig, val profile: ProfileConfig, val sasl: SaslConfig?)
10
+
11
+/**
12
+ * Dsl for configuring an IRC Client.
13
+ *
14
+ * [server] and [profile] blocks are required. The full range of configuration options are:
15
+ *
16
+ * ```
17
+ * server {
18
+ *     host = "irc.example.com"     // Required
19
+ *     port = 6667
20
+ *     useTls = true
21
+ *     password = "H4ckTh3Pl4n3t"
22
+ * }
23
+ *
24
+ * profile {
25
+ *     nickname = "MyBot"           // Required
26
+ *     username = "bot
27
+ *     realName = "Botomatic v1.2"
28
+ * }
29
+ *
30
+ * sasl {
31
+ *     username = "botaccount"
32
+ *     password = "s3cur3"
33
+ * }
34
+ * ```
35
+ */
36
+@IrcClientDsl
37
+class IrcClientConfigBuilder {
38
+
39
+    private var server: ServerConfig? = null
40
+    private var profile: ProfileConfig? = null
41
+    private var sasl: SaslConfig? = null
42
+
43
+    /**
44
+     * Configures the server that the IrcClient will connect to.
45
+     *
46
+     * At a minimum, [ServerConfig.host] must be supplied.
47
+     */
48
+    @IrcClientDsl
49
+    fun server(block: ServerConfig.() -> Unit) {
50
+        check(server == null) { "server may only be specified once" }
51
+        server = ServerConfig().apply(block).also { check(it.host.isNotEmpty()) { "server.host must be specified" } }
52
+    }
53
+
54
+    /**
55
+     * Configures the profile of the IrcClient user.
56
+     *
57
+     * At a minimum, [ProfileConfig.nickName] must be supplied.
58
+     */
59
+    @IrcClientDsl
60
+    fun profile(block: ProfileConfig.() -> Unit) {
61
+        check(profile == null) { "profile may only be specified once" }
62
+        profile = ProfileConfig().apply(block).also { check(it.nickname.isNotEmpty()) { "profile.nickname must be specified" } }
63
+    }
64
+
65
+    /**
66
+     * Configures SASL authentication (optional).
67
+     */
68
+    @IrcClientDsl
69
+    fun sasl(block: SaslConfig.() -> Unit) {
70
+        check(sasl == null) { "sasl may only be specified once" }
71
+        sasl = SaslConfig().apply(block)
72
+    }
73
+
74
+    internal fun build() =
75
+            IrcClientConfig(
76
+                    checkNotNull(server) { "Server must be specified " },
77
+                    checkNotNull(profile) { "Profile must be specified" },
78
+                    sasl)
79
+
80
+}
81
+
82
+/**
83
+ * Dsl for configuring a server.
84
+ */
85
+@IrcClientDsl
86
+class ServerConfig {
87
+    /** The hostname (or IP address) of the server to connect to. */
88
+    var host: String = ""
89
+    /** The port to connect on. Defaults to 6667. */
90
+    var port: Int = 6667
91
+    /** Whether or not to use TLS (an encrypted connection). */
92
+    var useTls: Boolean = false
93
+    /** The password required to connect to the server, if any. */
94
+    var password: String? = null
95
+}
96
+
97
+/**
98
+ * Dsl for configuring a profile.
99
+ */
100
+@IrcClientDsl
101
+class ProfileConfig {
102
+    /** The initial nickname to use when connecting. */
103
+    var nickname: String = ""
104
+    /** The username (used in place of an ident response) to provide to the server. */
105
+    var username: String = "KtIrc"
106
+    /** The "real name" to provide to the server. */
107
+    var realName: String = "KtIrc User"
108
+}
109
+
110
+/**
111
+ * Dsl for configuring SASL authentication.
112
+ */
113
+@IrcClientDsl
114
+class SaslConfig {
115
+    /** The username to provide when authenticating using SASL. */
116
+    var username: String = ""
117
+    /** The username to provide when authenticating using SASL. */
118
+    var password: String = ""
119
+}

+ 29
- 12
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt View File

@@ -4,6 +4,8 @@ import com.dmdirc.ktirc.events.*
4 4
 import com.dmdirc.ktirc.io.*
5 5
 import com.dmdirc.ktirc.messages.*
6 6
 import com.dmdirc.ktirc.model.*
7
+import com.dmdirc.ktirc.sasl.PlainMechanism
8
+import com.dmdirc.ktirc.sasl.SaslMechanism
7 9
 import com.dmdirc.ktirc.util.currentTimeProvider
8 10
 import com.dmdirc.ktirc.util.logger
9 11
 import io.ktor.util.KtorExperimentalAPI
@@ -19,7 +21,7 @@ interface IrcClient {
19 21
     val serverState: ServerState
20 22
     val channelState: ChannelStateMap
21 23
     val userState: UserState
22
-    val profile: Profile
24
+    val hasSaslConfig: Boolean
23 25
 
24 26
     val caseMapping: CaseMapping
25 27
         get() = serverState.features[ServerFeature.ServerCaseMapping] ?: CaseMapping.Rfc
@@ -79,15 +81,22 @@ interface IrcClient {
79 81
 }
80 82
 
81 83
 /**
82
- * Concrete implementation of an [IrcClient].
84
+ * Constructs a new [IrcClient] using a configuration DSL.
83 85
  *
84
- * @param server The server to connect to.
85
- * @param profile The user details to use when connecting.
86
+ * See [IrcClientConfigBuilder] for details of all options
87
+ */
88
+@IrcClientDsl
89
+@Suppress("FunctionName")
90
+fun IrcClient(block: IrcClientConfigBuilder.() -> Unit): IrcClient =
91
+        IrcClientImpl(IrcClientConfigBuilder().apply(block).build())
92
+
93
+/**
94
+ * Concrete implementation of an [IrcClient].
86 95
  */
87 96
 // TODO: How should alternative nicknames work?
88 97
 // TODO: Should IRC Client take a pool of servers and rotate through, or make the caller do that?
89 98
 // TODO: Should there be a default profile?
90
-class IrcClientImpl(private val server: Server, override val profile: Profile) : IrcClient, CoroutineScope {
99
+internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, CoroutineScope {
91 100
 
92 101
     private val log by logger()
93 102
 
@@ -98,9 +107,10 @@ class IrcClientImpl(private val server: Server, override val profile: Profile) :
98 107
     @KtorExperimentalAPI
99 108
     internal var socketFactory: (CoroutineScope, String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
100 109
 
101
-    override val serverState = ServerState(profile.initialNick, server.host)
110
+    override val serverState = ServerState(config.profile.nickname, config.server.host, getSaslMechanisms())
102 111
     override val channelState = ChannelStateMap { caseMapping }
103 112
     override val userState = UserState { caseMapping }
113
+    override val hasSaslConfig = config.sasl != null
104 114
 
105 115
     private val messageHandler = MessageHandler(messageProcessors.toList(), eventHandlers.toMutableList())
106 116
 
@@ -110,14 +120,14 @@ class IrcClientImpl(private val server: Server, override val profile: Profile) :
110 120
     private val connecting = AtomicBoolean(false)
111 121
 
112 122
     override fun send(message: String) {
113
-        socket?.sendChannel?.offer(message.toByteArray()) ?: log.warning { "No send channel for message: $message"}
123
+        socket?.sendChannel?.offer(message.toByteArray()) ?: log.warning { "No send channel for message: $message" }
114 124
     }
115 125
 
116 126
     override fun connect() {
117 127
         check(!connecting.getAndSet(true))
118 128
 
119 129
         @Suppress("EXPERIMENTAL_API_USAGE")
120
-        with(socketFactory(this, server.host, server.port, server.tls)) {
130
+        with(socketFactory(this, config.server.host, config.server.port, config.server.useTls)) {
121 131
             // TODO: Proper error handling - what if connect() fails?
122 132
             socket = this
123 133
 
@@ -128,9 +138,8 @@ class IrcClientImpl(private val server: Server, override val profile: Profile) :
128 138
                 emitEvent(ServerConnected(currentTimeProvider()))
129 139
                 sendCapabilityList()
130 140
                 sendPasswordIfPresent()
131
-                sendNickChange(profile.initialNick)
132
-                // TODO: Send correct host
133
-                sendUser(profile.userName, profile.realName)
141
+                sendNickChange(config.profile.nickname)
142
+                sendUser(config.profile.username, config.profile.realName)
134 143
                 messageHandler.processMessages(this@IrcClientImpl, receiveChannel.map { parser.parse(it) })
135 144
                 reset()
136 145
                 emitEvent(ServerDisconnected(currentTimeProvider()))
@@ -152,7 +161,7 @@ class IrcClientImpl(private val server: Server, override val profile: Profile) :
152 161
     }
153 162
 
154 163
     private fun emitEvent(event: IrcEvent) = messageHandler.emitEvent(this, event)
155
-    private fun sendPasswordIfPresent() = server.password?.let(this::sendPassword)
164
+    private fun sendPasswordIfPresent() = config.server.password?.let(this::sendPassword)
156 165
 
157 166
     internal fun reset() {
158 167
         serverState.reset()
@@ -162,4 +171,12 @@ class IrcClientImpl(private val server: Server, override val profile: Profile) :
162 171
         connecting.set(false)
163 172
     }
164 173
 
174
+    private fun getSaslMechanisms(): Collection<SaslMechanism> {
175
+        // TODO: Move this somewhere else
176
+        // TODO: Allow mechanisms to be configured
177
+        config.sasl?.let {
178
+            return listOf(PlainMechanism(it))
179
+        } ?: return emptyList()
180
+    }
181
+
165 182
 }

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

@@ -52,7 +52,7 @@ internal class CapabilitiesHandler : EventHandler {
52 52
             log.info { "Acknowledged capabilities: ${capabilities.keys.map { it.name }.toList()}" }
53 53
             enabledCapabilities.putAll(capabilities)
54 54
 
55
-            if (client.hasCredentials) {
55
+            if (client.hasSaslConfig) {
56 56
                 client.serverState.sasl.getPreferredSaslMechanism(enabledCapabilities[Capability.SaslAuthentication])?.let { mechanism ->
57 57
                     log.info { "Attempting SASL authentication using ${mechanism.ircName}" }
58 58
                     client.serverState.sasl.currentMechanism = mechanism
@@ -100,7 +100,4 @@ internal class CapabilitiesHandler : EventHandler {
100 100
         return if (data.isEmpty()) null else data
101 101
     }
102 102
 
103
-    private val IrcClient.hasCredentials
104
-            get() = profile.authUsername != null && profile.authPassword != null
105
-
106 103
 }

+ 0
- 18
src/main/kotlin/com/dmdirc/ktirc/model/Profile.kt View File

@@ -1,18 +0,0 @@
1
-package com.dmdirc.ktirc.model
2
-
3
-/**
4
- * Describes the client's profile information that will be provided to a server.
5
- *
6
- * @param initialNick The initial nickname to attempt to use
7
- * @param realName The real name to provide to the IRC server
8
- * @param userName The username to use if your system doesn't supply an IDENT response (or the server doesn't ask)
9
- * @param authUsername The username to authenticate over SASL with (e.g. services account)
10
- * @param authPassword The password to authenticate the [authUsername] account with
11
- */
12
-data class Profile(
13
-        val initialNick: String,
14
-        val realName: String,
15
-        val userName: String,
16
-        val authUsername: String? = null,
17
-        val authPassword: String? = null
18
-)

+ 0
- 6
src/main/kotlin/com/dmdirc/ktirc/model/Server.kt View File

@@ -1,6 +0,0 @@
1
-package com.dmdirc.ktirc.model
2
-
3
-/**
4
- * Describes a server to connect to.
5
- */
6
-data class Server(val host: String, val port: Int, val tls: Boolean = false, val password: String? = null)

+ 3
- 4
src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt View File

@@ -2,7 +2,6 @@ package com.dmdirc.ktirc.model
2 2
 
3 3
 import com.dmdirc.ktirc.io.CaseMapping
4 4
 import com.dmdirc.ktirc.sasl.SaslMechanism
5
-import com.dmdirc.ktirc.sasl.supportedSaslMechanisms
6 5
 import com.dmdirc.ktirc.util.logger
7 6
 import kotlin.reflect.KClass
8 7
 
@@ -12,7 +11,7 @@ import kotlin.reflect.KClass
12 11
 class ServerState internal constructor(
13 12
         private val initialNickname: String,
14 13
         private val initialServerName: String,
15
-        saslMechanisms: Collection<SaslMechanism> = supportedSaslMechanisms) {
14
+        saslMechanisms: Collection<SaslMechanism>) {
16 15
 
17 16
     private val log by logger()
18 17
 
@@ -26,7 +25,7 @@ class ServerState internal constructor(
26 25
     /**
27 26
      * What we believe our current nickname to be on the server.
28 27
      *
29
-     * Initially this will be the nickname provided in the [Profile]. It will be updated to the actual nickname
28
+     * Initially this will be the nickname provided in the config. It will be updated to the actual nickname
30 29
      * in use when connecting. Once you have received a [com.dmdirc.ktirc.events.ServerWelcome] event you can
31 30
      * rely on this value being current.
32 31
      * */
@@ -36,7 +35,7 @@ class ServerState internal constructor(
36 35
     /**
37 36
      * The name of the server we are connected to.
38 37
      *
39
-     * Initially this will be the hostname or IP address provided in the [Server]. It will be updated to the server's
38
+     * Initially this will be the hostname or IP address provided in the config. It will be updated to the server's
40 39
      * self-reported hostname when connecting. Once you have received a [com.dmdirc.ktirc.events.ServerWelcome] event
41 40
      * you can rely on this value being current.
42 41
      */

+ 4
- 3
src/main/kotlin/com/dmdirc/ktirc/sasl/Plain.kt View File

@@ -1,16 +1,17 @@
1 1
 package com.dmdirc.ktirc.sasl
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.SaslConfig
4 5
 import com.dmdirc.ktirc.messages.sendAuthenticationMessage
5 6
 
6
-internal class PlainMechanism : SaslMechanism {
7
+internal class PlainMechanism(private val saslConfig: SaslConfig) : SaslMechanism {
7 8
 
8 9
     override val ircName = "PLAIN"
9 10
     override val priority = 0
10 11
 
11 12
     override fun handleAuthenticationEvent(client: IrcClient, data: ByteArray?) {
12
-        with (client.profile) {
13
-            client.sendAuthenticationMessage("$authUsername\u0000$authUsername\u0000$authPassword".toByteArray().toBase64())
13
+        with (saslConfig) {
14
+            client.sendAuthenticationMessage("$username\u0000$username\u0000$password".toByteArray().toBase64())
14 15
         }
15 16
     }
16 17
 

+ 0
- 4
src/main/kotlin/com/dmdirc/ktirc/sasl/SaslMechanism.kt View File

@@ -10,7 +10,3 @@ internal interface SaslMechanism {
10 10
     fun handleAuthenticationEvent(client: IrcClient, data: ByteArray?)
11 11
 
12 12
 }
13
-
14
-internal val supportedSaslMechanisms = listOf<SaslMechanism>(
15
-        PlainMechanism()
16
-)

+ 125
- 0
src/test/kotlin/com/dmdirc/ktirc/DslTest.kt View File

@@ -0,0 +1,125 @@
1
+package com.dmdirc.ktirc
2
+
3
+import org.junit.jupiter.api.Assertions.assertEquals
4
+import org.junit.jupiter.api.Assertions.assertTrue
5
+import org.junit.jupiter.api.Test
6
+import org.junit.jupiter.api.assertThrows
7
+
8
+internal class IrcClientConfigBuilderTest {
9
+
10
+    @Test
11
+    fun `throws if server is defined twice`() {
12
+        assertThrows<IllegalStateException> {
13
+            IrcClientConfigBuilder().apply {
14
+                server { host = "1" }
15
+                server { host = "2" }
16
+            }
17
+        }
18
+    }
19
+
20
+    @Test
21
+    fun `throws if no host is provided`() {
22
+        assertThrows<IllegalStateException> {
23
+            IrcClientConfigBuilder().apply {
24
+                server {}
25
+            }
26
+        }
27
+    }
28
+
29
+    @Test
30
+    fun `throws if profile is defined twice`() {
31
+        assertThrows<IllegalStateException> {
32
+            IrcClientConfigBuilder().apply {
33
+                profile { nickname = "acidBurn" }
34
+                profile { nickname = "zeroCool" }
35
+            }
36
+        }
37
+    }
38
+
39
+    @Test
40
+    fun `throws if no nickname is provided`() {
41
+        assertThrows<IllegalStateException> {
42
+            IrcClientConfigBuilder().apply {
43
+                profile {}
44
+            }
45
+        }
46
+    }
47
+
48
+    @Test
49
+    fun `throws if sasl is defined twice`() {
50
+        assertThrows<IllegalStateException> {
51
+            IrcClientConfigBuilder().apply {
52
+                sasl {}
53
+                sasl {}
54
+            }
55
+        }
56
+    }
57
+
58
+    @Test
59
+    fun `throws if server is not defined`() {
60
+        assertThrows<IllegalStateException> {
61
+            IrcClientConfigBuilder().apply {
62
+                profile { nickname = "acidBurn" }
63
+            }.build()
64
+        }
65
+    }
66
+
67
+    @Test
68
+    fun `throws if profile is not defined`() {
69
+        assertThrows<IllegalStateException> {
70
+            IrcClientConfigBuilder().apply {
71
+                server { host = "thegibson.com" }
72
+            }.build()
73
+        }
74
+    }
75
+
76
+    @Test
77
+    fun `applies server settings`() {
78
+        val config = IrcClientConfigBuilder().apply {
79
+            profile { nickname = "acidBurn" }
80
+            server {
81
+                host = "thegibson.com"
82
+                port = 1337
83
+                password = "h4cktheplan3t"
84
+                useTls = true
85
+            }
86
+        }.build()
87
+
88
+        assertEquals("thegibson.com", config.server.host)
89
+        assertEquals(1337, config.server.port)
90
+        assertEquals("h4cktheplan3t", config.server.password)
91
+        assertTrue(config.server.useTls)
92
+    }
93
+
94
+    @Test
95
+    fun `applies profile settings`() {
96
+        val config = IrcClientConfigBuilder().apply {
97
+            profile {
98
+                nickname = "acidBurn"
99
+                username = "acidB"
100
+                realName = "Kate"
101
+            }
102
+            server { host = "thegibson.com" }
103
+        }.build()
104
+
105
+        assertEquals("acidBurn", config.profile.nickname)
106
+        assertEquals("acidB", config.profile.username)
107
+        assertEquals("Kate", config.profile.realName)
108
+    }
109
+
110
+    @Test
111
+    fun `applies sasl settings`() {
112
+        val config = IrcClientConfigBuilder().apply {
113
+            profile { nickname = "acidBurn" }
114
+            server { host = "thegibson.com" }
115
+            sasl {
116
+                username = "acidBurn"
117
+                password = "h4ckthepl@net"
118
+            }
119
+        }.build()
120
+
121
+        assertEquals("acidBurn", config.sasl?.username)
122
+        assertEquals("h4ckthepl@net", config.sasl?.password)
123
+    }
124
+
125
+}

+ 39
- 18
src/test/kotlin/com/dmdirc/ktirc/IrcClientTest.kt View File

@@ -6,7 +6,9 @@ import com.dmdirc.ktirc.events.ServerConnecting
6 6
 import com.dmdirc.ktirc.events.ServerWelcome
7 7
 import com.dmdirc.ktirc.io.CaseMapping
8 8
 import com.dmdirc.ktirc.io.LineBufferedSocket
9
-import com.dmdirc.ktirc.model.*
9
+import com.dmdirc.ktirc.model.ChannelState
10
+import com.dmdirc.ktirc.model.ServerFeature
11
+import com.dmdirc.ktirc.model.User
10 12
 import com.dmdirc.ktirc.util.currentTimeProvider
11 13
 import com.nhaarman.mockitokotlin2.*
12 14
 import io.ktor.util.KtorExperimentalAPI
@@ -46,6 +48,17 @@ internal class IrcClientImplTest {
46 48
 
47 49
     private val mockEventHandler = mock<(IrcEvent) -> Unit>()
48 50
 
51
+    private val profileConfig = ProfileConfig().apply {
52
+        nickname = NICK
53
+        realName = REAL_NAME
54
+        username = USER_NAME
55
+    }
56
+
57
+    private val normalConfig = IrcClientConfig(ServerConfig().apply {
58
+        host = HOST
59
+        port = PORT
60
+    }, profileConfig, null)
61
+
49 62
     @BeforeEach
50 63
     fun setUp() {
51 64
         currentTimeProvider = { TestConstants.time }
@@ -53,7 +66,7 @@ internal class IrcClientImplTest {
53 66
 
54 67
     @Test
55 68
     fun `uses socket factory to create a new socket on connect`() {
56
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
69
+        val client = IrcClientImpl(normalConfig)
57 70
         client.socketFactory = mockSocketFactory
58 71
         client.connect()
59 72
 
@@ -62,7 +75,11 @@ internal class IrcClientImplTest {
62 75
 
63 76
     @Test
64 77
     fun `uses socket factory to create a new tls on connect`() {
65
-        val client = IrcClientImpl(Server(HOST, PORT, true), Profile(NICK, REAL_NAME, USER_NAME))
78
+        val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
79
+            host = HOST
80
+            port = PORT
81
+            useTls = true
82
+        }, profileConfig, null))
66 83
         client.socketFactory = mockSocketFactory
67 84
         client.connect()
68 85
 
@@ -71,7 +88,7 @@ internal class IrcClientImplTest {
71 88
 
72 89
     @Test
73 90
     fun `throws if socket already exists`() {
74
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
91
+        val client = IrcClientImpl(normalConfig)
75 92
         client.socketFactory = mockSocketFactory
76 93
         client.connect()
77 94
 
@@ -83,7 +100,7 @@ internal class IrcClientImplTest {
83 100
     @Test
84 101
     fun `emits connection events with local time`() = runBlocking {
85 102
         currentTimeProvider = { TestConstants.time }
86
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
103
+        val client = IrcClientImpl(normalConfig)
87 104
         client.socketFactory = mockSocketFactory
88 105
         client.onEvent(mockEventHandler)
89 106
         client.connect()
@@ -100,7 +117,7 @@ internal class IrcClientImplTest {
100 117
 
101 118
     @Test
102 119
     fun `sends basic connection strings`() = runBlocking {
103
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
120
+        val client = IrcClientImpl(normalConfig)
104 121
         client.socketFactory = mockSocketFactory
105 122
         client.connect()
106 123
 
@@ -111,7 +128,11 @@ internal class IrcClientImplTest {
111 128
 
112 129
     @Test
113 130
     fun `sends password first, when present`() = runBlocking {
114
-        val client = IrcClientImpl(Server(HOST, PORT, password = PASSWORD), Profile(NICK, REAL_NAME, USER_NAME))
131
+        val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
132
+            host = HOST
133
+            port = PORT
134
+            password = PASSWORD
135
+        }, profileConfig, null))
115 136
         client.socketFactory = mockSocketFactory
116 137
         client.connect()
117 138
 
@@ -121,7 +142,7 @@ internal class IrcClientImplTest {
121 142
 
122 143
     @Test
123 144
     fun `sends events to provided event handler`() {
124
-        val client = IrcClientImpl(Server(HOST, PORT, password = PASSWORD), Profile(NICK, REAL_NAME, USER_NAME))
145
+        val client = IrcClientImpl(normalConfig)
125 146
         client.socketFactory = mockSocketFactory
126 147
         client.onEvent(mockEventHandler)
127 148
 
@@ -136,14 +157,14 @@ internal class IrcClientImplTest {
136 157
 
137 158
     @Test
138 159
     fun `gets case mapping from server features`() {
139
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
160
+        val client = IrcClientImpl(normalConfig)
140 161
         client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.RfcStrict
141 162
         assertEquals(CaseMapping.RfcStrict, client.caseMapping)
142 163
     }
143 164
 
144 165
     @Test
145 166
     fun `indicates if user is local user or not`() {
146
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
167
+        val client = IrcClientImpl(normalConfig)
147 168
         client.serverState.localNickname = "[acidBurn]"
148 169
 
149 170
         assertTrue(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
@@ -152,7 +173,7 @@ internal class IrcClientImplTest {
152 173
 
153 174
     @Test
154 175
     fun `indicates if nickname is local user or not`() {
155
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
176
+        val client = IrcClientImpl(normalConfig)
156 177
         client.serverState.localNickname = "[acidBurn]"
157 178
 
158 179
         assertTrue(client.isLocalUser("{acidBurn}"))
@@ -161,7 +182,7 @@ internal class IrcClientImplTest {
161 182
 
162 183
     @Test
163 184
     fun `uses current case mapping to check local user`() {
164
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
185
+        val client = IrcClientImpl(normalConfig)
165 186
         client.serverState.localNickname = "[acidBurn]"
166 187
         client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.Ascii
167 188
         assertFalse(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
@@ -169,7 +190,7 @@ internal class IrcClientImplTest {
169 190
 
170 191
     @Test
171 192
     fun `sends text to socket`() = runBlocking {
172
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
193
+        val client = IrcClientImpl(normalConfig)
173 194
         client.socketFactory = mockSocketFactory
174 195
         client.connect()
175 196
 
@@ -189,7 +210,7 @@ internal class IrcClientImplTest {
189 210
 
190 211
     @Test
191 212
     fun `disconnects the socket`() = runBlocking {
192
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
213
+        val client = IrcClientImpl(normalConfig)
193 214
         client.socketFactory = mockSocketFactory
194 215
         client.connect()
195 216
 
@@ -200,7 +221,7 @@ internal class IrcClientImplTest {
200 221
 
201 222
     @Test
202 223
     fun `sends messages in order`() = runBlocking {
203
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
224
+        val client = IrcClientImpl(normalConfig)
204 225
         client.socketFactory = mockSocketFactory
205 226
         client.connect()
206 227
 
@@ -220,19 +241,19 @@ internal class IrcClientImplTest {
220 241
 
221 242
     @Test
222 243
     fun `defaults local nickname to profile`() = runBlocking {
223
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
244
+        val client = IrcClientImpl(normalConfig)
224 245
         assertEquals(NICK, client.serverState.localNickname)
225 246
     }
226 247
 
227 248
     @Test
228 249
     fun `defaults server name to host name`() = runBlocking {
229
-        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
250
+        val client = IrcClientImpl(normalConfig)
230 251
         assertEquals(HOST, client.serverState.serverName)
231 252
     }
232 253
 
233 254
     @Test
234 255
     fun `reset clears all state`() {
235
-        with (IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))) {
256
+        with(IrcClientImpl(normalConfig)) {
236 257
             userState += User("acidBurn")
237 258
             channelState += ChannelState("#thegibson") { CaseMapping.Rfc }
238 259
             serverState.serverName = "root.$HOST"

+ 5
- 9
src/test/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandlerTest.kt View File

@@ -4,7 +4,6 @@ import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.TestConstants
5 5
 import com.dmdirc.ktirc.model.CapabilitiesNegotiationState
6 6
 import com.dmdirc.ktirc.model.Capability
7
-import com.dmdirc.ktirc.model.Profile
8 7
 import com.dmdirc.ktirc.model.ServerState
9 8
 import com.dmdirc.ktirc.sasl.SaslMechanism
10 9
 import com.dmdirc.ktirc.sasl.fromBase64
@@ -32,11 +31,8 @@ internal class CapabilitiesHandlerTest {
32 31
 
33 32
     private val handler = CapabilitiesHandler()
34 33
     private val serverState = ServerState("", "", listOf(saslMech1, saslMech2, saslMech3))
35
-    private val nonSaslProfile = Profile("acidBurn", "Kate Libby", "acidB")
36
-    private val saslProfile = Profile("acidBurn", "Kate Libby", "acidB", "acidB", "HackThePlan3t!")
37 34
     private val ircClient = mock<IrcClient> {
38 35
         on { serverState } doReturn serverState
39
-        on { profile } doReturn nonSaslProfile
40 36
     }
41 37
 
42 38
     @Test
@@ -96,7 +92,7 @@ internal class CapabilitiesHandlerTest {
96 92
 
97 93
     @Test
98 94
     fun `sends END when capabilities acknowledged and no sasl state`() {
99
-        whenever(ircClient.profile).thenReturn(saslProfile)
95
+        whenever(ircClient.hasSaslConfig).thenReturn(true)
100 96
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
101 97
                 Capability.EchoMessages to "",
102 98
                 Capability.HostsInNamesReply to "123"
@@ -107,7 +103,7 @@ internal class CapabilitiesHandlerTest {
107 103
 
108 104
     @Test
109 105
     fun `sends END when capabilities acknowledged and no shared mechanism`() {
110
-        whenever(ircClient.profile).thenReturn(saslProfile)
106
+        whenever(ircClient.hasSaslConfig).thenReturn(true)
111 107
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
112 108
                 Capability.SaslAuthentication to "fake1,fake2",
113 109
                 Capability.HostsInNamesReply to "123"
@@ -118,7 +114,7 @@ internal class CapabilitiesHandlerTest {
118 114
 
119 115
     @Test
120 116
     fun `sends AUTHENTICATE when capabilities acknowledged with shared mechanism`() {
121
-        whenever(ircClient.profile).thenReturn(saslProfile)
117
+        whenever(ircClient.hasSaslConfig).thenReturn(true)
122 118
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
123 119
                 Capability.SaslAuthentication to "mech1,fake2",
124 120
                 Capability.HostsInNamesReply to "123"
@@ -129,7 +125,7 @@ internal class CapabilitiesHandlerTest {
129 125
 
130 126
     @Test
131 127
     fun `sets current SASL mechanism when capabilities acknowledged with shared mechanism`() {
132
-        whenever(ircClient.profile).thenReturn(saslProfile)
128
+        whenever(ircClient.hasSaslConfig).thenReturn(true)
133 129
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
134 130
                 Capability.SaslAuthentication to "mech1,fake2",
135 131
                 Capability.HostsInNamesReply to "123"
@@ -140,7 +136,7 @@ internal class CapabilitiesHandlerTest {
140 136
 
141 137
     @Test
142 138
     fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
143
-        whenever(ircClient.profile).thenReturn(saslProfile)
139
+        whenever(ircClient.hasSaslConfig).thenReturn(true)
144 140
         handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
145 141
                 Capability.SaslAuthentication to "mech1,fake2",
146 142
                 Capability.HostsInNamesReply to "123"

+ 1
- 1
src/test/kotlin/com/dmdirc/ktirc/events/ChannelStateHandlerTest.kt View File

@@ -13,7 +13,7 @@ internal class ChannelStateHandlerTest {
13 13
 
14 14
     private val handler = ChannelStateHandler()
15 15
     private val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
16
-    private val serverState = ServerState("", "")
16
+    private val serverState = ServerState("", "", emptyList())
17 17
     private val ircClient = mock<IrcClient> {
18 18
         on { serverState } doReturn serverState
19 19
         on { channelState } doReturn channelStateMap

+ 1
- 1
src/test/kotlin/com/dmdirc/ktirc/events/EventUtilsTest.kt View File

@@ -16,7 +16,7 @@ import org.junit.jupiter.api.Test
16 16
 
17 17
 internal class EventUtilsTest {
18 18
 
19
-    private val serverState = ServerState("", "")
19
+    private val serverState = ServerState("", "", emptyList())
20 20
     private val ircClient = mock<IrcClient> {
21 21
         on { serverState } doReturn serverState
22 22
         on { caseMapping } doReturn CaseMapping.Ascii

+ 1
- 1
src/test/kotlin/com/dmdirc/ktirc/events/ServerStateHandlerTest.kt View File

@@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test
11 11
 
12 12
 internal class ServerStateHandlerTest {
13 13
 
14
-    private val serverState = ServerState("", "")
14
+    private val serverState = ServerState("", "", emptyList())
15 15
     private val ircClient = mock<IrcClient> {
16 16
         on { serverState } doReturn serverState
17 17
     }

+ 1
- 1
src/test/kotlin/com/dmdirc/ktirc/events/UserStateHandlerTest.kt View File

@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test
14 14
 
15 15
 internal class UserStateHandlerTest {
16 16
 
17
-    private val serverState = ServerState("", "")
17
+    private val serverState = ServerState("", "", emptyList())
18 18
     private val userState = UserState { CaseMapping.Rfc }
19 19
 
20 20
     private val ircClient = mock<IrcClient> {

+ 8
- 8
src/test/kotlin/com/dmdirc/ktirc/model/ServerStateTest.kt View File

@@ -7,25 +7,25 @@ internal class ServerStateTest {
7 7
 
8 8
     @Test
9 9
     fun `ServerState should use the initial nickname as local nickname`() {
10
-        val serverState = ServerState("acidBurn", "")
10
+        val serverState = ServerState("acidBurn", "", emptyList())
11 11
         assertEquals("acidBurn", serverState.localNickname)
12 12
     }
13 13
 
14 14
     @Test
15 15
     fun `ServerState should use the initial name as server name`() {
16
-        val serverState = ServerState("", "the.gibson")
16
+        val serverState = ServerState("", "the.gibson", emptyList())
17 17
         assertEquals("the.gibson", serverState.serverName)
18 18
     }
19 19
 
20 20
     @Test
21 21
     fun `ServerState should default status to disconnected`() {
22
-        val serverState = ServerState("acidBurn", "")
22
+        val serverState = ServerState("acidBurn", "", emptyList())
23 23
         assertEquals(ServerStatus.Disconnected, serverState.status)
24 24
     }
25 25
 
26 26
     @Test
27 27
     fun `returns mode type for known channel mode`() {
28
-        val serverState = ServerState("acidBurn", "")
28
+        val serverState = ServerState("acidBurn", "", emptyList())
29 29
         serverState.features[ServerFeature.ChannelModes] = arrayOf("ab", "cd", "ef", "gh")
30 30
         assertEquals(ChannelModeType.List, serverState.channelModeType('a'))
31 31
         assertEquals(ChannelModeType.SetUnsetParameter, serverState.channelModeType('d'))
@@ -35,7 +35,7 @@ internal class ServerStateTest {
35 35
 
36 36
     @Test
37 37
     fun `returns whether a mode is a channel user mode or not`() {
38
-        val serverState = ServerState("acidBurn", "")
38
+        val serverState = ServerState("acidBurn", "", emptyList())
39 39
         serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("oqv", "@~+")
40 40
         assertTrue(serverState.isChannelUserMode('o'))
41 41
         assertTrue(serverState.isChannelUserMode('q'))
@@ -47,19 +47,19 @@ internal class ServerStateTest {
47 47
 
48 48
     @Test
49 49
     fun `returns NoParameter for unknown channel mode`() {
50
-        val serverState = ServerState("acidBurn", "")
50
+        val serverState = ServerState("acidBurn", "", emptyList())
51 51
         serverState.features[ServerFeature.ChannelModes] = arrayOf("ab", "cd", "ef", "gh")
52 52
         assertEquals(ChannelModeType.NoParameter, serverState.channelModeType('z'))
53 53
     }
54 54
 
55 55
     @Test
56 56
     fun `returns NoParameter for channel modes if feature doesn't exist`() {
57
-        val serverState = ServerState("acidBurn", "")
57
+        val serverState = ServerState("acidBurn", "", emptyList())
58 58
         assertEquals(ChannelModeType.NoParameter, serverState.channelModeType('b'))
59 59
     }
60 60
 
61 61
     @Test
62
-    fun `reset clears all state`() = with(ServerState("acidBurn", "")) {
62
+    fun `reset clears all state`() = with(ServerState("acidBurn", "", emptyList())) {
63 63
         receivedWelcome = true
64 64
         status = ServerStatus.Connecting
65 65
         localNickname = "acidBurn3"

+ 5
- 4
src/test/kotlin/com/dmdirc/ktirc/sasl/PlainMechanismTest.kt View File

@@ -1,7 +1,7 @@
1 1
 package com.dmdirc.ktirc.sasl
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4
-import com.dmdirc.ktirc.model.Profile
4
+import com.dmdirc.ktirc.SaslConfig
5 5
 import com.dmdirc.ktirc.model.ServerState
6 6
 import com.nhaarman.mockitokotlin2.argumentCaptor
7 7
 import com.nhaarman.mockitokotlin2.doReturn
@@ -13,13 +13,14 @@ import org.junit.jupiter.api.Test
13 13
 internal class PlainMechanismTest {
14 14
 
15 15
     private val serverState = ServerState("", "", emptyList())
16
-    private val saslProfile = Profile("acidBurn", "Kate Libby", "acidB", "acidB", "HackThePlan3t!")
17 16
     private val ircClient = mock<IrcClient> {
18 17
         on { serverState } doReturn serverState
19
-        on { profile } doReturn saslProfile
20 18
     }
21 19
 
22
-    private val mechanism = PlainMechanism()
20
+    private val mechanism = PlainMechanism(SaslConfig().apply {
21
+        username = "acidB"
22
+        password = "HackThePlan3t!"
23
+    })
23 24
 
24 25
     @Test
25 26
     fun `sends encoded username and password when first message received`() {

Loading…
Cancel
Save