You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

IrcClientTest.kt 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. package com.dmdirc.ktirc
  2. import com.dmdirc.ktirc.events.*
  3. import com.dmdirc.ktirc.io.CaseMapping
  4. import com.dmdirc.ktirc.io.LineBufferedSocket
  5. import com.dmdirc.ktirc.model.*
  6. import com.dmdirc.ktirc.util.currentTimeProvider
  7. import com.nhaarman.mockitokotlin2.*
  8. import io.ktor.util.KtorExperimentalAPI
  9. import kotlinx.coroutines.*
  10. import kotlinx.coroutines.channels.Channel
  11. import kotlinx.coroutines.channels.filter
  12. import kotlinx.coroutines.channels.map
  13. import org.junit.jupiter.api.Assertions.*
  14. import org.junit.jupiter.api.BeforeEach
  15. import org.junit.jupiter.api.Test
  16. import org.junit.jupiter.api.assertThrows
  17. @KtorExperimentalAPI
  18. @ExperimentalCoroutinesApi
  19. internal class IrcClientImplTest {
  20. companion object {
  21. private const val HOST = "thegibson.com"
  22. private const val PORT = 12345
  23. private const val NICK = "AcidBurn"
  24. private const val REAL_NAME = "Kate Libby"
  25. private const val USER_NAME = "acidb"
  26. private const val PASSWORD = "HackThePlanet"
  27. }
  28. private val readLineChannel = Channel<ByteArray>(Channel.UNLIMITED)
  29. private val sendLineChannel = Channel<ByteArray>(Channel.UNLIMITED)
  30. private val mockSocket = mock<LineBufferedSocket> {
  31. on { receiveChannel } doReturn readLineChannel
  32. on { sendChannel } doReturn sendLineChannel
  33. }
  34. private val mockSocketFactory = mock<(CoroutineScope, String, Int, Boolean) -> LineBufferedSocket> {
  35. on { invoke(any(), eq(HOST), eq(PORT), any()) } doReturn mockSocket
  36. }
  37. private val mockEventHandler = mock<(IrcEvent) -> Unit>()
  38. @BeforeEach
  39. fun setUp() {
  40. currentTimeProvider = { TestConstants.time }
  41. }
  42. @Test
  43. fun `IrcClientImpl uses socket factory to create a new socket on connect`() {
  44. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  45. client.socketFactory = mockSocketFactory
  46. client.connect()
  47. verify(mockSocketFactory, timeout(500)).invoke(client, HOST, PORT, false)
  48. }
  49. @Test
  50. fun `IrcClientImpl uses socket factory to create a new tls on connect`() {
  51. val client = IrcClientImpl(Server(HOST, PORT, true), Profile(NICK, REAL_NAME, USER_NAME))
  52. client.socketFactory = mockSocketFactory
  53. client.connect()
  54. verify(mockSocketFactory, timeout(500)).invoke(client, HOST, PORT, true)
  55. }
  56. @Test
  57. fun `IrcClientImpl throws if socket already exists`() {
  58. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  59. client.socketFactory = mockSocketFactory
  60. client.connect()
  61. assertThrows<IllegalStateException> {
  62. client.connect()
  63. }
  64. }
  65. @Test
  66. fun `IrcClientImpl emits connection events with local time`() = runBlocking {
  67. currentTimeProvider = { TestConstants.time }
  68. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  69. client.socketFactory = mockSocketFactory
  70. client.onEvent(mockEventHandler)
  71. client.connect()
  72. val captor = argumentCaptor<IrcEvent>()
  73. verify(mockEventHandler, timeout(500).atLeast(2)).invoke(captor.capture())
  74. assertTrue(captor.firstValue is ServerConnecting)
  75. assertEquals(TestConstants.time, captor.firstValue.time)
  76. assertTrue(captor.secondValue is ServerConnected)
  77. assertEquals(TestConstants.time, captor.secondValue.time)
  78. }
  79. @Test
  80. fun `IrcClientImpl emits disconnected event with local time when read channel closed`() = runBlocking {
  81. currentTimeProvider = { TestConstants.time }
  82. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  83. client.socketFactory = mockSocketFactory
  84. client.connect()
  85. client.blockUntilConnected()
  86. client.onEvent(mockEventHandler)
  87. readLineChannel.close()
  88. val captor = argumentCaptor<ServerDisconnected>()
  89. verify(mockEventHandler, timeout(500)).invoke(captor.capture())
  90. assertEquals(TestConstants.time, captor.lastValue.time)
  91. }
  92. @Test
  93. fun `IrcClientImpl sends basic connection strings`() = runBlocking {
  94. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  95. client.socketFactory = mockSocketFactory
  96. client.connect()
  97. client.blockUntilConnected()
  98. assertEquals("CAP LS 302", String(sendLineChannel.receive()))
  99. assertEquals("NICK :$NICK", String(sendLineChannel.receive()))
  100. assertEquals("USER $USER_NAME 0 * :$REAL_NAME", String(sendLineChannel.receive()))
  101. }
  102. @Test
  103. fun `IrcClientImpl sends password first, when present`() = runBlocking {
  104. val client = IrcClientImpl(Server(HOST, PORT, password = PASSWORD), Profile(NICK, REAL_NAME, USER_NAME))
  105. client.socketFactory = mockSocketFactory
  106. client.connect()
  107. client.blockUntilConnected()
  108. assertEquals("CAP LS 302", String(sendLineChannel.receive()))
  109. assertEquals("PASS :$PASSWORD", String(sendLineChannel.receive()))
  110. }
  111. @Test
  112. fun `IrcClientImpl sends events to provided event handler`() {
  113. val client = IrcClientImpl(Server(HOST, PORT, password = PASSWORD), Profile(NICK, REAL_NAME, USER_NAME))
  114. client.socketFactory = mockSocketFactory
  115. client.onEvent(mockEventHandler)
  116. GlobalScope.launch {
  117. readLineChannel.send(":the.gibson 001 acidBurn :Welcome to the IRC!".toByteArray())
  118. }
  119. client.connect()
  120. verify(mockEventHandler, timeout(500)).invoke(isA<ServerWelcome>())
  121. }
  122. @Test
  123. fun `IrcClient gets case mapping from server features`() {
  124. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  125. client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.RfcStrict
  126. assertEquals(CaseMapping.RfcStrict, client.caseMapping)
  127. }
  128. @Test
  129. fun `IrcClient indicates if user is local user or not`() {
  130. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  131. client.serverState.localNickname = "[acidBurn]"
  132. assertTrue(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
  133. assertFalse(client.isLocalUser(User("acid-Burn", "libby", "root.localhost")))
  134. }
  135. @Test
  136. fun `IrcClient indicates if nickname is local user or not`() {
  137. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  138. client.serverState.localNickname = "[acidBurn]"
  139. assertTrue(client.isLocalUser("{acidBurn}"))
  140. assertFalse(client.isLocalUser("acid-Burn"))
  141. }
  142. @Test
  143. fun `IrcClient uses current case mapping to check local user`() {
  144. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  145. client.serverState.localNickname = "[acidBurn]"
  146. client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.Ascii
  147. assertFalse(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
  148. }
  149. @Test
  150. fun `IrcClientImpl sends text to socket`() = runBlocking {
  151. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  152. client.socketFactory = mockSocketFactory
  153. client.connect()
  154. client.blockUntilConnected()
  155. client.send("testing 123")
  156. assertEquals(true, withTimeoutOrNull(500) {
  157. var found = false
  158. for (line in sendLineChannel) {
  159. if (String(line) == "testing 123") {
  160. found = true
  161. break
  162. }
  163. }
  164. found
  165. })
  166. }
  167. @Test
  168. fun `IrcClientImpl disconnects the socket`() = runBlocking {
  169. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  170. client.socketFactory = mockSocketFactory
  171. client.connect()
  172. client.blockUntilConnected()
  173. client.disconnect()
  174. verify(mockSocket, timeout(500)).disconnect()
  175. }
  176. @Test
  177. fun `IrcClientImpl sends messages in order`() = runBlocking {
  178. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  179. client.socketFactory = mockSocketFactory
  180. client.connect()
  181. client.blockUntilConnected()
  182. (0..100).forEach { client.send("TEST $it") }
  183. assertEquals(100, withTimeoutOrNull(500) {
  184. var next = 0
  185. for (line in sendLineChannel.map { String(it) }.filter { it.startsWith("TEST ") }) {
  186. assertEquals("TEST $next", line)
  187. if (++next == 100) {
  188. break
  189. }
  190. }
  191. next
  192. })
  193. }
  194. @Test
  195. fun `IrcClientImpl defaults local nickname to profile`() = runBlocking {
  196. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  197. assertEquals(NICK, client.serverState.localNickname)
  198. }
  199. @Test
  200. fun `IrcClientImpl defaults server name to host name`() = runBlocking {
  201. val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
  202. assertEquals(HOST, client.serverState.serverName)
  203. }
  204. private suspend fun IrcClientImpl.blockUntilConnected() {
  205. // Yuck. Maybe connect should be asynchronous?
  206. while (serverState.status <= ServerStatus.Connecting) {
  207. delay(50)
  208. }
  209. }
  210. }