diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-06-17 16:26:01 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-06-17 16:26:01 +0900 |
| commit | d194126980c8b686ad4afccdfd902c53f73e66a0 (patch) | |
| tree | a3ec485c3b74cfb03ea20eb3855bf4a5abd0295c /platform-paper/src | |
| parent | c678bd15324dad0519cb1d9a4c58202a981ee8e2 (diff) | |
| download | LunaticChat-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')
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>()) } + } +} |
