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 11KB

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