Przeglądaj źródła

Add kick processing and event

Closes #7
tags/v0.5.0
Chris Smith 5 lat temu
rodzic
commit
4a6ff8d7c6

+ 1
- 0
CHANGELOG Wyświetl plik

12
  * Other new events:
12
  * Other new events:
13
     * Added MotdFinished event
13
     * Added MotdFinished event
14
     * Added UserAccountChanged event
14
     * Added UserAccountChanged event
15
+    * Added ChannelUserKicked event
15
  * Improved some documentation
16
  * Improved some documentation
16
 
17
 
17
 v0.4.0
18
 v0.4.0

+ 6
- 1
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt Wyświetl plik

69
     /**
69
     /**
70
      * Utility method to determine if the given user is the one we are connected to IRC as.
70
      * Utility method to determine if the given user is the one we are connected to IRC as.
71
      */
71
      */
72
-    fun isLocalUser(user: User) = caseMapping.areEquivalent(user.nickname, serverState.localNickname)
72
+    fun isLocalUser(user: User) = isLocalUser(user.nickname)
73
+
74
+    /**
75
+     * Utility method to determine if the given user is the one we are connected to IRC as.
76
+     */
77
+    fun isLocalUser(nickname: String) = caseMapping.areEquivalent(nickname, serverState.localNickname)
73
 
78
 
74
 }
79
 }
75
 
80
 

+ 12
- 0
src/main/kotlin/com/dmdirc/ktirc/events/ChannelStateHandler.kt Wyświetl plik

15
             is ChannelParted -> handlePart(client, event)
15
             is ChannelParted -> handlePart(client, event)
16
             is ChannelNamesReceived -> handleNamesReceived(client, event)
16
             is ChannelNamesReceived -> handleNamesReceived(client, event)
17
             is ChannelNamesFinished -> handleNamesFinished(client, event)
17
             is ChannelNamesFinished -> handleNamesFinished(client, event)
18
+            is ChannelUserKicked -> handleKick(client, event)
18
             is ModeChanged -> handleModeChanged(client, event)
19
             is ModeChanged -> handleModeChanged(client, event)
19
             is UserQuit -> return handleQuit(client, event)
20
             is UserQuit -> return handleQuit(client, event)
20
         }
21
         }
41
         }
42
         }
42
     }
43
     }
43
 
44
 
45
+    private fun handleKick(client: IrcClient, event: ChannelUserKicked) {
46
+        if (client.isLocalUser(event.victim)) {
47
+            log.info { "Kicked from channel: ${event.channel}" }
48
+            client.channelState -= event.channel
49
+        } else {
50
+            client.channelState[event.channel]?.let {
51
+                it.users -= event.victim
52
+            }
53
+        }
54
+    }
55
+
44
     private fun handleNamesReceived(client: IrcClient, event: ChannelNamesReceived) {
56
     private fun handleNamesReceived(client: IrcClient, event: ChannelNamesReceived) {
45
         val channel = client.channelState[event.channel] ?: return
57
         val channel = client.channelState[event.channel] ?: return
46
 
58
 

+ 3
- 0
src/main/kotlin/com/dmdirc/ktirc/events/Events.kt Wyświetl plik

35
 /** Raised when a user leaves a channel. */
35
 /** Raised when a user leaves a channel. */
36
 class ChannelParted(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
36
 class ChannelParted(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
37
 
37
 
38
+/** Raised when a [victim] is kicked from a channel. */
39
+class ChannelUserKicked(time: LocalDateTime, val user: User, val channel: String, val victim: String, val reason: String = ""): IrcEvent(time)
40
+
38
 /** Raised when a user quits, and is in a channel. */
41
 /** Raised when a user quits, and is in a channel. */
39
 class ChannelQuit(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
42
 class ChannelQuit(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
40
 
43
 

+ 16
- 0
src/main/kotlin/com/dmdirc/ktirc/events/UserStateHandler.kt Wyświetl plik

9
         when (event) {
9
         when (event) {
10
             is ChannelJoined -> handleJoin(client.userState, event)
10
             is ChannelJoined -> handleJoin(client.userState, event)
11
             is ChannelParted -> handlePart(client, event)
11
             is ChannelParted -> handlePart(client, event)
12
+            is ChannelUserKicked -> handleKick(client, event)
12
             is ChannelNamesReceived  -> handleNamesReceived(client, event)
13
             is ChannelNamesReceived  -> handleNamesReceived(client, event)
13
             is UserAccountChanged -> handleAccountChanged(client, event)
14
             is UserAccountChanged -> handleAccountChanged(client, event)
14
             is UserQuit -> handleQuit(client.userState, event)
15
             is UserQuit -> handleQuit(client.userState, event)
36
         }
37
         }
37
     }
38
     }
38
 
39
 
40
+    private fun handleKick(client: IrcClient, event: ChannelUserKicked) {
41
+        if (client.isLocalUser(event.victim)) {
42
+            // Remove channel from all users
43
+            client.userState.forEach { it.channels -= event.channel }
44
+            client.userState.removeIf { it.channels.isEmpty() && !client.isLocalUser(it.details) }
45
+        } else {
46
+            client.userState[event.victim]?.channels?.let {
47
+                it -= event.channel
48
+                if (it.isEmpty()) {
49
+                    client.userState -= event.victim
50
+                }
51
+            }
52
+        }
53
+    }
54
+
39
     private fun handleNamesReceived(client: IrcClient, event: ChannelNamesReceived) {
55
     private fun handleNamesReceived(client: IrcClient, event: ChannelNamesReceived) {
40
         event.toModesAndUsers(client).forEach { (_, user) ->
56
         event.toModesAndUsers(client).forEach { (_, user) ->
41
             client.userState.addToChannel(user, event.channel)
57
             client.userState.addToChannel(user, event.channel)

+ 23
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/KickProcessor.kt Wyświetl plik

1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.events.ChannelUserKicked
4
+import com.dmdirc.ktirc.model.IrcMessage
5
+
6
+internal class KickProcessor : MessageProcessor {
7
+
8
+    override val commands = arrayOf("KICK")
9
+
10
+    override fun process(message: IrcMessage) = message.sourceUser?.let { user ->
11
+        listOf(ChannelUserKicked(message.time, user, message.channel, message.victim, message.reason))
12
+    } ?: emptyList()
13
+
14
+    private val IrcMessage.channel
15
+        get() = String(params[0])
16
+
17
+    private val IrcMessage.victim
18
+        get() = String(params[1])
19
+
20
+    private val IrcMessage.reason
21
+        get() = if (params.size > 2) String(params[2]) else ""
22
+
23
+}

+ 1
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/MessageProcessor.kt Wyświetl plik

22
         CapabilityProcessor(),
22
         CapabilityProcessor(),
23
         ISupportProcessor(),
23
         ISupportProcessor(),
24
         JoinProcessor(),
24
         JoinProcessor(),
25
+        KickProcessor(),
25
         ModeProcessor(),
26
         ModeProcessor(),
26
         MotdProcessor(),
27
         MotdProcessor(),
27
         NamesProcessor(),
28
         NamesProcessor(),

+ 1
- 0
src/main/kotlin/com/dmdirc/ktirc/model/UserState.kt Wyświetl plik

16
 
16
 
17
     internal operator fun plusAssign(details: User) { users += KnownUser(caseMappingProvider, details) }
17
     internal operator fun plusAssign(details: User) { users += KnownUser(caseMappingProvider, details) }
18
     internal operator fun minusAssign(details: User) { users -= details.nickname }
18
     internal operator fun minusAssign(details: User) { users -= details.nickname }
19
+    internal operator fun minusAssign(nickname: String) { users -= nickname }
19
 
20
 
20
     /** Provides a read-only iterator of all users. */
21
     /** Provides a read-only iterator of all users. */
21
     override operator fun iterator() = users.iterator().iterator()
22
     override operator fun iterator() = users.iterator().iterator()

+ 9
- 0
src/test/kotlin/com/dmdirc/ktirc/IrcClientTest.kt Wyświetl plik

163
         assertFalse(client.isLocalUser(User("acid-Burn", "libby", "root.localhost")))
163
         assertFalse(client.isLocalUser(User("acid-Burn", "libby", "root.localhost")))
164
     }
164
     }
165
 
165
 
166
+    @Test
167
+    fun `IrcClient indicates if nickname is local user or not`() {
168
+        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
169
+        client.serverState.localNickname = "[acidBurn]"
170
+
171
+        assertTrue(client.isLocalUser("{acidBurn}"))
172
+        assertFalse(client.isLocalUser("acid-Burn"))
173
+    }
174
+
166
     @Test
175
     @Test
167
     fun `IrcClient uses current case mapping to check local user`() {
176
     fun `IrcClient uses current case mapping to check local user`() {
168
         val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
177
         val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))

+ 34
- 12
src/test/kotlin/com/dmdirc/ktirc/events/ChannelStateHandlerTest.kt Wyświetl plik

18
         on { serverState } doReturn serverState
18
         on { serverState } doReturn serverState
19
         on { channelState } doReturn channelStateMap
19
         on { channelState } doReturn channelStateMap
20
         on { isLocalUser(User("acidburn", "libby", "root.localhost")) } doReturn true
20
         on { isLocalUser(User("acidburn", "libby", "root.localhost")) } doReturn true
21
+        on { isLocalUser("acidburn") } doReturn  true
21
     }
22
     }
22
 
23
 
23
     @Test
24
     @Test
24
-    fun `ChannelStateHandler creates new state object for local joins`() {
25
+    fun `creates new state object for local joins`() {
25
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("acidburn", "libby", "root.localhost"), "#thegibson"))
26
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("acidburn", "libby", "root.localhost"), "#thegibson"))
26
         assertTrue("#thegibson" in channelStateMap)
27
         assertTrue("#thegibson" in channelStateMap)
27
     }
28
     }
28
 
29
 
29
     @Test
30
     @Test
30
-    fun `ChannelStateHandler does not create new state object for remote joins`() {
31
+    fun `does not create new state object for remote joins`() {
31
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson"))
32
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson"))
32
         assertFalse("#thegibson" in channelStateMap)
33
         assertFalse("#thegibson" in channelStateMap)
33
     }
34
     }
34
 
35
 
35
     @Test
36
     @Test
36
-    fun `ChannelStateHandler adds joiners to channel state`() {
37
+    fun `adds joiners to channel state`() {
37
         channelStateMap += ChannelState("#thegibson") { CaseMapping.Rfc }
38
         channelStateMap += ChannelState("#thegibson") { CaseMapping.Rfc }
38
 
39
 
39
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson"))
40
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson"))
42
     }
43
     }
43
 
44
 
44
     @Test
45
     @Test
45
-    fun `ChannelStateHandler clears existing users when getting a new list`() {
46
+    fun `clears existing users when getting a new list`() {
46
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
47
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
47
         channel.users += ChannelUser("acidBurn")
48
         channel.users += ChannelUser("acidBurn")
48
         channel.users += ChannelUser("thePlague")
49
         channel.users += ChannelUser("thePlague")
55
     }
56
     }
56
 
57
 
57
     @Test
58
     @Test
58
-    fun `ChannelStateHandler adds users from multiple name received events`() {
59
+    fun `adds users from multiple name received events`() {
59
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
60
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
60
         channelStateMap += channel
61
         channelStateMap += channel
61
 
62
 
70
     }
71
     }
71
 
72
 
72
     @Test
73
     @Test
73
-    fun `ChannelStateHandler clears and readds users on additional names received`() {
74
+    fun `clears and readds users on additional names received`() {
74
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
75
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
75
         channelStateMap += channel
76
         channelStateMap += channel
76
 
77
 
85
     }
86
     }
86
 
87
 
87
     @Test
88
     @Test
88
-    fun `ChannelStateHandler adds users with mode prefixes`() {
89
+    fun `adds users with mode prefixes`() {
89
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
90
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
90
         channelStateMap += channel
91
         channelStateMap += channel
91
         serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("ov", "@+")
92
         serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("ov", "@+")
101
     }
102
     }
102
 
103
 
103
     @Test
104
     @Test
104
-    fun `ChannelStateHandler adds users with full hosts`() {
105
+    fun `adds users with full hosts`() {
105
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
106
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
106
         channelStateMap += channel
107
         channelStateMap += channel
107
         serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("ov", "@+")
108
         serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("ov", "@+")
115
     }
116
     }
116
 
117
 
117
     @Test
118
     @Test
118
-    fun `ChannelStateHandler removes state object for local parts`() {
119
+    fun `removes state object for local parts`() {
119
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
120
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
120
         channelStateMap += channel
121
         channelStateMap += channel
121
 
122
 
125
     }
126
     }
126
 
127
 
127
     @Test
128
     @Test
128
-    fun `ChannelStateHandler removes user from channel member list for remote parts`() {
129
+    fun `removes user from channel member list for remote parts`() {
129
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
130
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
130
         channel.users += ChannelUser("ZeroCool")
131
         channel.users += ChannelUser("ZeroCool")
131
         channelStateMap += channel
132
         channelStateMap += channel
136
     }
137
     }
137
 
138
 
138
     @Test
139
     @Test
139
-    fun `ChannelStateHandler removes user from all channel member lists for quits`() {
140
+    fun `removes state object for local kicks`() {
141
+        val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
142
+        channelStateMap += channel
143
+
144
+        handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson", "acidburn", "Bye!"))
145
+
146
+        assertFalse("#thegibson" in channelStateMap)
147
+    }
148
+
149
+    @Test
150
+    fun `removes user from channel member list for remote kicks`() {
151
+        val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
152
+        channel.users += ChannelUser("ZeroCool")
153
+        channelStateMap += channel
154
+
155
+        handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("acidburn", "libby", "root.localhost"), "#thegibson", "zerocool", "Bye!"))
156
+
157
+        assertFalse("zerocool" in channel.users)
158
+    }
159
+
160
+    @Test
161
+    fun `removes user from all channel member lists for quits`() {
140
         with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
162
         with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
141
             users += ChannelUser("ZeroCool")
163
             users += ChannelUser("ZeroCool")
142
             channelStateMap += this
164
             channelStateMap += this
162
 
184
 
163
 
185
 
164
     @Test
186
     @Test
165
-    fun `ChannelStateHandler raises ChannelQuit event for each channel a user quits from`() {
187
+    fun `raises ChannelQuit event for each channel a user quits from`() {
166
         with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
188
         with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
167
             users += ChannelUser("ZeroCool")
189
             users += ChannelUser("ZeroCool")
168
             channelStateMap += this
190
             channelStateMap += this

+ 70
- 1
src/test/kotlin/com/dmdirc/ktirc/events/UserStateHandlerTest.kt Wyświetl plik

20
     private val ircClient = mock<IrcClient> {
20
     private val ircClient = mock<IrcClient> {
21
         on { serverState } doReturn serverState
21
         on { serverState } doReturn serverState
22
         on { userState } doReturn userState
22
         on { userState } doReturn userState
23
-        on { isLocalUser(argForWhich { nickname == "zeroCool" }) } doReturn true
23
+        on { isLocalUser(argForWhich<User> { nickname == "zeroCool" }) } doReturn true
24
+        on { isLocalUser("zeroCool") } doReturn true
24
     }
25
     }
25
 
26
 
26
     private val handler = UserStateHandler()
27
     private val handler = UserStateHandler()
121
         }
122
         }
122
     }
123
     }
123
 
124
 
125
+
126
+    @Test
127
+    fun `removes channel from user on kick`() {
128
+        runBlocking {
129
+            userState += User("acidBurn")
130
+            userState.addToChannel(User("acidBurn"), "#thegibson")
131
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
132
+
133
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "acidBurn"))
134
+
135
+            assertEquals(listOf("#thegibson"), userState["acidBurn"]?.channels?.toList())
136
+        }
137
+    }
138
+
139
+    @Test
140
+    fun `removes user on kick from last channel`() {
141
+        runBlocking {
142
+            userState += User("acidBurn")
143
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
144
+
145
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "acidBurn"))
146
+
147
+            assertNull(userState["acidBurn"])
148
+        }
149
+    }
150
+
151
+    @Test
152
+    fun `removes channel from all users on local kick`() {
153
+        runBlocking {
154
+            userState += User("acidBurn")
155
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
156
+            userState.addToChannel(User("acidBurn"), "#thegibson")
157
+
158
+            userState += User("zeroCool")
159
+            userState.addToChannel(User("zeroCool"), "#dumpsterdiving")
160
+            userState.addToChannel(User("zeroCool"), "#thegibson")
161
+
162
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "zeroCool"))
163
+
164
+            assertEquals(listOf("#thegibson"), userState["acidBurn"]?.channels?.toList())
165
+            assertEquals(listOf("#thegibson"), userState["zeroCool"]?.channels?.toList())
166
+        }
167
+    }
168
+
169
+    @Test
170
+    fun `removes remote users with no remaining channels on local kick`() {
171
+        runBlocking {
172
+            userState += User("acidBurn")
173
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
174
+
175
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "zeroCool"))
176
+
177
+            assertNull(userState["acidBurn"])
178
+        }
179
+    }
180
+
181
+    @Test
182
+    fun `keeps local user with no remaining channels after local kick`() {
183
+        runBlocking {
184
+            userState += User("zeroCool")
185
+            userState.addToChannel(User("zeroCool"), "#dumpsterdiving")
186
+
187
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "zeroCool"))
188
+
189
+            assertNotNull(userState["zeroCool"])
190
+        }
191
+    }
192
+
124
     @Test
193
     @Test
125
     fun `removes user entirely on quit`() {
194
     fun `removes user entirely on quit`() {
126
         runBlocking {
195
         runBlocking {

+ 52
- 0
src/test/kotlin/com/dmdirc/ktirc/messages/KickProcessorTest.kt Wyświetl plik

1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.TestConstants
4
+import com.dmdirc.ktirc.model.IrcMessage
5
+import com.dmdirc.ktirc.model.User
6
+import com.dmdirc.ktirc.params
7
+import com.dmdirc.ktirc.util.currentTimeProvider
8
+import org.junit.jupiter.api.Assertions.assertEquals
9
+import org.junit.jupiter.api.BeforeEach
10
+import org.junit.jupiter.api.Test
11
+
12
+internal class KickProcessorTest {
13
+
14
+    @BeforeEach
15
+    fun setUp() {
16
+        currentTimeProvider = { TestConstants.time }
17
+    }
18
+
19
+    @Test
20
+    fun `raises kick event without message`() {
21
+        val events = KickProcessor().process(
22
+                IrcMessage(emptyMap(), "acidburn!libby@root.localhost".toByteArray(), "KICK", params("#crashandburn", "zeroCool")))
23
+        assertEquals(1, events.size)
24
+
25
+        assertEquals(TestConstants.time, events[0].time)
26
+        assertEquals(User("acidburn", "libby", "root.localhost"), events[0].user)
27
+        assertEquals("#crashandburn", events[0].channel)
28
+        assertEquals("zeroCool", events[0].victim)
29
+        assertEquals("", events[0].reason)
30
+    }
31
+
32
+    @Test
33
+    fun `raises kick event with message`() {
34
+        val events = KickProcessor().process(
35
+                IrcMessage(emptyMap(), "acidburn!libby@root.localhost".toByteArray(), "KICK", params("#crashandburn", "zeroCool", "Hack the planet!")))
36
+        assertEquals(1, events.size)
37
+
38
+        assertEquals(TestConstants.time, events[0].time)
39
+        assertEquals(User("acidburn", "libby", "root.localhost"), events[0].user)
40
+        assertEquals("#crashandburn", events[0].channel)
41
+        assertEquals("zeroCool", events[0].victim)
42
+        assertEquals("Hack the planet!", events[0].reason)
43
+    }
44
+
45
+    @Test
46
+    fun `does nothing if prefix missing`() {
47
+        val events = KickProcessor().process(
48
+                IrcMessage(emptyMap(), null, "KICK", params("#crashandburn", "zeroCool")))
49
+        assertEquals(0, events.size)
50
+    }
51
+
52
+}

+ 7
- 0
src/test/kotlin/com/dmdirc/ktirc/model/UserStateTest.kt Wyświetl plik

25
         assertNull(userState["acidburn"])
25
         assertNull(userState["acidburn"])
26
     }
26
     }
27
 
27
 
28
+    @Test
29
+    fun `UserState removes users by nickname`() {
30
+        userState += User("acidBurn", "libby", "root.localhost")
31
+        userState -= "ACIDBURN"
32
+        assertNull(userState["acidburn"])
33
+    }
34
+
28
     @Test
35
     @Test
29
     fun `UserState updates existing user with same nickname`() {
36
     fun `UserState updates existing user with same nickname`() {
30
         userState += User("acidBurn", "libby", "root.localhost")
37
         userState += User("acidBurn", "libby", "root.localhost")

Ładowanie…
Anuluj
Zapisz