summaryrefslogtreecommitdiff
path: root/platform-paper/src
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-06-17 16:26:01 +0900
committerSho Sakuma <me@m1sk9.dev>2026-06-17 16:26:01 +0900
commitd194126980c8b686ad4afccdfd902c53f73e66a0 (patch)
treea3ec485c3b74cfb03ea20eb3855bf4a5abd0295c /platform-paper/src
parentc678bd15324dad0519cb1d9a4c58202a981ee8e2 (diff)
downloadLunaticChat-d194126980c8b686ad4afccdfd902c53f73e66a0.tar.gz
LunaticChat-d194126980c8b686ad4afccdfd902c53f73e66a0.tar.bz2
LunaticChat-d194126980c8b686ad4afccdfd902c53f73e66a0.zip
test: cover cross-server direct messaging; exclude bootstrap from coverage
Add unit tests for CrossServerDirectMessageManager (send/receive/error/dedup), DirectMessageHandler cross-server display and ReplyTarget resolution, and ServiceContainer wiring to raise patch coverage. Extend codecov ignore list to the plugin bootstrap classes (LunaticChat, ServiceInitializer), consistent with the existing exclusions for runtime-coupled classes (VelocityConnectionManager, command/impl) that are covered by end-to-end server tests. Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'platform-paper/src')
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainerTest.kt30
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt93
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt149
3 files changed, 272 insertions, 0 deletions
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainerTest.kt
new file mode 100644
index 0000000..78e3b2f
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainerTest.kt
@@ -0,0 +1,30 @@
+package dev.m1sk9.lunaticChat.paper
+
+import dev.m1sk9.lunaticChat.paper.velocity.CrossServerDirectMessageManager
+import dev.m1sk9.lunaticChat.paper.velocity.RemotePlayerRegistry
+import io.mockk.mockk
+import kotlin.test.Test
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+
+class ServiceContainerTest {
+ @Test
+ fun `holds cross-server direct message services and defaults others to null`() {
+ val dmManager = mockk<CrossServerDirectMessageManager>()
+ val registry = mockk<RemotePlayerRegistry>()
+
+ val container =
+ ServiceContainer(
+ languageManager = mockk(),
+ playerSettingsManager = mockk(),
+ directMessageHandler = mockk(),
+ crossServerDirectMessageManager = dmManager,
+ remotePlayerRegistry = registry,
+ )
+
+ assertSame(dmManager, container.crossServerDirectMessageManager)
+ assertSame(registry, container.remotePlayerRegistry)
+ assertNull(container.velocityConnectionManager)
+ assertNull(container.crossServerChatManager)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
index 923031b..e8764b2 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
@@ -1,13 +1,21 @@
package dev.m1sk9.lunaticChat.paper.chat.handler
+import dev.m1sk9.lunaticChat.engine.protocol.PresenceEntry
import dev.m1sk9.lunaticChat.paper.TestUtils
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import dev.m1sk9.lunaticChat.paper.velocity.RemotePlayerRegistry
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import org.bukkit.Bukkit
import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
@@ -120,6 +128,91 @@ class DirectMessageHandlerTest {
}
@Test
+ fun `getReplyTarget returns null when there is no history`() {
+ val handler = createHandler()
+ val player = TestUtils.createMockPlayer()
+
+ assertNull(handler.getReplyTarget(player))
+ }
+
+ @Test
+ fun `handleOutgoingCrossServerMessage records a remote reply target and returns the body`() {
+ val handler = createHandler()
+ val registry = RemotePlayerRegistry(localServerName = "lobby")
+ registry.replaceAll(listOf(PresenceEntry("Bob", "survival")))
+ handler.remotePlayerRegistry = registry
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+
+ val relayed = handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi")
+ assertEquals("hi", relayed)
+
+ val target = handler.getReplyTarget(sender)
+ assertIs<ReplyTarget.Remote>(target)
+ assertEquals("Bob", target.playerName)
+ assertEquals("survival", target.serverName)
+ }
+
+ @Test
+ fun `handleIncomingCrossServerMessage records the remote sender as reply target`() {
+ val handler = createHandler()
+ val registry = RemotePlayerRegistry(localServerName = "survival")
+ registry.replaceAll(listOf(PresenceEntry("Alice", "lobby")))
+ handler.remotePlayerRegistry = registry
+ val recipient = TestUtils.createMockPlayer(name = "Bob")
+
+ handler.handleIncomingCrossServerMessage(recipient, "Alice", "lobby", "hi")
+
+ val target = handler.getReplyTarget(recipient)
+ assertIs<ReplyTarget.Remote>(target)
+ assertEquals("Alice", target.playerName)
+ assertEquals("lobby", target.serverName)
+ }
+
+ @Test
+ fun `getReplyTarget skips a remote target that left the proxy`() {
+ val handler = createHandler()
+ handler.remotePlayerRegistry = RemotePlayerRegistry(localServerName = "lobby") // empty roster
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+
+ handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi")
+
+ assertNull(handler.getReplyTarget(sender))
+ }
+
+ @Test
+ fun `getReplyTarget returns Local for a recorded online player`() {
+ mockkStatic(Bukkit::class)
+ try {
+ val handler = createHandler()
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+ val recipient = TestUtils.createMockPlayer(name = "Bob")
+ every { Bukkit.getPlayer(recipient.uniqueId) } returns recipient
+
+ handler.sendDirectMessage(sender, recipient, "hi")
+
+ val target = handler.getReplyTarget(sender)
+ assertIs<ReplyTarget.Local>(target)
+ assertEquals(recipient.uniqueId, target.uuid)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `clearPlayer removes a remote reply target`() {
+ val handler = createHandler()
+ val registry = RemotePlayerRegistry(localServerName = "lobby")
+ registry.replaceAll(listOf(PresenceEntry("Bob", "survival")))
+ handler.remotePlayerRegistry = registry
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+
+ handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi")
+ handler.clearPlayer(sender)
+
+ assertNull(handler.getReplyTarget(sender))
+ }
+
+ @Test
fun `handler can be created with all dependencies`() {
val config = TestUtils.createTestConfiguration()
val settingsManager = mockk<PlayerSettingsManager>(relaxed = true)
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt
new file mode 100644
index 0000000..cf9f529
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt
@@ -0,0 +1,149 @@
+package dev.m1sk9.lunaticChat.paper.velocity
+
+import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import net.kyori.adventure.text.Component
+import org.bukkit.plugin.Plugin
+import java.util.UUID
+import java.util.logging.Logger
+import kotlin.test.Test
+
+class CrossServerDirectMessageManagerTest {
+ private class Fixture(
+ cacheSize: Int = 100,
+ ) {
+ val plugin = mockk<Plugin>(relaxed = true)
+ val logger = mockk<Logger>(relaxed = true)
+ val config = TestUtils.createTestConfiguration()
+ val dmHandler = mockk<DirectMessageHandler>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ val manager =
+ CrossServerDirectMessageManager(plugin, logger, config, dmHandler, languageManager, cacheSize)
+
+ init {
+ // Run scheduled main-thread tasks inline so assertions can observe their effects.
+ every { plugin.server.scheduler.runTask(plugin, any<Runnable>()) } answers {
+ secondArg<Runnable>().run()
+ mockk(relaxed = true)
+ }
+ every { languageManager.getMessage(any(), any()) } returns "msg"
+ }
+ }
+
+ private fun relay(
+ messageId: String = "id-1",
+ targetName: String = "Bob",
+ sourceServer: String = "lobby",
+ ) = PluginMessage.DirectMessageRelay(
+ messageId = messageId,
+ sourceServerName = sourceServer,
+ senderId = UUID.randomUUID().toString(),
+ senderName = "Alice",
+ targetServerName = "survival",
+ targetName = targetName,
+ message = "hi",
+ )
+
+ @Test
+ fun `sendCrossServerMessage relays via plugin channel and delegates display`() {
+ val f = Fixture()
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+ every { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } returns "hi"
+
+ f.manager.sendCrossServerMessage(sender, "Bob", "survival", "hi")
+
+ verify { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") }
+ verify { sender.sendPluginMessage(f.plugin, "lunaticchat:main", any<ByteArray>()) }
+ }
+
+ @Test
+ fun `handleIncomingMessage delivers to the local recipient`() {
+ val f = Fixture()
+ val recipient = TestUtils.createMockPlayer(name = "Bob")
+ every { f.plugin.server.getPlayer("Bob") } returns recipient
+
+ f.manager.handleIncomingMessage(relay())
+
+ verify { f.dmHandler.handleIncomingCrossServerMessage(recipient, "Alice", "lobby", "hi") }
+ }
+
+ @Test
+ fun `handleIncomingMessage ignores duplicate message ids`() {
+ val f = Fixture()
+ val recipient = TestUtils.createMockPlayer(name = "Bob")
+ every { f.plugin.server.getPlayer("Bob") } returns recipient
+ val message = relay(messageId = "dup")
+
+ f.manager.handleIncomingMessage(message)
+ f.manager.handleIncomingMessage(message)
+
+ verify(exactly = 1) {
+ f.dmHandler.handleIncomingCrossServerMessage(any(), any(), any(), any())
+ }
+ }
+
+ @Test
+ fun `handleIncomingMessage does not deliver when recipient is offline`() {
+ val f = Fixture()
+ every { f.plugin.server.getPlayer("Ghost") } returns null
+
+ f.manager.handleIncomingMessage(relay(targetName = "Ghost"))
+
+ verify(exactly = 0) {
+ f.dmHandler.handleIncomingCrossServerMessage(any(), any(), any(), any())
+ }
+ }
+
+ @Test
+ fun `handleError notifies the original sender`() {
+ val f = Fixture()
+ val senderId = UUID.randomUUID()
+ val sender = TestUtils.createMockPlayer(uuid = senderId, name = "Alice")
+ every { f.plugin.server.getPlayer(senderId) } returns sender
+
+ val error =
+ PluginMessage.DirectMessageError(
+ messageId = "id",
+ senderId = senderId.toString(),
+ targetName = "Bob",
+ targetServerName = "survival",
+ reason = PluginMessage.DirectMessageError.Reason.TARGET_OFFLINE,
+ )
+ f.manager.handleError(error)
+
+ verify { sender.sendMessage(any<Component>()) }
+ }
+
+ @Test
+ fun `handleError with malformed sender id is ignored`() {
+ val f = Fixture()
+
+ val error =
+ PluginMessage.DirectMessageError(
+ messageId = "id",
+ senderId = "not-a-uuid",
+ targetName = "Bob",
+ targetServerName = "survival",
+ reason = PluginMessage.DirectMessageError.Reason.SERVER_NOT_FOUND,
+ )
+
+ // Should not throw.
+ f.manager.handleError(error)
+ }
+
+ @Test
+ fun `sendCrossServerMessage prunes the dedup cache when over capacity`() {
+ val f = Fixture(cacheSize = 1)
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+ every { f.dmHandler.handleOutgoingCrossServerMessage(any(), any(), any(), any()) } returns "hi"
+
+ repeat(3) { f.manager.sendCrossServerMessage(sender, "Bob$it", "survival", "hi") }
+
+ verify(atLeast = 1) { sender.sendPluginMessage(f.plugin, "lunaticchat:main", any<ByteArray>()) }
+ }
+}