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

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