Browse Source

SASL support!

Closes #2
tags/v0.6.0
Chris Smith 5 years ago
parent
commit
2ba511702b
24 changed files with 659 additions and 81 deletions
  1. 3
    0
      CHANGELOG
  2. 7
    0
      README.md
  3. 3
    2
      src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt
  4. 54
    3
      src/main/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandler.kt
  5. 9
    0
      src/main/kotlin/com/dmdirc/ktirc/events/Events.kt
  6. 21
    0
      src/main/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessor.kt
  7. 4
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/MessageBuilders.kt
  8. 1
    0
      src/main/kotlin/com/dmdirc/ktirc/messages/MessageProcessor.kt
  9. 4
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/NumericConstants.kt
  10. 14
    0
      src/main/kotlin/com/dmdirc/ktirc/model/CapabilitiesState.kt
  11. 16
    2
      src/main/kotlin/com/dmdirc/ktirc/model/Profile.kt
  12. 25
    0
      src/main/kotlin/com/dmdirc/ktirc/model/SaslState.kt
  13. 9
    1
      src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt
  14. 6
    0
      src/main/kotlin/com/dmdirc/ktirc/sasl/Base64.kt
  15. 17
    0
      src/main/kotlin/com/dmdirc/ktirc/sasl/Plain.kt
  16. 16
    0
      src/main/kotlin/com/dmdirc/ktirc/sasl/SaslMechanism.kt
  17. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/IrcClientTest.kt
  18. 221
    66
      src/test/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandlerTest.kt
  19. 72
    0
      src/test/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessorTest.kt
  20. 15
    3
      src/test/kotlin/com/dmdirc/ktirc/messages/MessageBuildersTest.kt
  21. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/model/CapabilitiesStateTest.kt
  22. 81
    0
      src/test/kotlin/com/dmdirc/ktirc/model/SaslStateTest.kt
  23. 39
    0
      src/test/kotlin/com/dmdirc/ktirc/sasl/PlainMechanismTest.kt
  24. 20
    0
      src/test/kotlin/com/dmdirc/ktirc/util/Base64Test.kt

+ 3
- 0
CHANGELOG View File

@@ -1,5 +1,8 @@
1 1
 vNEXT (in development)
2 2
 
3
+ * Changed USER command to not send the server name, per modern standards
4
+ * Added support for SASL authentication (with PLAIN mechanism)
5
+
3 6
 v0.5.0
4 7
 
5 8
  * Server state:

+ 7
- 0
README.md View File

@@ -86,6 +86,13 @@ handlers may themselves raise events. This is useful for higher-order
86 86
 events such as `ServerReady` that depend on a variety of factors and
87 87
 states.
88 88
 
89
+Handlers themselves may not keep state, as they will be shared across
90
+multiple instances of `IrcClient` and won't be reset on reconnection.
91
+State is instead stored in the various `*State` properties of the
92
+`IrcClient` such as `serverState` and `channelState`. Fields that
93
+should not be exposed to users of KtIrc can be placed in these
94
+public state objects but marked as `internal`.
95
+
89 96
 All the generated events (from processors or from event handlers) are
90 97
 passed to the `IrcClient`, which in turn passes them to the library
91 98
 user via the delegates passed to the `onEvent` method. 

+ 3
- 2
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt View File

@@ -20,6 +20,7 @@ interface IrcClient {
20 20
     val serverState: ServerState
21 21
     val channelState: ChannelStateMap
22 22
     val userState: UserState
23
+    val profile: Profile
23 24
 
24 25
     val caseMapping: CaseMapping
25 26
         get() = serverState.features[ServerFeature.ServerCaseMapping] ?: CaseMapping.Rfc
@@ -87,7 +88,7 @@ interface IrcClient {
87 88
 // TODO: How should alternative nicknames work?
88 89
 // TODO: Should IRC Client take a pool of servers and rotate through, or make the caller do that?
89 90
 // TODO: Should there be a default profile?
90
-class IrcClientImpl(private val server: Server, private val profile: Profile) : IrcClient {
91
+class IrcClientImpl(private val server: Server, override val profile: Profile) : IrcClient {
91 92
 
92 93
     internal var socketFactory: (String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
93 94
 
@@ -135,7 +136,7 @@ class IrcClientImpl(private val server: Server, private val profile: Profile) :
135 136
                 sendPasswordIfPresent()
136 137
                 sendNickChange(profile.initialNick)
137 138
                 // TODO: Send correct host
138
-                sendUser(profile.userName, "localhost", server.host, profile.realName)
139
+                sendUser(profile.userName, profile.realName)
139 140
                 messageHandler.processMessages(this@IrcClientImpl, readLines(scope).map { parser.parse(it) })
140 141
                 emitEvent(ServerDisconnected(currentTimeProvider()))
141 142
             }

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

@@ -1,11 +1,13 @@
1 1
 package com.dmdirc.ktirc.events
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.messages.sendAuthenticationMessage
4 5
 import com.dmdirc.ktirc.messages.sendCapabilityEnd
5 6
 import com.dmdirc.ktirc.messages.sendCapabilityRequest
6 7
 import com.dmdirc.ktirc.model.CapabilitiesNegotiationState
7 8
 import com.dmdirc.ktirc.model.CapabilitiesState
8 9
 import com.dmdirc.ktirc.model.Capability
10
+import com.dmdirc.ktirc.sasl.fromBase64
9 11
 import com.dmdirc.ktirc.util.logger
10 12
 
11 13
 internal class CapabilitiesHandler : EventHandler {
@@ -17,6 +19,8 @@ internal class CapabilitiesHandler : EventHandler {
17 19
             is ServerCapabilitiesReceived -> handleCapabilitiesReceived(client.serverState.capabilities, event.capabilities)
18 20
             is ServerCapabilitiesFinished -> handleCapabilitiesFinished(client)
19 21
             is ServerCapabilitiesAcknowledged -> handleCapabilitiesAcknowledged(client, event.capabilities)
22
+            is AuthenticationMessage -> handleAuthenticationMessage(client, event.argument)
23
+            is SaslFinished -> handleSaslFinished(client)
20 24
         }
21 25
         return emptyList()
22 26
     }
@@ -27,7 +31,7 @@ internal class CapabilitiesHandler : EventHandler {
27 31
 
28 32
     private fun handleCapabilitiesFinished(client: IrcClient) {
29 33
         // TODO: We probably need to split the outgoing REQ lines if there are lots of caps
30
-        // TODO: For caps with values we'll need to decide which value to use/whether to enable them/etc
34
+        // TODO: For caps with values we may need to decide which value to use/whether to enable them/etc
31 35
         with (client.serverState.capabilities) {
32 36
             if (advertisedCapabilities.keys.isEmpty()) {
33 37
                 negotiationState = CapabilitiesNegotiationState.FINISHED
@@ -46,10 +50,57 @@ internal class CapabilitiesHandler : EventHandler {
46 50
         // TODO: Check if everything we wanted is enabled
47 51
         with (client.serverState.capabilities) {
48 52
             log.info { "Acknowledged capabilities: ${capabilities.keys.map { it.name }.toList()}" }
49
-            negotiationState = CapabilitiesNegotiationState.FINISHED
50 53
             enabledCapabilities.putAll(capabilities)
51
-            client.sendCapabilityEnd()
54
+
55
+            if (client.hasCredentials) {
56
+                client.serverState.sasl.getPreferredSaslMechanism(enabledCapabilities[Capability.SaslAuthentication])?.let { mechanism ->
57
+                    log.info { "Attempting SASL authentication using ${mechanism.ircName}" }
58
+                    client.serverState.sasl.currentMechanism = mechanism
59
+                    negotiationState = CapabilitiesNegotiationState.AUTHENTICATING
60
+                    client.sendAuthenticationMessage(mechanism.ircName)
61
+                    return
62
+                }
63
+                log.warning { "User supplied credentials but we couldn't negotiate a SASL mechanism with the server" }
64
+            }
65
+
66
+            client.endNegotiation()
67
+        }
68
+    }
69
+
70
+    private fun handleAuthenticationMessage(client: IrcClient, argument: String?) {
71
+        if (argument?.length == 400) {
72
+            client.serverState.sasl.saslBuffer += argument
73
+            return
74
+        }
75
+
76
+        client.serverState.sasl.currentMechanism?.let {
77
+            it.handleAuthenticationEvent(client, client.getStoredSaslBuffer(argument)?.fromBase64())
78
+        } ?: run {
79
+            client.sendAuthenticationMessage("*")
52 80
         }
53 81
     }
54 82
 
83
+    private fun handleSaslFinished(client: IrcClient) = with (client) {
84
+        with (serverState.sasl) {
85
+            saslBuffer = ""
86
+            mechanismState = null
87
+            currentMechanism = null
88
+        }
89
+        endNegotiation()
90
+    }
91
+
92
+    private fun IrcClient.endNegotiation() {
93
+        serverState.capabilities.negotiationState = CapabilitiesNegotiationState.FINISHED
94
+        sendCapabilityEnd()
95
+    }
96
+
97
+    private fun IrcClient.getStoredSaslBuffer(argument: String?): String? {
98
+        val data = serverState.sasl.saslBuffer + (argument ?: "")
99
+        serverState.sasl.saslBuffer = ""
100
+        return if (data.isEmpty()) null else data
101
+    }
102
+
103
+    private val IrcClient.hasCredentials
104
+            get() = profile.authUsername != null && profile.authPassword != null
105
+
55 106
 }

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

@@ -96,3 +96,12 @@ class MotdFinished(time: LocalDateTime, val missing: Boolean = false): IrcEvent(
96 96
  * state.
97 97
  */
98 98
 class ModeChanged(time: LocalDateTime, val target: String, val modes: String, val arguments: Array<String>, val discovered: Boolean = false): IrcEvent(time)
99
+
100
+/** Raised when an AUTHENTICATION message is received. [argument] is `null` if the server sent an empty reply ("+") */
101
+class AuthenticationMessage(time: LocalDateTime, val argument: String?): IrcEvent(time)
102
+
103
+/** Raised when a SASL attempt finishes, successfully or otherwise. */
104
+class SaslFinished(time: LocalDateTime, var success: Boolean) : IrcEvent(time)
105
+
106
+/** Raised when the server says our SASL mechanism isn't available, but gives us a list of others. */
107
+class SaslMechanismNotAvailableError(time: LocalDateTime, var mechanisms: Array<String>) : IrcEvent(time)

+ 21
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessor.kt View File

@@ -0,0 +1,21 @@
1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.events.AuthenticationMessage
4
+import com.dmdirc.ktirc.events.SaslFinished
5
+import com.dmdirc.ktirc.model.IrcMessage
6
+
7
+internal class AuthenticationProcessor : MessageProcessor {
8
+
9
+    override val commands = arrayOf("AUTHENTICATE", RPL_SASLSUCCESS, ERR_SASLFAIL)
10
+
11
+    override fun process(message: IrcMessage) = when(message.command) {
12
+        "AUTHENTICATE" -> listOf(AuthenticationMessage(message.time, message.authenticateArgument))
13
+        RPL_SASLSUCCESS -> listOf(SaslFinished(message.time, true))
14
+        ERR_SASLFAIL -> listOf(SaslFinished(message.time, false))
15
+        else -> emptyList()
16
+    }
17
+
18
+    private val IrcMessage.authenticateArgument: String?
19
+        get() = if (params.isEmpty() || params[0].size == 1 && String(params[0]) == "+") null else String(params[0])
20
+
21
+}

+ 4
- 1
src/main/kotlin/com/dmdirc/ktirc/messages/MessageBuilders.kt View File

@@ -34,4 +34,7 @@ fun IrcClient.sendAction(target: String, action: String) = sendCtcp(target, "ACT
34 34
 fun IrcClient.sendMessage(target: String, message: String) = send("PRIVMSG $target :$message")
35 35
 
36 36
 /** Sends a message to register a user with the server. */
37
-internal fun IrcClient.sendUser(userName: String, localHostName: String, serverHostName: String, realName: String) = send("USER $userName $localHostName $serverHostName :$realName")
37
+internal fun IrcClient.sendUser(userName: String, realName: String) = send("USER $userName 0 * :$realName")
38
+
39
+/** Starts an authentication request. */
40
+internal fun IrcClient.sendAuthenticationMessage(data: String = "+") =send("AUTHENTICATE $data")

+ 1
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/MessageProcessor.kt View File

@@ -19,6 +19,7 @@ internal interface MessageProcessor {
19 19
 
20 20
 internal val messageProcessors = setOf(
21 21
         AccountProcessor(),
22
+        AuthenticationProcessor(),
22 23
         CapabilityProcessor(),
23 24
         ISupportProcessor(),
24 25
         JoinProcessor(),

+ 4
- 1
src/main/kotlin/com/dmdirc/ktirc/messages/NumericConstants.kt View File

@@ -10,4 +10,7 @@ internal const val RPL_UMODEIS = "221"
10 10
 internal const val RPL_CHANNELMODEIS = "324"
11 11
 internal const val RPL_ENDOFMOTD = "376"
12 12
 
13
-internal const val ERR_NOMOTD = "422"
13
+internal const val ERR_NOMOTD = "422"
14
+
15
+internal const val RPL_SASLSUCCESS = "903"
16
+internal const val ERR_SASLFAIL = "904"

+ 14
- 0
src/main/kotlin/com/dmdirc/ktirc/model/CapabilitiesState.kt View File

@@ -32,6 +32,11 @@ enum class CapabilitiesNegotiationState {
32 32
      */
33 33
     AWAITING_ACK,
34 34
 
35
+    /**
36
+     * We are attempting to authenticate with SASL.
37
+     */
38
+    AUTHENTICATING,
39
+
35 40
     /**
36 41
      * Negotiation has completed.
37 42
      */
@@ -44,17 +49,24 @@ enum class CapabilitiesNegotiationState {
44 49
  */
45 50
 @Suppress("unused")
46 51
 sealed class Capability(val name: String) {
52
+    // Capabilities that introduce extra commands:
53
+    /** Allows authentication using SASL via the AUTHENTICATE command. */
54
+    object SaslAuthentication : Capability("sasl")
55
+
47 56
     // Capabilities that enable more information in message tags:
48 57
     /** Messages are tagged with the server time they originated at. */
49 58
     object ServerTimeMessageTag : Capability("server-time")
59
+
50 60
     /** Messages are tagged with the sender's account name. */
51 61
     object UserAccountMessageTag : Capability("account-tag")
52 62
 
53 63
     // Capabilities that extend existing commands to supply extra information:
54 64
     /** Hosts are included for users in NAMES messages. */
55 65
     object HostsInNamesReply : Capability("userhost-in-names")
66
+
56 67
     /** Multiple mode prefixes are returned per-user in NAMES messages. */
57 68
     object MultipleUserModePrefixes : Capability("multi-prefix")
69
+
58 70
     /** The user's account and real name are provided when they join a channel. */
59 71
     object AccountAndRealNameInJoinMessages : Capability("extended-join")
60 72
 
@@ -65,8 +77,10 @@ sealed class Capability(val name: String) {
65 77
     // Capabilities that notify us of changes to other clients:
66 78
     /** Receive a notification when a user's account changes. */
67 79
     object AccountChangeMessages : Capability("account-notify") // TODO: Add processor
80
+
68 81
     /** Receive a notification when a user's away state changes. */
69 82
     object AwayStateMessages : Capability("away-notify") // TODO: Add processor
83
+
70 84
     /** Receive a notification when a user's host changes, instead of a quit/join. */
71 85
     object HostChangeMessages : Capability("chghost") // TODO: Add processor
72 86
 

+ 16
- 2
src/main/kotlin/com/dmdirc/ktirc/model/Profile.kt View File

@@ -1,4 +1,18 @@
1 1
 package com.dmdirc.ktirc.model
2 2
 
3
-/** Describes the client's profile information that will be provided to a server. */
4
-data class Profile(val initialNick: String, val realName: String, val userName: String)
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
+)

+ 25
- 0
src/main/kotlin/com/dmdirc/ktirc/model/SaslState.kt View File

@@ -0,0 +1,25 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+import com.dmdirc.ktirc.sasl.SaslMechanism
4
+
5
+internal class SaslState(private val mechanisms: Collection<SaslMechanism>) {
6
+
7
+    var saslBuffer: String = ""
8
+    var currentMechanism: SaslMechanism? = null
9
+        set(value) {
10
+            mechanismState = null
11
+            field = value
12
+        }
13
+
14
+    var mechanismState: Any? = null
15
+
16
+    fun getPreferredSaslMechanism(serverMechanisms: String?): SaslMechanism? {
17
+        serverMechanisms ?: return null
18
+        val serverSupported = serverMechanisms.split(',')
19
+        return mechanisms
20
+                .filter { it.priority < currentMechanism?.priority ?: Int.MAX_VALUE }
21
+                .filter { serverMechanisms.isEmpty() || it.ircName in serverSupported }
22
+                .maxBy { it.priority }
23
+    }
24
+
25
+}

+ 9
- 1
src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt View File

@@ -1,13 +1,18 @@
1 1
 package com.dmdirc.ktirc.model
2 2
 
3 3
 import com.dmdirc.ktirc.io.CaseMapping
4
+import com.dmdirc.ktirc.sasl.SaslMechanism
5
+import com.dmdirc.ktirc.sasl.supportedSaslMechanisms
4 6
 import com.dmdirc.ktirc.util.logger
5 7
 import kotlin.reflect.KClass
6 8
 
7 9
 /**
8 10
  * Contains the current state of a single IRC server.
9 11
  */
10
-class ServerState internal constructor(initialNickname: String, initialServerName: String) {
12
+class ServerState internal constructor(
13
+        initialNickname: String,
14
+        initialServerName: String,
15
+        saslMechanisms: Collection<SaslMechanism> = supportedSaslMechanisms) {
11 16
 
12 17
     private val log by logger()
13 18
 
@@ -44,6 +49,9 @@ class ServerState internal constructor(initialNickname: String, initialServerNam
44 49
     /** The capabilities we have negotiated with the server (from IRCv3). */
45 50
     val capabilities = CapabilitiesState()
46 51
 
52
+    /** The current state of SASL authentication. */
53
+    internal val sasl = SaslState(saslMechanisms)
54
+
47 55
     /**
48 56
      * Determines what type of channel mode the given character is, based on the server features.
49 57
      *

+ 6
- 0
src/main/kotlin/com/dmdirc/ktirc/sasl/Base64.kt View File

@@ -0,0 +1,6 @@
1
+package com.dmdirc.ktirc.sasl
2
+
3
+import java.util.*
4
+
5
+internal fun ByteArray.toBase64() = String(Base64.getEncoder().encode(this))
6
+internal fun String.fromBase64() = Base64.getDecoder().decode(this)

+ 17
- 0
src/main/kotlin/com/dmdirc/ktirc/sasl/Plain.kt View File

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

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

@@ -0,0 +1,16 @@
1
+package com.dmdirc.ktirc.sasl
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+
5
+internal interface SaslMechanism {
6
+
7
+    val ircName: String
8
+    val priority: Int
9
+
10
+    fun handleAuthenticationEvent(client: IrcClient, data: ByteArray?)
11
+
12
+}
13
+
14
+internal val supportedSaslMechanisms = listOf<SaslMechanism>(
15
+        PlainMechanism()
16
+)

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

@@ -117,7 +117,7 @@ internal class IrcClientImplTest {
117 117
 
118 118
         assertEquals("CAP LS 302", String(client.writeChannel!!.receive()))
119 119
         assertEquals("NICK :$NICK", String(client.writeChannel!!.receive()))
120
-        assertEquals("USER $USER_NAME localhost $HOST :$REAL_NAME", String(client.writeChannel!!.receive()))
120
+        assertEquals("USER $USER_NAME 0 * :$REAL_NAME", String(client.writeChannel!!.receive()))
121 121
     }
122 122
 
123 123
     @Test

+ 221
- 66
src/test/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandlerTest.kt View File

@@ -4,114 +4,269 @@ 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
7 8
 import com.dmdirc.ktirc.model.ServerState
8
-import com.nhaarman.mockitokotlin2.argThat
9
-import com.nhaarman.mockitokotlin2.doReturn
10
-import com.nhaarman.mockitokotlin2.mock
11
-import com.nhaarman.mockitokotlin2.verify
12
-import kotlinx.coroutines.runBlocking
13
-import org.junit.jupiter.api.Assertions.assertEquals
9
+import com.dmdirc.ktirc.sasl.SaslMechanism
10
+import com.dmdirc.ktirc.sasl.fromBase64
11
+import com.dmdirc.ktirc.sasl.toBase64
12
+import com.nhaarman.mockitokotlin2.*
13
+import org.junit.jupiter.api.Assertions.*
14 14
 import org.junit.jupiter.api.Test
15 15
 
16 16
 internal class CapabilitiesHandlerTest {
17 17
 
18
+    private val saslMech1 = mock<SaslMechanism> {
19
+        on { priority } doReturn 1
20
+        on { ircName } doReturn "mech1"
21
+    }
22
+
23
+    private val saslMech2 = mock<SaslMechanism> {
24
+        on { priority } doReturn 2
25
+        on { ircName } doReturn "mech2"
26
+    }
27
+
28
+    private val saslMech3 = mock<SaslMechanism> {
29
+        on { priority } doReturn 3
30
+        on { ircName } doReturn "mech3"
31
+    }
32
+
18 33
     private val handler = CapabilitiesHandler()
19
-    private val serverState = ServerState("", "")
34
+    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!")
20 37
     private val ircClient = mock<IrcClient> {
21 38
         on { serverState } doReturn serverState
39
+        on { profile } doReturn nonSaslProfile
22 40
     }
23 41
 
24 42
     @Test
25
-    fun `CapabilitiesHandler adds new capabilities to the state`() {
26
-        runBlocking {
27
-            handler.processEvent(ircClient, ServerCapabilitiesReceived(TestConstants.time, hashMapOf(
28
-                    Capability.EchoMessages to "",
29
-                    Capability.HostsInNamesReply to "123"
30
-            )))
43
+    fun `adds new capabilities to the state`() {
44
+        handler.processEvent(ircClient, ServerCapabilitiesReceived(TestConstants.time, hashMapOf(
45
+                Capability.EchoMessages to "",
46
+                Capability.HostsInNamesReply to "123"
47
+        )))
31 48
 
32
-            assertEquals(2, serverState.capabilities.advertisedCapabilities.size)
33
-            assertEquals("", serverState.capabilities.advertisedCapabilities[Capability.EchoMessages])
34
-            assertEquals("123", serverState.capabilities.advertisedCapabilities[Capability.HostsInNamesReply])
35
-        }
49
+        assertEquals(2, serverState.capabilities.advertisedCapabilities.size)
50
+        assertEquals("", serverState.capabilities.advertisedCapabilities[Capability.EchoMessages])
51
+        assertEquals("123", serverState.capabilities.advertisedCapabilities[Capability.HostsInNamesReply])
36 52
     }
37 53
 
38 54
     @Test
39
-    fun `CapabilitiesHandler updates negotiation state when capabilities finished`() {
40
-        runBlocking {
41
-            serverState.capabilities.advertisedCapabilities[Capability.EchoMessages] = ""
55
+    fun `updates negotiation state when capabilities finished`() {
56
+        serverState.capabilities.advertisedCapabilities[Capability.EchoMessages] = ""
42 57
 
43
-            handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
58
+        handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
44 59
 
45
-            assertEquals(CapabilitiesNegotiationState.AWAITING_ACK, serverState.capabilities.negotiationState)
46
-        }
60
+        assertEquals(CapabilitiesNegotiationState.AWAITING_ACK, serverState.capabilities.negotiationState)
47 61
     }
48 62
 
49 63
     @Test
50
-    fun `CapabilitiesHandler sends REQ when capabilities received`() {
51
-        runBlocking {
52
-            serverState.capabilities.advertisedCapabilities[Capability.EchoMessages] = ""
53
-            serverState.capabilities.advertisedCapabilities[Capability.AccountChangeMessages] = ""
64
+    fun `sends REQ when capabilities received`() {
65
+        serverState.capabilities.advertisedCapabilities[Capability.EchoMessages] = ""
66
+        serverState.capabilities.advertisedCapabilities[Capability.AccountChangeMessages] = ""
54 67
 
55
-            handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
68
+        handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
56 69
 
57
-            verify(ircClient).send(argThat { equals("CAP REQ :echo-message account-notify") || equals("CAP REQ :account-notify echo-message") })
58
-        }
70
+        verify(ircClient).send(argThat { equals("CAP REQ :echo-message account-notify") || equals("CAP REQ :account-notify echo-message") })
59 71
     }
60 72
 
61 73
     @Test
62
-    fun `CapabilitiesHandler sends END when blank capabilities received`() {
63
-        runBlocking {
64
-            handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
74
+    fun `sends END when blank capabilities received`() {
75
+        handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
65 76
 
66
-            verify(ircClient).send("CAP END")
67
-        }
77
+        verify(ircClient).send("CAP END")
68 78
     }
69 79
 
70 80
     @Test
71
-    fun `CapabilitiesHandler updates negotiation when blank capabilities received`() {
72
-        runBlocking {
73
-            handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
81
+    fun `updates negotiation when blank capabilities received`() {
82
+        handler.processEvent(ircClient, ServerCapabilitiesFinished(TestConstants.time))
74 83
 
75
-            assertEquals(CapabilitiesNegotiationState.FINISHED, serverState.capabilities.negotiationState)
76
-        }
84
+        assertEquals(CapabilitiesNegotiationState.FINISHED, serverState.capabilities.negotiationState)
77 85
     }
78 86
 
79 87
     @Test
80
-    fun `CapabilitiesHandler sends END when capabilities acknowledged`() {
81
-        runBlocking {
82
-            handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
83
-                    Capability.EchoMessages to "",
84
-                    Capability.HostsInNamesReply to "123"
85
-            )))
88
+    fun `sends END when capabilities acknowledged and no profile`() {
89
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
90
+                Capability.EchoMessages to "",
91
+                Capability.HostsInNamesReply to "123"
92
+        )))
86 93
 
87
-            verify(ircClient).send("CAP END")
88
-        }
94
+        verify(ircClient).send("CAP END")
89 95
     }
90 96
 
91 97
     @Test
92
-    fun `CapabilitiesHandler updates negotiation state when capabilities acknowledged`() {
93
-        runBlocking {
94
-            handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
95
-                    Capability.EchoMessages to "",
96
-                    Capability.HostsInNamesReply to "123"
97
-            )))
98
+    fun `sends END when capabilities acknowledged and no sasl state`() {
99
+        whenever(ircClient.profile).thenReturn(saslProfile)
100
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
101
+                Capability.EchoMessages to "",
102
+                Capability.HostsInNamesReply to "123"
103
+        )))
98 104
 
99
-            assertEquals(CapabilitiesNegotiationState.FINISHED, serverState.capabilities.negotiationState)
100
-        }
105
+        verify(ircClient).send("CAP END")
106
+    }
107
+
108
+    @Test
109
+    fun `sends END when capabilities acknowledged and no shared mechanism`() {
110
+        whenever(ircClient.profile).thenReturn(saslProfile)
111
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
112
+                Capability.SaslAuthentication to "fake1,fake2",
113
+                Capability.HostsInNamesReply to "123"
114
+        )))
115
+
116
+        verify(ircClient).send("CAP END")
117
+    }
118
+
119
+    @Test
120
+    fun `sends AUTHENTICATE when capabilities acknowledged with shared mechanism`() {
121
+        whenever(ircClient.profile).thenReturn(saslProfile)
122
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
123
+                Capability.SaslAuthentication to "mech1,fake2",
124
+                Capability.HostsInNamesReply to "123"
125
+        )))
126
+
127
+        verify(ircClient).send("AUTHENTICATE mech1")
128
+    }
129
+
130
+    @Test
131
+    fun `sets current SASL mechanism when capabilities acknowledged with shared mechanism`() {
132
+        whenever(ircClient.profile).thenReturn(saslProfile)
133
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
134
+                Capability.SaslAuthentication to "mech1,fake2",
135
+                Capability.HostsInNamesReply to "123"
136
+        )))
137
+
138
+        assertSame(saslMech1, serverState.sasl.currentMechanism)
139
+    }
140
+
141
+    @Test
142
+    fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
143
+        whenever(ircClient.profile).thenReturn(saslProfile)
144
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
145
+                Capability.SaslAuthentication to "mech1,fake2",
146
+                Capability.HostsInNamesReply to "123"
147
+        )))
148
+
149
+        assertEquals(CapabilitiesNegotiationState.AUTHENTICATING, serverState.capabilities.negotiationState)
150
+    }
151
+
152
+    @Test
153
+    fun `updates negotiation state when capabilities acknowledged`() {
154
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
155
+                Capability.EchoMessages to "",
156
+                Capability.HostsInNamesReply to "123"
157
+        )))
158
+
159
+        assertEquals(CapabilitiesNegotiationState.FINISHED, serverState.capabilities.negotiationState)
160
+    }
161
+
162
+    @Test
163
+    fun `stores enabled caps when capabilities acknowledged`() {
164
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
165
+                Capability.EchoMessages to "",
166
+                Capability.HostsInNamesReply to "123"
167
+        )))
168
+
169
+        assertEquals(2, serverState.capabilities.enabledCapabilities.size)
170
+        assertEquals("", serverState.capabilities.enabledCapabilities[Capability.EchoMessages])
171
+        assertEquals("123", serverState.capabilities.enabledCapabilities[Capability.HostsInNamesReply])
172
+    }
173
+
174
+    @Test
175
+    fun `aborts authentication attempt if not expecting one`() {
176
+        serverState.sasl.currentMechanism = null
177
+        handler.processEvent(ircClient, AuthenticationMessage(TestConstants.time, "+"))
178
+
179
+        verify(ircClient).send("AUTHENTICATE *")
180
+    }
181
+
182
+    @Test
183
+    fun `passes authentication message to mechanism if in auth process`() {
184
+        serverState.sasl.currentMechanism = saslMech1
185
+
186
+        val argument = "ABC"
187
+        handler.processEvent(ircClient, AuthenticationMessage(TestConstants.time, argument))
188
+
189
+        verify(saslMech1).handleAuthenticationEvent(ircClient, argument.fromBase64())
190
+    }
191
+
192
+    @Test
193
+    fun `stores partial authentication message if it's 400 bytes long`() {
194
+        serverState.sasl.currentMechanism = saslMech1
195
+
196
+        val argument = "A".repeat(400)
197
+        handler.processEvent(ircClient, AuthenticationMessage(TestConstants.time, argument))
198
+
199
+        assertEquals(argument, serverState.sasl.saslBuffer)
200
+        verify(saslMech1, never()).handleAuthenticationEvent(any(), any())
201
+    }
202
+
203
+    @Test
204
+    fun `appends authentication messages if it's 400 bytes long and data already exists`() {
205
+        serverState.sasl.currentMechanism = saslMech1
206
+
207
+        serverState.sasl.saslBuffer = "A".repeat(400)
208
+        handler.processEvent(ircClient, AuthenticationMessage(TestConstants.time, "B".repeat(400)))
209
+
210
+        assertEquals("A".repeat(400) + "B".repeat(400), serverState.sasl.saslBuffer)
211
+        verify(saslMech1, never()).handleAuthenticationEvent(any(), any())
101 212
     }
102 213
 
103 214
     @Test
104
-    fun `CapabilitiesHandler stores enabled caps when capabilities acknowledged`() {
105
-        runBlocking {
106
-            handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
107
-                    Capability.EchoMessages to "",
108
-                    Capability.HostsInNamesReply to "123"
109
-            )))
215
+    fun `reconstructs partial authentication message to mechanism if data stored and partial received`() {
216
+        serverState.sasl.currentMechanism = saslMech1
217
+
218
+        serverState.sasl.saslBuffer = "A".repeat(400)
219
+
220
+        val argument = "ABCD"
221
+        handler.processEvent(ircClient, AuthenticationMessage(TestConstants.time, argument))
222
+
223
+        val captor = argumentCaptor<ByteArray>()
224
+        verify(saslMech1).handleAuthenticationEvent(same(ircClient), captor.capture())
225
+        assertEquals("A".repeat(400) + "ABCD", captor.firstValue.toBase64())
226
+    }
227
+
228
+    @Test
229
+    fun `reconstructs partial authentication message to mechanism if data stored and null received`() {
230
+        serverState.sasl.currentMechanism = saslMech1
231
+
232
+        serverState.sasl.saslBuffer = "A".repeat(400)
233
+
234
+        handler.processEvent(ircClient, AuthenticationMessage(TestConstants.time, null))
235
+
236
+        val captor = argumentCaptor<ByteArray>()
237
+        verify(saslMech1).handleAuthenticationEvent(same(ircClient), captor.capture())
238
+        assertEquals("A".repeat(400), captor.firstValue.toBase64())
239
+    }
240
+
241
+    @Test
242
+    fun `sends END when SASL auth finished`() {
243
+        handler.processEvent(ircClient, SaslFinished(TestConstants.time, true))
244
+
245
+        verify(ircClient).send("CAP END")
246
+    }
247
+
248
+    @Test
249
+    fun `sets negotiation state when SASL auth finished`() {
250
+        handler.processEvent(ircClient, SaslFinished(TestConstants.time, true))
251
+
252
+        assertEquals(CapabilitiesNegotiationState.FINISHED, serverState.capabilities.negotiationState)
253
+    }
254
+
255
+    @Test
256
+    fun `resets SASL state when SASL auth finished`() {
257
+        with (serverState.sasl) {
258
+            currentMechanism = saslMech1
259
+            saslBuffer = "HackThePlanet"
260
+            mechanismState = "root@thegibson"
261
+        }
262
+
263
+        handler.processEvent(ircClient, SaslFinished(TestConstants.time, true))
110 264
 
111
-            assertEquals(2, serverState.capabilities.enabledCapabilities.size)
112
-            assertEquals("", serverState.capabilities.enabledCapabilities[Capability.EchoMessages])
113
-            assertEquals("123", serverState.capabilities.enabledCapabilities[Capability.HostsInNamesReply])
265
+        with (serverState.sasl) {
266
+            assertNull(currentMechanism)
267
+            assertEquals("", saslBuffer)
268
+            assertNull(mechanismState)
114 269
         }
115 270
     }
116 271
 
117
-}
272
+}

+ 72
- 0
src/test/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessorTest.kt View File

@@ -0,0 +1,72 @@
1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.TestConstants
4
+import com.dmdirc.ktirc.events.AuthenticationMessage
5
+import com.dmdirc.ktirc.events.SaslFinished
6
+import com.dmdirc.ktirc.model.IrcMessage
7
+import com.dmdirc.ktirc.params
8
+import com.dmdirc.ktirc.util.currentTimeProvider
9
+import org.junit.jupiter.api.Assertions.*
10
+import org.junit.jupiter.api.BeforeEach
11
+import org.junit.jupiter.api.Test
12
+
13
+internal class AuthenticationProcessorTest {
14
+
15
+    private var processor = AuthenticationProcessor()
16
+
17
+    @BeforeEach
18
+    fun setUp() {
19
+        currentTimeProvider = { TestConstants.time }
20
+    }
21
+
22
+    @Test
23
+    fun `raises authentication message with null argument if no params specified`() {
24
+        val events = processor.process(IrcMessage(emptyMap(), null, "AUTHENTICATE", emptyList()))
25
+
26
+        assertEquals(1, events.size)
27
+        val event = events[0] as AuthenticationMessage
28
+        assertEquals(TestConstants.time, event.time)
29
+        assertNull(event.argument)
30
+    }
31
+
32
+    @Test
33
+    fun `raises authentication message with null argument if + specified`() {
34
+        val events = processor.process(IrcMessage(emptyMap(), null, "AUTHENTICATE", params("+")))
35
+
36
+        assertEquals(1, events.size)
37
+        val event = events[0] as AuthenticationMessage
38
+        assertEquals(TestConstants.time, event.time)
39
+        assertNull(event.argument)
40
+    }
41
+
42
+    @Test
43
+    fun `raises authentication message with argument`() {
44
+        val events = processor.process(IrcMessage(emptyMap(), null, "AUTHENTICATE", params("HackThePlanet")))
45
+
46
+        assertEquals(1, events.size)
47
+        val event = events[0] as AuthenticationMessage
48
+        assertEquals(TestConstants.time, event.time)
49
+        assertEquals("HackThePlanet", event.argument)
50
+    }
51
+
52
+    @Test
53
+    fun `raises sasl finished on success`() {
54
+        val events = processor.process(IrcMessage(emptyMap(), ":the.gibson".toByteArray(), "903", params("*", "SASL authentication successful")))
55
+
56
+        assertEquals(1, events.size)
57
+        val event = events[0] as SaslFinished
58
+        assertEquals(TestConstants.time, event.time)
59
+        assertTrue(event.success)
60
+    }
61
+
62
+    @Test
63
+    fun `raises sasl finished on generic failure`() {
64
+        val events = processor.process(IrcMessage(emptyMap(), ":the.gibson".toByteArray(), "904", params("*", "SASL authentication failed")))
65
+
66
+        assertEquals(1, events.size)
67
+        val event = events[0] as SaslFinished
68
+        assertEquals(TestConstants.time, event.time)
69
+        assertFalse(event.success)
70
+    }
71
+
72
+}

+ 15
- 3
src/test/kotlin/com/dmdirc/ktirc/messages/MessageBuildersTest.kt View File

@@ -71,8 +71,20 @@ internal class MessageBuildersTest {
71 71
 
72 72
     @Test
73 73
     fun `sendUser sends correct USER message`() {
74
-        mockClient.sendUser("AcidBurn", "localhost", "gibson", "Kate")
75
-        verify(mockClient).send("USER AcidBurn localhost gibson :Kate")
74
+        mockClient.sendUser("AcidBurn","Kate")
75
+        verify(mockClient).send("USER AcidBurn 0 * :Kate")
76 76
     }
77 77
 
78
-}
78
+    @Test
79
+    fun `sendUser sends correct AUTHENTICATE message`() {
80
+        mockClient.sendAuthenticationMessage("SCRAM-MD5")
81
+        verify(mockClient).send("AUTHENTICATE SCRAM-MD5")
82
+    }
83
+
84
+    @Test
85
+    fun `sendUser sends correct blank AUTHENTICATE message`() {
86
+        mockClient.sendAuthenticationMessage()
87
+        verify(mockClient).send("AUTHENTICATE +")
88
+    }
89
+
90
+}

+ 1
- 1
src/test/kotlin/com/dmdirc/ktirc/model/CapabilitiesStateTest.kt View File

@@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test
6 6
 internal class CapabilitiesStateTest {
7 7
 
8 8
     @Test
9
-    fun `CapabilitiesState defaults negotiation state to awaiting list`() {
9
+    fun `defaults negotiation state to awaiting list`() {
10 10
         val capabilitiesState = CapabilitiesState()
11 11
 
12 12
         assertEquals(CapabilitiesNegotiationState.AWAITING_LIST, capabilitiesState.negotiationState)

+ 81
- 0
src/test/kotlin/com/dmdirc/ktirc/model/SaslStateTest.kt View File

@@ -0,0 +1,81 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+import com.dmdirc.ktirc.sasl.SaslMechanism
4
+import com.nhaarman.mockitokotlin2.doReturn
5
+import com.nhaarman.mockitokotlin2.mock
6
+import org.junit.jupiter.api.Assertions.assertEquals
7
+import org.junit.jupiter.api.Assertions.assertNull
8
+import org.junit.jupiter.api.Test
9
+
10
+internal class SaslStateTest {
11
+
12
+    private val mech1 = mock<SaslMechanism> {
13
+        on { priority } doReturn 1
14
+        on { ircName } doReturn "mech1"
15
+    }
16
+
17
+    private val mech2 = mock<SaslMechanism> {
18
+        on { priority } doReturn 2
19
+        on { ircName } doReturn "mech2"
20
+    }
21
+
22
+    private val mech3 = mock<SaslMechanism> {
23
+        on { priority } doReturn 3
24
+        on { ircName } doReturn "mech3"
25
+    }
26
+
27
+    private val mechanisms = listOf(mech1, mech2, mech3)
28
+
29
+    @Test
30
+    fun `gets most preferred client SASL mechanism if none are specified by server`() {
31
+        val state = SaslState(mechanisms)
32
+
33
+        assertEquals(mech3, state.getPreferredSaslMechanism(""))
34
+    }
35
+
36
+    @Test
37
+    fun `gets next preferred client SASL mechanism if one was tried`() {
38
+        val state = SaslState(mechanisms)
39
+        state.currentMechanism = mech3
40
+
41
+        assertEquals(mech2, state.getPreferredSaslMechanism(""))
42
+    }
43
+
44
+    @Test
45
+    fun `gets no preferred client SASL mechanism if all were tried`() {
46
+        val state = SaslState(mechanisms)
47
+        state.currentMechanism = mech1
48
+
49
+        assertNull(state.getPreferredSaslMechanism(""))
50
+    }
51
+
52
+    @Test
53
+    fun `gets most preferred client SASL mechanism if the server supports all`() {
54
+        val state = SaslState(mechanisms)
55
+
56
+        assertEquals(mech3, state.getPreferredSaslMechanism("mech1,mech3,mech2"))
57
+    }
58
+
59
+    @Test
60
+    fun `gets most preferred client SASL mechanism if the server supports some`() {
61
+        val state = SaslState(mechanisms)
62
+
63
+        assertEquals(mech2, state.getPreferredSaslMechanism("mech2,mech1,other"))
64
+    }
65
+
66
+    @Test
67
+    fun `gets no preferred client SASL mechanism if the server supports none`() {
68
+        val state = SaslState(mechanisms)
69
+
70
+        assertNull(state.getPreferredSaslMechanism("foo,bar,baz"))
71
+    }
72
+
73
+    @Test
74
+    fun `setting the current mechanism clears the existing state`() {
75
+        val state = SaslState(mechanisms)
76
+        state.mechanismState = "in progress"
77
+        state.currentMechanism = mech2
78
+        assertNull(state.mechanismState)
79
+    }
80
+
81
+}

+ 39
- 0
src/test/kotlin/com/dmdirc/ktirc/sasl/PlainMechanismTest.kt View File

@@ -0,0 +1,39 @@
1
+package com.dmdirc.ktirc.sasl
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.model.Profile
5
+import com.dmdirc.ktirc.model.ServerState
6
+import com.nhaarman.mockitokotlin2.argumentCaptor
7
+import com.nhaarman.mockitokotlin2.doReturn
8
+import com.nhaarman.mockitokotlin2.mock
9
+import com.nhaarman.mockitokotlin2.verify
10
+import org.junit.jupiter.api.Assertions.assertEquals
11
+import org.junit.jupiter.api.Test
12
+
13
+internal class PlainMechanismTest {
14
+
15
+    private val serverState = ServerState("", "", emptyList())
16
+    private val saslProfile = Profile("acidBurn", "Kate Libby", "acidB", "acidB", "HackThePlan3t!")
17
+    private val ircClient = mock<IrcClient> {
18
+        on { serverState } doReturn serverState
19
+        on { profile } doReturn saslProfile
20
+    }
21
+
22
+    private val mechanism = PlainMechanism()
23
+
24
+    @Test
25
+    fun `sends encoded username and password when first message received`() {
26
+        mechanism.handleAuthenticationEvent(ircClient, null)
27
+
28
+        val captor = argumentCaptor<String>()
29
+        verify(ircClient).send(captor.capture())
30
+        val parts = captor.firstValue.split(' ')
31
+        assertEquals("AUTHENTICATE", parts[0])
32
+
33
+        val data = String(parts[1].fromBase64()).split('\u0000')
34
+        assertEquals("acidB", data[0])
35
+        assertEquals("acidB", data[1])
36
+        assertEquals("HackThePlan3t!", data[2])
37
+    }
38
+
39
+}

+ 20
- 0
src/test/kotlin/com/dmdirc/ktirc/util/Base64Test.kt View File

@@ -0,0 +1,20 @@
1
+package com.dmdirc.ktirc.util
2
+
3
+import com.dmdirc.ktirc.sasl.fromBase64
4
+import com.dmdirc.ktirc.sasl.toBase64
5
+import org.junit.jupiter.api.Assertions.assertEquals
6
+import org.junit.jupiter.api.Test
7
+
8
+class Base64Test {
9
+
10
+    @Test
11
+    fun `encodes byte arrays into base64`() {
12
+        assertEquals("SGFjayB0aGUgUGxhbmV0", "Hack the Planet".toByteArray().toBase64())
13
+    }
14
+
15
+    @Test
16
+    fun `decodes byte arrays from base64`() {
17
+        assertEquals("Hack the Planet", String("SGFjayB0aGUgUGxhbmV0".fromBase64()))
18
+    }
19
+
20
+}

Loading…
Cancel
Save