summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--codecov.yml5
-rw-r--r--gradle.properties2
-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
-rw-r--r--website/src/ja/docs/features/direct-message.md2
6 files changed, 280 insertions, 1 deletions
diff --git a/codecov.yml b/codecov.yml
index d1c94cc..61d53ff 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -24,6 +24,11 @@ ignore:
# VelocityConnectionManager is tightly coupled to the Paper/Bukkit runtime (Plugin, Player,
# PluginMessageListener) and is exercised via end-to-end multi-server tests.
- "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt"
+ # Plugin bootstrap/DI wiring: the main plugin class and service initializer drive the Paper
+ # lifecycle (onEnable, dataFolder, schedulers, command registration) and are exercised via
+ # end-to-end server tests rather than unit tests.
+ - "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt"
+ - "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt"
# The Velocity platform has no unit-test infrastructure; it is exercised via end-to-end
# multi-server tests (docker compose).
- "platform-velocity/src/main/kotlin/**"
diff --git a/gradle.properties b/gradle.properties
index 9ee526c..ae2b5f7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,5 +5,5 @@ org.gradle.parallel=true
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
# Platform versions (can be released independently)
-paperVersion=1.2.2
+paperVersion=1.3.0
velocityVersion=1.1.0
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>()) }
+ }
+}
diff --git a/website/src/ja/docs/features/direct-message.md b/website/src/ja/docs/features/direct-message.md
index ae943e6..06174d5 100644
--- a/website/src/ja/docs/features/direct-message.md
+++ b/website/src/ja/docs/features/direct-message.md
@@ -30,6 +30,8 @@ layout: doc
クイック返信を利用するには `config.yml` で `features.quickReplies.enabled` が `true` (デフォルト) である必要があります.
+## クロスサーバーチャット
+
## 通知設定
プレイヤーはダイレクトメッセージ受信時のサウンド通知を個別に制御できます.