123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- package com.dmdirc.ktirc
-
- import com.dmdirc.ktirc.events.*
- import com.dmdirc.ktirc.io.CaseMapping
- import com.dmdirc.ktirc.io.LineBufferedSocket
- import com.dmdirc.ktirc.messages.tagMap
- import com.dmdirc.ktirc.model.*
- import com.dmdirc.ktirc.util.RemoveIn
- import com.dmdirc.ktirc.util.currentTimeProvider
- import com.dmdirc.ktirc.util.generateLabel
- import io.mockk.*
- import kotlinx.coroutines.*
- import kotlinx.coroutines.channels.Channel
- import kotlinx.coroutines.channels.filter
- import kotlinx.coroutines.channels.map
- import kotlinx.coroutines.sync.Mutex
- import org.junit.jupiter.api.Assertions.*
- import org.junit.jupiter.api.BeforeEach
- import org.junit.jupiter.api.Test
- import org.junit.jupiter.api.assertThrows
- import java.net.UnknownHostException
- import java.nio.channels.UnresolvedAddressException
- import java.security.cert.CertificateException
- import java.util.concurrent.atomic.AtomicReference
-
- @ExperimentalCoroutinesApi
- internal class IrcClientImplTest {
-
- companion object {
- private const val HOST = "thegibson.com"
- private const val HOST2 = "irc.thegibson.com"
- private const val IP = "127.0.13.37"
- private const val PORT = 12345
- private const val NICK = "AcidBurn"
- private const val REAL_NAME = "Kate Libby"
- private const val USER_NAME = "acidb"
- private const val PASSWORD = "HackThePlanet"
- }
-
- private val readLineChannel = Channel<ByteArray>(Channel.UNLIMITED)
- private val sendLineChannel = Channel<ByteArray>(Channel.UNLIMITED)
-
- private val mockSocket = mockk<LineBufferedSocket> {
- every { receiveChannel } returns readLineChannel
- every { sendChannel } returns sendLineChannel
- }
-
- private val mockSocketFactory = mockk<(CoroutineScope, String, String, Int, Boolean) -> LineBufferedSocket> {
- every { this@mockk.invoke(any(), eq(HOST), eq(IP), eq(PORT), any()) } returns mockSocket
- every { this@mockk.invoke(any(), eq(HOST2), any(), eq(PORT), any()) } returns mockSocket
- }
-
- private val mockResolver = mockk<(String) -> Collection<ResolveResult>> {
- every { this@mockk.invoke(HOST) } returns listOf(ResolveResult(IP, false))
- }
-
- private val mockEventHandler = mockk<(IrcEvent) -> Unit> {
- every { this@mockk.invoke(any()) } just Runs
- }
-
- private val profileConfig = ProfileConfig().apply {
- nickname = NICK
- realName = REAL_NAME
- username = USER_NAME
- }
-
- private val serverConfig = ServerConfig().apply {
- host = HOST
- port = PORT
- useTls = false
- }
-
- private val normalConfig = IrcClientConfig(serverConfig, profileConfig, BehaviourConfig(), null)
-
- @BeforeEach
- fun setUp() {
- currentTimeProvider = { TestConstants.time }
- }
-
- @Test
- fun `uses socket factory to create a new socket on connect`() {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- verify(timeout = 500) { mockSocketFactory(client, HOST, IP, PORT, false) }
- }
-
- @Test
- fun `uses socket factory to create a new tls on connect`() {
- val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
- host = HOST
- port = PORT
- useTls = true
- }, profileConfig, BehaviourConfig(), null))
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- verify(timeout = 500) { mockSocketFactory(client, HOST, IP, PORT, true) }
- }
-
- @Test
- fun `prefers ipv6 addresses if behaviour is enabled`() {
- val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
- host = HOST2
- port = PORT
- }, profileConfig, BehaviourConfig().apply { preferIPv6 = true }, null))
-
- every { mockResolver(HOST2) } returns listOf(
- ResolveResult(IP, false),
- ResolveResult("::13:37", true),
- ResolveResult("0.0.0.0", false)
- )
-
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- verify(timeout = 500) { mockSocketFactory(client, HOST2, "::13:37", PORT, true) }
- }
-
- @Test
- fun `falls back to ipv4 if no ipv6 addresses are available`() {
- val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
- host = HOST2
- port = PORT
- }, profileConfig, BehaviourConfig().apply { preferIPv6 = true }, null))
-
- every { mockResolver(HOST2) } returns listOf(
- ResolveResult("0.0.0.0", false)
- )
-
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- verify(timeout = 500) { mockSocketFactory(client, HOST2, "0.0.0.0", PORT, true) }
- }
-
- @Test
- fun `prefers ipv4 addresses if ipv6 behaviour is disabled`() {
- val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
- host = HOST2
- port = PORT
- }, profileConfig, BehaviourConfig().apply { preferIPv6 = false }, null))
-
- every { mockResolver(HOST2) } returns listOf(
- ResolveResult("::13:37", true),
- ResolveResult("::313:37", true),
- ResolveResult("0.0.0.0", false)
- )
-
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- verify(timeout = 500) { mockSocketFactory(client, HOST2, "0.0.0.0", PORT, true) }
- }
-
- @Test
- fun `falls back to ipv6 if no ipv4 addresses available`() {
- val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
- host = HOST2
- port = PORT
- }, profileConfig, BehaviourConfig().apply { preferIPv6 = false }, null))
-
- every { mockResolver(HOST2) } returns listOf(
- ResolveResult("::13:37", true)
- )
-
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- verify(timeout = 500) { mockSocketFactory(client, HOST2, "::13:37", PORT, true) }
- }
-
- @Test
- fun `raises error if dns fails`() {
- val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
- host = HOST2
- }, profileConfig, BehaviourConfig().apply { preferIPv6 = true }, null))
-
- every { mockResolver(HOST2) } throws UnknownHostException("oops")
-
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.onEvent(mockEventHandler)
- client.connect()
-
- verify(timeout = 500) {
- mockEventHandler(match { it is ServerConnectionError && it.error == ConnectionError.UnresolvableAddress })
- }
- }
-
- @Test
- fun `throws if socket already exists`() {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- assertThrows<IllegalStateException> {
- client.connect()
- }
- }
-
- @Test
- fun `emits connection events with local time`() = runBlocking {
- currentTimeProvider = { TestConstants.time }
-
- val connectingSlot = slot<ServerConnecting>()
- val connectedSlot = slot<ServerConnected>()
-
- every { mockEventHandler.invoke(capture(connectingSlot)) } just Runs
- every { mockEventHandler.invoke(capture(connectedSlot)) } just Runs
-
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.onEvent(mockEventHandler)
- client.connect()
-
- verify(timeout = 500) {
- mockEventHandler(ofType<ServerConnecting>())
- mockEventHandler(ofType<ServerConnected>())
- }
-
- assertEquals(TestConstants.time, connectingSlot.captured.metadata.time)
- assertEquals(TestConstants.time, connectedSlot.captured.metadata.time)
- }
-
- @Test
- fun `sends basic connection strings`() = runBlocking {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- assertEquals("CAP LS 302", String(sendLineChannel.receive()))
- assertEquals("NICK $NICK", String(sendLineChannel.receive()))
- assertEquals("USER $USER_NAME 0 * :$REAL_NAME", String(sendLineChannel.receive()))
- }
-
- @Test
- fun `sends password first, when present`() = runBlocking {
- val client = IrcClientImpl(IrcClientConfig(ServerConfig().apply {
- host = HOST
- port = PORT
- password = PASSWORD
- }, profileConfig, BehaviourConfig(), null))
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- assertEquals("CAP LS 302", String(sendLineChannel.receive()))
- assertEquals("PASS $PASSWORD", String(sendLineChannel.receive()))
- }
-
- @Test
- fun `sends events to provided event handler`() {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.onEvent(mockEventHandler)
-
- GlobalScope.launch {
- readLineChannel.send(":the.gibson 001 acidBurn :Welcome to the IRC!".toByteArray())
- }
-
- client.connect()
-
- verify(timeout = 500) {
- mockEventHandler(ofType<ServerWelcome>())
- }
- }
-
- @Test
- fun `gets case mapping from server features`() {
- val client = IrcClientImpl(normalConfig)
- client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.RfcStrict
- assertEquals(CaseMapping.RfcStrict, client.caseMapping)
- }
-
- @Test
- fun `indicates if user is local user or not`() {
- val client = IrcClientImpl(normalConfig)
- client.localUser.nickname = "[acidBurn]"
-
- assertTrue(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
- assertFalse(client.isLocalUser(User("acid-Burn", "libby", "root.localhost")))
- }
-
- @Test
- fun `indicates if nickname is local user or not`() {
- val client = IrcClientImpl(normalConfig)
- client.localUser.nickname = "[acidBurn]"
-
- assertTrue(client.isLocalUser("{acidBurn}"))
- assertFalse(client.isLocalUser("acid-Burn"))
- }
-
- @Test
- fun `uses current case mapping to check local user`() {
- val client = IrcClientImpl(normalConfig)
- client.localUser.nickname = "[acidBurn]"
- client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.Ascii
- assertFalse(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
- }
-
- @Test
- @Deprecated("Tests deprecated method")
- @RemoveIn("2.0.0")
- fun `sends text to socket`() = runBlocking {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- client.send("testing 123")
-
- assertLineReceived("testing 123")
- }
-
- @Test
- fun `sends structured text to socket`() = runBlocking {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- client.send("testing", "123", "456")
-
- assertLineReceived("testing 123 456")
- }
-
- @Test
- fun `echoes message event when behaviour is set and cap is unsupported`() = runBlocking {
- val config = IrcClientConfig(serverConfig, profileConfig, BehaviourConfig().apply { alwaysEchoMessages = true }, null)
- val client = IrcClientImpl(config)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
-
- val slot = slot<MessageReceived>()
- val mockkEventHandler = mockk<(IrcEvent) -> Unit>(relaxed = true)
- every { mockkEventHandler(capture(slot)) } just Runs
-
- client.onEvent(mockkEventHandler)
- client.connect()
-
- client.send("PRIVMSG", "#thegibson", "Mess with the best, die like the rest")
-
- assertTrue(slot.isCaptured)
- val event = slot.captured
- assertEquals("#thegibson", event.target)
- assertEquals("Mess with the best, die like the rest", event.message)
- assertEquals(NICK, event.user.nickname)
- assertEquals(TestConstants.time, event.metadata.time)
- }
-
- @Test
- fun `does not echo message event when behaviour is set and cap is supported`() = runBlocking {
- val config = IrcClientConfig(serverConfig, profileConfig, BehaviourConfig().apply { alwaysEchoMessages = true }, null)
- val client = IrcClientImpl(config)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.serverState.capabilities.enabledCapabilities[Capability.EchoMessages] = ""
- client.connect()
-
- client.onEvent(mockEventHandler)
- client.send("PRIVMSG", "#thegibson", "Mess with the best, die like the rest")
-
- verify(inverse = true) {
- mockEventHandler(ofType<MessageReceived>())
- }
- }
-
- @Test
- fun `does not echo message event when behaviour is unset`() = runBlocking {
- val config = IrcClientConfig(serverConfig, profileConfig, BehaviourConfig().apply { alwaysEchoMessages = false }, null)
- val client = IrcClientImpl(config)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- client.onEvent(mockEventHandler)
- client.send("PRIVMSG", "#thegibson", "Mess with the best, die like the rest")
-
- verify(inverse = true) {
- mockEventHandler(ofType<MessageReceived>())
- }
- }
-
- @Test
- fun `sends structured text to socket with tags`() = runBlocking {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- client.send(tagMap(MessageTag.AccountName to "acidB"), "testing", "123", "456")
-
- assertLineReceived("@account=acidB testing 123 456")
- }
-
- @Test
- fun `asynchronously sends text to socket without label if cap is missing`() = runBlocking {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
-
- client.connect()
-
- client.sendAsync(tagMap(), "testing", arrayOf("123")) { false }
-
- assertLineReceived("testing 123")
- }
-
- @Test
- fun `asynchronously sends text to socket with added tags and label`() = runBlocking {
- generateLabel = { "abc123" }
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
-
- client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
- client.connect()
-
- client.sendAsync(tagMap(), "testing", arrayOf("123")) { false }
-
- assertLineReceived("@draft/label=abc123 testing 123")
- }
-
- @Test
- fun `asynchronously sends tagged text to socket with label`() = runBlocking {
- generateLabel = { "abc123" }
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
-
- client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
- client.connect()
-
- client.sendAsync(tagMap(MessageTag.AccountName to "x"), "testing", arrayOf("123")) { false }
-
- assertLineReceived("@account=x;draft/label=abc123 testing 123")
- }
-
- @Test
- fun `disconnects the socket`() = runBlocking {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- launch {
- delay(50)
- readLineChannel.close()
- sendLineChannel.close()
- }
-
- client.disconnect()
-
- verify(timeout = 500) {
- mockSocket.disconnect()
- }
- }
-
- @Test
- @ObsoleteCoroutinesApi
- fun `sends messages in order`() = runBlocking {
- val client = IrcClientImpl(normalConfig)
- client.socketFactory = mockSocketFactory
- client.resolver = mockResolver
- client.connect()
-
- (0..100).forEach { client.send("TEST", "$it") }
-
- assertEquals(100, withTimeoutOrNull(500) {
- var next = 0
- for (line in sendLineChannel.map { String(it) }.filter { it.startsWith("TEST ") }) {
- assertEquals("TEST $next", line)
- if (++next == 100) {
- break
- }
- }
- next
- })
- }
-
- @Test
- @Deprecated("Tests deprecated method")
- @RemoveIn("3.0.0")
- fun `defaults local nickname to profile`() {
- val client = IrcClientImpl(normalConfig)
- assertEquals(NICK, client.serverState.localNickname)
- }
-
- @Test
- fun `defaults local user to nickname in profile`() {
- val client = IrcClientImpl(normalConfig)
- assertEquals(User(NICK), client.localUser)
- }
-
- @Test
- fun `defaults server name to host name`() {
- val client = IrcClientImpl(normalConfig)
- assertEquals(HOST, client.serverState.serverName)
- }
-
- @Test
- fun `exposes behaviour config`() {
- val client = IrcClientImpl(IrcClientConfig(
- ServerConfig().apply { host = HOST },
- profileConfig,
- BehaviourConfig().apply { requestModesOnJoin = true },
- null))
-
- assertTrue(client.behaviour.requestModesOnJoin)
- }
-
- @Test
- fun `reset clears all state`() {
- with(IrcClientImpl(normalConfig)) {
- userState += User("zeroCool")
- channelState += ChannelState("#thegibson") { CaseMapping.Rfc }
- serverState.serverName = "root.$HOST"
- localUser.awayMessage = "Hacking the planet"
-
- reset()
-
- assertEquals(1, userState.count())
- assertEquals(0, channelState.count())
- assertEquals(HOST, serverState.serverName)
- assertEquals(User("AcidBurn"), localUser)
- }
- }
-
- @Test
- fun `sends connect error when host is unresolvable`() = runBlocking {
- every { mockSocket.connect() } throws UnresolvedAddressException()
- with(IrcClientImpl(normalConfig)) {
- socketFactory = mockSocketFactory
- resolver = mockResolver
- withTimeout(500) {
- launch {
- delay(50)
- connect()
- }
- val event = waitForEvent<ServerConnectionError>()
- assertEquals(ConnectionError.UnresolvableAddress, event.error)
- }
- }
- }
-
- @Test
- fun `sends connect error when tls certificate is bad`() = runBlocking {
- every { mockSocket.connect() } throws CertificateException("Boooo")
- with(IrcClientImpl(normalConfig)) {
- socketFactory = mockSocketFactory
- resolver = mockResolver
- withTimeout(500) {
- launch {
- delay(50)
- connect()
- }
- val event = waitForEvent<ServerConnectionError>()
- assertEquals(ConnectionError.BadTlsCertificate, event.error)
- assertEquals("Boooo", event.details)
- }
- }
- }
-
- @Test
- fun `identifies channels that have a prefix in the chantypes feature`() {
- with(IrcClientImpl(normalConfig)) {
- serverState.features[ServerFeature.ChannelTypes] = "&~"
- assertTrue(isChannel("&dumpsterdiving"))
- assertTrue(isChannel("~hacktheplanet"))
- assertFalse(isChannel("#root"))
- assertFalse(isChannel("acidBurn"))
- assertFalse(isChannel(""))
- assertFalse(isChannel("acidBurn#~"))
- }
- }
-
- private suspend inline fun <reified T : IrcEvent> IrcClient.waitForEvent(): T {
- val mutex = Mutex(true)
- val value = AtomicReference<T>()
- onEvent {
- if (it is T) {
- value.set(it)
- mutex.unlock()
- }
- }
- mutex.lock()
- return value.get()
- }
-
- private suspend fun assertLineReceived(expected: String) {
- assertEquals(true, withTimeoutOrNull(500) {
- for (line in sendLineChannel.map { String(it) }) {
- println(line)
- if (line == expected) {
- return@withTimeoutOrNull true
- }
- }
- false
- }) { "Expected to receive $expected" }
- }
-
-
- }
|