summaryrefslogtreecommitdiff
path: root/platform-paper
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-02-25 22:38:19 +0900
committerSho Sakuma <me@m1sk9.dev>2026-02-25 22:38:44 +0900
commitf4bf20791f4470d3e9b865a53b610b5f277ebb82 (patch)
tree7d0b3f9d3eff87730413290d4436b43ff6146b7a /platform-paper
parent56ed04185036635dcfd2763391cd64b8be371b1a (diff)
downloadLunaticChat-f4bf20791f4470d3e9b865a53b610b5f277ebb82.tar.gz
LunaticChat-f4bf20791f4470d3e9b865a53b610b5f277ebb82.tar.bz2
LunaticChat-f4bf20791f4470d3e9b865a53b610b5f277ebb82.zip
feat: add Codecov/Jacoco integration and expand test coverage
- Add Jacoco plugin to all subprojects with XML report generation - Replace post-test-results.sh with Codecov upload in CI workflow - Add codecov.yml configuration - Add 140 new tests across engine, platform-paper, and platform-velocity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'platform-paper')
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt513
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt384
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKeyTest.kt50
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt159
4 files changed, 1106 insertions, 0 deletions
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt
new file mode 100644
index 0000000..acf610f
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt
@@ -0,0 +1,513 @@
+package dev.m1sk9.lunaticChat.paper.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMember
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.exception.ChannelLimitExceededException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberLimitExceededException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerAlreadyBannedException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerNotBannedException
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.TestUtils.createTestChannel
+import dev.m1sk9.lunaticChat.paper.TestUtils.createTestUUID
+import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertIs
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ChannelManagerTest {
+ private fun createManager(
+ initialData: ChannelData = ChannelData(),
+ maxChannelsPerServer: Int = 10,
+ maxMembersPerChannel: Int = 50,
+ ): Triple<ChannelManager, ChannelStorage, TestUtils.TestLogger> {
+ val logger = TestUtils.TestLogger()
+ val storage = mockk<ChannelStorage>(relaxed = true)
+ val config =
+ ChannelChatFeatureConfig(
+ enabled = true,
+ maxChannelsPerServer = maxChannelsPerServer,
+ maxMembersPerChannel = maxMembersPerChannel,
+ )
+
+ every { storage.loadFromDisk() } returns initialData
+
+ val manager = ChannelManager(storage, logger, config)
+ return Triple(manager, storage, logger)
+ }
+
+ @Test
+ fun `initialize should load channels from storage`() {
+ val ownerId = createTestUUID(1)
+ val channel = createTestChannel(id = "test-ch", name = "Test", ownerId = ownerId)
+ val members =
+ listOf(
+ ChannelMember(channelId = "test-ch", playerId = ownerId, role = ChannelRole.OWNER),
+ )
+ val data =
+ ChannelData(
+ channels = mapOf("test-ch" to channel),
+ members = mapOf("test-ch" to members),
+ )
+
+ val (manager, _, logger) = createManager(initialData = data)
+ manager.initialize()
+
+ val result = manager.getChannel("test-ch")
+ assertTrue(result.isSuccess)
+ assertEquals("test-ch", result.getOrThrow().id)
+ assertTrue(logger.infoMessages.any { it.contains("1 channels") })
+ }
+
+ @Test
+ fun `createChannel should succeed for new channel`() {
+ val (manager, storage, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val channel = createTestChannel(id = "new-ch", name = "New Channel", ownerId = ownerId)
+
+ val result = manager.createChannel(channel)
+
+ assertTrue(result.isSuccess)
+ assertEquals("new-ch", result.getOrThrow().id)
+ verify(atLeast = 1) { storage.queueAsyncSave(any()) }
+ }
+
+ @Test
+ fun `createChannel should auto-add owner as member`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val channel = createTestChannel(id = "new-ch", name = "New Channel", ownerId = ownerId)
+ manager.createChannel(channel)
+
+ val members = manager.getChannelMembers("new-ch")
+ assertTrue(members.isSuccess)
+ assertEquals(1, members.getOrThrow().size)
+ assertEquals(ownerId, members.getOrThrow()[0].playerId)
+ assertEquals(ChannelRole.OWNER, members.getOrThrow()[0].role)
+ }
+
+ @Test
+ fun `createChannel should set owner active channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val channel = createTestChannel(id = "new-ch", name = "New Channel", ownerId = ownerId)
+ manager.createChannel(channel)
+
+ assertEquals("new-ch", manager.getPlayerChannel(ownerId))
+ }
+
+ @Test
+ fun `createChannel should fail for duplicate ID`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val channel = createTestChannel(id = "dup-ch", name = "Channel", ownerId = ownerId)
+ manager.createChannel(channel)
+
+ val result = manager.createChannel(channel)
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotFoundException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `createChannel should fail when limit reached`() {
+ val (manager, _, _) = createManager(maxChannelsPerServer = 1)
+ manager.initialize()
+
+ val owner1 = createTestUUID(1)
+ val owner2 = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "ch1", name = "Ch1", ownerId = owner1))
+
+ val result = manager.createChannel(createTestChannel(id = "ch2", name = "Ch2", ownerId = owner2))
+ assertTrue(result.isFailure)
+ assertIs<ChannelLimitExceededException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `createChannel with unlimited limit should allow many channels`() {
+ val (manager, _, _) = createManager(maxChannelsPerServer = 0)
+ manager.initialize()
+
+ repeat(5) { i ->
+ val result =
+ manager.createChannel(
+ createTestChannel(id = "ch-$i", name = "Channel $i", ownerId = createTestUUID(i)),
+ )
+ assertTrue(result.isSuccess)
+ }
+ }
+
+ @Test
+ fun `deleteChannel should succeed for owner`() {
+ val (manager, storage, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "del-ch", name = "To Delete", ownerId = ownerId))
+
+ val result = manager.deleteChannel("del-ch", ownerId)
+ assertTrue(result.isSuccess)
+
+ val getResult = manager.getChannel("del-ch")
+ assertTrue(getResult.isFailure)
+ }
+
+ @Test
+ fun `deleteChannel should fail for non-owner without bypass`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val otherId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "del-ch", name = "Channel", ownerId = ownerId))
+
+ val result = manager.deleteChannel("del-ch", otherId)
+ assertTrue(result.isFailure)
+ assertIs<ChannelNoOwnerPermissionException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `deleteChannel should succeed for non-owner with bypass permission`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val adminId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "del-ch", name = "Channel", ownerId = ownerId))
+
+ val result = manager.deleteChannel("del-ch", adminId, hasBypassPermission = true)
+ assertTrue(result.isSuccess)
+ }
+
+ @Test
+ fun `deleteChannel should fail for nonexistent channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val result = manager.deleteChannel("nonexistent", createTestUUID(1))
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotFoundException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `deleteChannel should clear active channels for affected players`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "del-ch", name = "Channel", ownerId = ownerId))
+ assertEquals("del-ch", manager.getPlayerChannel(ownerId))
+
+ manager.deleteChannel("del-ch", ownerId)
+ assertNull(manager.getPlayerChannel(ownerId))
+ }
+
+ @Test
+ fun `getChannel should return channel when exists`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "get-ch", name = "Get Channel", ownerId = ownerId))
+
+ val result = manager.getChannel("get-ch")
+ assertTrue(result.isSuccess)
+ assertEquals("Get Channel", result.getOrThrow().name)
+ }
+
+ @Test
+ fun `getChannel should fail for nonexistent channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val result = manager.getChannel("nonexistent")
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotFoundException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `getAllChannels should return all channels`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ manager.createChannel(createTestChannel(id = "ch1", name = "Ch1", ownerId = createTestUUID(1)))
+ manager.createChannel(createTestChannel(id = "ch2", name = "Ch2", ownerId = createTestUUID(2)))
+
+ val result = manager.getAllChannels()
+ assertTrue(result.isSuccess)
+ assertEquals(2, result.getOrThrow().size)
+ }
+
+ @Test
+ fun `getPublicChannels should exclude private channels`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ manager.createChannel(createTestChannel(id = "public-ch", name = "Public", ownerId = createTestUUID(1)))
+ manager.createChannel(
+ createTestChannel(id = "private-ch", name = "Private", ownerId = createTestUUID(2), isPrivate = true),
+ )
+
+ val result = manager.getPublicChannels()
+ assertTrue(result.isSuccess)
+ assertEquals(1, result.getOrThrow().size)
+ assertEquals("public-ch", result.getOrThrow()[0].id)
+ }
+
+ @Test
+ fun `addMember should add member to channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val memberId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "mem-ch", name = "Channel", ownerId = ownerId))
+
+ val result = manager.addMember("mem-ch", memberId, ChannelRole.MEMBER)
+ assertTrue(result.isSuccess)
+
+ val members = manager.getChannelMembers("mem-ch").getOrThrow()
+ assertEquals(2, members.size)
+ assertTrue(members.any { it.playerId == memberId })
+ }
+
+ @Test
+ fun `addMember should fail when member limit reached`() {
+ val (manager, _, _) = createManager(maxMembersPerChannel = 1)
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "lim-ch", name = "Channel", ownerId = ownerId))
+
+ val result = manager.addMember("lim-ch", createTestUUID(2), ChannelRole.MEMBER)
+ assertTrue(result.isFailure)
+ assertIs<ChannelMemberLimitExceededException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `addMember should fail for nonexistent channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val result = manager.addMember("nonexistent", createTestUUID(1), ChannelRole.MEMBER)
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotFoundException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `removeMember should remove member from channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val memberId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "rem-ch", name = "Channel", ownerId = ownerId))
+ manager.addMember("rem-ch", memberId, ChannelRole.MEMBER)
+
+ val result = manager.removeMember("rem-ch", memberId)
+ assertTrue(result.isSuccess)
+
+ val members = manager.getChannelMembers("rem-ch").getOrThrow()
+ assertFalse(members.any { it.playerId == memberId })
+ }
+
+ @Test
+ fun `banPlayer should add to banned list and remove from members`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val bannedId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "ban-ch", name = "Channel", ownerId = ownerId))
+ manager.addMember("ban-ch", bannedId, ChannelRole.MEMBER)
+
+ val result = manager.banPlayer("ban-ch", bannedId)
+ assertTrue(result.isSuccess)
+ assertTrue(result.getOrThrow().bannedPlayers.contains(bannedId))
+
+ val members = manager.getChannelMembers("ban-ch").getOrThrow()
+ assertFalse(members.any { it.playerId == bannedId })
+ }
+
+ @Test
+ fun `banPlayer should clear active channel for banned player`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val bannedId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "ban-ch", name = "Channel", ownerId = ownerId))
+ manager.addMember("ban-ch", bannedId, ChannelRole.MEMBER)
+ manager.setPlayerChannel(bannedId, "ban-ch")
+
+ manager.banPlayer("ban-ch", bannedId)
+ assertNull(manager.getPlayerChannel(bannedId))
+ }
+
+ @Test
+ fun `banPlayer should fail if already banned`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val bannedId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "ban-ch", name = "Channel", ownerId = ownerId))
+ manager.banPlayer("ban-ch", bannedId)
+
+ val result = manager.banPlayer("ban-ch", bannedId)
+ assertTrue(result.isFailure)
+ assertIs<ChannelPlayerAlreadyBannedException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `unbanPlayer should remove from banned list`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val bannedId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "unban-ch", name = "Channel", ownerId = ownerId))
+ manager.banPlayer("unban-ch", bannedId)
+
+ val result = manager.unbanPlayer("unban-ch", bannedId)
+ assertTrue(result.isSuccess)
+ assertFalse(result.getOrThrow().bannedPlayers.contains(bannedId))
+ }
+
+ @Test
+ fun `unbanPlayer should fail if not banned`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "unban-ch", name = "Channel", ownerId = ownerId))
+
+ val result = manager.unbanPlayer("unban-ch", createTestUUID(2))
+ assertTrue(result.isFailure)
+ assertIs<ChannelPlayerNotBannedException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `isPlayerBanned should return true for banned player`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val bannedId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "ban-ch", name = "Channel", ownerId = ownerId))
+ manager.banPlayer("ban-ch", bannedId)
+
+ assertTrue(manager.isPlayerBanned("ban-ch", bannedId).getOrThrow())
+ }
+
+ @Test
+ fun `isPlayerBanned should return false for non-banned player`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "ban-ch", name = "Channel", ownerId = ownerId))
+
+ assertFalse(manager.isPlayerBanned("ban-ch", createTestUUID(2)).getOrThrow())
+ }
+
+ @Test
+ fun `updateMemberRole should change member role`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val memberId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "role-ch", name = "Channel", ownerId = ownerId))
+ manager.addMember("role-ch", memberId, ChannelRole.MEMBER)
+
+ val result = manager.updateMemberRole("role-ch", memberId, ChannelRole.MODERATOR)
+ assertTrue(result.isSuccess)
+
+ val members = manager.getChannelMembers("role-ch").getOrThrow()
+ val updatedMember = members.find { it.playerId == memberId }
+ assertNotNull(updatedMember)
+ assertEquals(ChannelRole.MODERATOR, updatedMember.role)
+ }
+
+ @Test
+ fun `updateChannelOwner should transfer ownership`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val oldOwnerId = createTestUUID(1)
+ val newOwnerId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "own-ch", name = "Channel", ownerId = oldOwnerId))
+ manager.addMember("own-ch", newOwnerId, ChannelRole.MEMBER)
+
+ val result = manager.updateChannelOwner("own-ch", newOwnerId)
+ assertTrue(result.isSuccess)
+ assertEquals(newOwnerId, result.getOrThrow().ownerId)
+
+ val members = manager.getChannelMembers("own-ch").getOrThrow()
+ assertEquals(ChannelRole.OWNER, members.find { it.playerId == newOwnerId }?.role)
+ assertEquals(ChannelRole.MODERATOR, members.find { it.playerId == oldOwnerId }?.role)
+ }
+
+ @Test
+ fun `setPlayerChannel should set and clear active channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val playerId = createTestUUID(1)
+
+ manager.setPlayerChannel(playerId, "test-ch")
+ assertEquals("test-ch", manager.getPlayerChannel(playerId))
+
+ manager.setPlayerChannel(playerId, null)
+ assertNull(manager.getPlayerChannel(playerId))
+ }
+
+ @Test
+ fun `getPlayerChannelContext should return context for active channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "ctx-ch", name = "Context Channel", ownerId = ownerId))
+
+ val context = manager.getPlayerChannelContext(ownerId)
+ assertNotNull(context)
+ assertEquals("ctx-ch", context.channelId)
+ assertEquals("Context Channel", context.channel.name)
+ }
+
+ @Test
+ fun `getPlayerChannelContext should return null for no active channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ assertNull(manager.getPlayerChannelContext(createTestUUID(99)))
+ }
+
+ @Test
+ fun `saveToDisk should call storage saveToDisk`() {
+ val (manager, storage, _) = createManager()
+ manager.initialize()
+
+ manager.saveToDisk()
+
+ verify { storage.saveToDisk(any()) }
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt
new file mode 100644
index 0000000..1fcc39b
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt
@@ -0,0 +1,384 @@
+package dev.m1sk9.lunaticChat.paper.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerMembershipLimitExceededException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.TestUtils.createTestChannel
+import dev.m1sk9.lunaticChat.paper.TestUtils.createTestUUID
+import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig
+import io.mockk.every
+import io.mockk.mockk
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertIs
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ChannelMembershipManagerTest {
+ private fun createManagers(
+ initialData: ChannelData = ChannelData(),
+ maxChannelsPerServer: Int = 10,
+ maxMembersPerChannel: Int = 50,
+ maxMembershipPerPlayer: Int = 5,
+ ): Triple<ChannelMembershipManager, ChannelManager, TestUtils.TestLogger> {
+ val logger = TestUtils.TestLogger()
+ val storage = mockk<ChannelStorage>(relaxed = true)
+ val config =
+ ChannelChatFeatureConfig(
+ enabled = true,
+ maxChannelsPerServer = maxChannelsPerServer,
+ maxMembersPerChannel = maxMembersPerChannel,
+ maxMembershipPerPlayer = maxMembershipPerPlayer,
+ )
+
+ every { storage.loadFromDisk() } returns initialData
+
+ val channelManager = ChannelManager(storage, logger, config)
+ channelManager.initialize()
+
+ val membershipManager = ChannelMembershipManager(channelManager, logger, config)
+ return Triple(membershipManager, channelManager, logger)
+ }
+
+ @Test
+ fun `joinChannel should succeed for public channel`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "pub-ch", name = "Public", ownerId = ownerId))
+
+ val result = membership.joinChannel(playerId, "pub-ch")
+ assertTrue(result.isSuccess)
+ }
+
+ @Test
+ fun `joinChannel should set channel as active`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "pub-ch", name = "Public", ownerId = ownerId))
+
+ membership.joinChannel(playerId, "pub-ch")
+ assertEquals("pub-ch", channelManager.getPlayerChannel(playerId))
+ }
+
+ @Test
+ fun `joinChannel should fail for nonexistent channel`() {
+ val (membership, _, _) = createManagers()
+
+ val result = membership.joinChannel(createTestUUID(1), "nonexistent")
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotFoundException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `joinChannel should fail if channel is already active`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ channelManager.createChannel(createTestChannel(id = "active-ch", name = "Active", ownerId = ownerId))
+
+ // Owner already has this as active channel after creation
+ val result = membership.joinChannel(ownerId, "active-ch")
+ assertTrue(result.isFailure)
+ assertIs<ChannelAlreadyActiveException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `joinChannel should fail if player is banned`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val bannedId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "ban-ch", name = "Channel", ownerId = ownerId))
+ channelManager.banPlayer("ban-ch", bannedId)
+
+ val result = membership.joinChannel(bannedId, "ban-ch")
+ assertTrue(result.isFailure)
+ assertIs<ChannelPlayerBannedException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `joinChannel should fail for private channel without bypass`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(
+ createTestChannel(id = "priv-ch", name = "Private", ownerId = ownerId, isPrivate = true),
+ )
+
+ val result = membership.joinChannel(playerId, "priv-ch")
+ assertTrue(result.isFailure)
+ assertIs<ChannelPrivateRequiresInvitationException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `joinChannel should succeed for private channel with bypass`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(
+ createTestChannel(id = "priv-ch", name = "Private", ownerId = ownerId, isPrivate = true),
+ )
+
+ val result = membership.joinChannel(playerId, "priv-ch", bypassPrivateCheck = true)
+ assertTrue(result.isSuccess)
+ }
+
+ @Test
+ fun `joinChannel should fail if already a member`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "mem-ch", name = "Channel", ownerId = ownerId))
+ membership.joinChannel(playerId, "mem-ch")
+
+ // Switch active to another channel first so we can try joining again
+ channelManager.createChannel(createTestChannel(id = "other-ch", name = "Other", ownerId = createTestUUID(3)))
+ channelManager.setPlayerChannel(playerId, "other-ch")
+
+ val result = membership.joinChannel(playerId, "mem-ch")
+ assertTrue(result.isFailure)
+ assertIs<ChannelMemberAlreadyException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `joinChannel should fail when membership limit reached`() {
+ val (membership, channelManager, _) = createManagers(maxMembershipPerPlayer = 1)
+
+ val ownerId1 = createTestUUID(1)
+ val ownerId2 = createTestUUID(2)
+ val playerId = createTestUUID(3)
+
+ channelManager.createChannel(createTestChannel(id = "ch1", name = "Ch1", ownerId = ownerId1))
+ channelManager.createChannel(createTestChannel(id = "ch2", name = "Ch2", ownerId = ownerId2))
+
+ membership.joinChannel(playerId, "ch1")
+
+ // Clear active channel to try joining second
+ channelManager.setPlayerChannel(playerId, null)
+
+ val result = membership.joinChannel(playerId, "ch2")
+ assertTrue(result.isFailure)
+ assertIs<ChannelPlayerMembershipLimitExceededException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `leaveChannel should remove from membership and clear active`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "leave-ch", name = "Channel", ownerId = ownerId))
+ membership.joinChannel(playerId, "leave-ch")
+
+ val result = membership.leaveChannel(playerId)
+ assertTrue(result.isSuccess)
+ assertNull(channelManager.getPlayerChannel(playerId))
+ }
+
+ @Test
+ fun `leaveChannel should fail if no active channel`() {
+ val (membership, _, _) = createManagers()
+
+ val result = membership.leaveChannel(createTestUUID(99))
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotMemberException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `switchChannel should switch to another member channel`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId1 = createTestUUID(1)
+ val ownerId2 = createTestUUID(2)
+ val playerId = createTestUUID(3)
+
+ channelManager.createChannel(createTestChannel(id = "ch1", name = "Ch1", ownerId = ownerId1))
+ channelManager.createChannel(createTestChannel(id = "ch2", name = "Ch2", ownerId = ownerId2))
+
+ membership.joinChannel(playerId, "ch1")
+ // Now switch active to ch2 first by joining
+ channelManager.setPlayerChannel(playerId, null)
+ membership.joinChannel(playerId, "ch2")
+
+ // Switch back to ch1
+ val result = membership.switchChannel(playerId, "ch1")
+ assertTrue(result.isSuccess)
+ assertEquals("ch1", channelManager.getPlayerChannel(playerId))
+ }
+
+ @Test
+ fun `switchChannel should fail for nonexistent channel`() {
+ val (membership, _, _) = createManagers()
+
+ val result = membership.switchChannel(createTestUUID(1), "nonexistent")
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotFoundException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `switchChannel should fail if already active`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "sw-ch", name = "Channel", ownerId = ownerId))
+ membership.joinChannel(playerId, "sw-ch")
+
+ val result = membership.switchChannel(playerId, "sw-ch")
+ assertTrue(result.isFailure)
+ assertIs<ChannelAlreadyActiveException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `switchChannel should fail if not a member`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val playerId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "sw-ch", name = "Channel", ownerId = ownerId))
+
+ val result = membership.switchChannel(playerId, "sw-ch")
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotMemberException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `isMember should return true for members`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ channelManager.createChannel(createTestChannel(id = "mem-ch", name = "Channel", ownerId = ownerId))
+
+ assertTrue(membership.isMember(ownerId, "mem-ch").getOrThrow())
+ }
+
+ @Test
+ fun `isMember should return false for non-members`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ channelManager.createChannel(createTestChannel(id = "mem-ch", name = "Channel", ownerId = ownerId))
+
+ assertFalse(membership.isMember(createTestUUID(2), "mem-ch").getOrThrow())
+ }
+
+ @Test
+ fun `getMemberRole should return role for member`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ channelManager.createChannel(createTestChannel(id = "role-ch", name = "Channel", ownerId = ownerId))
+
+ assertEquals(ChannelRole.OWNER, membership.getMemberRole(ownerId, "role-ch").getOrThrow())
+ }
+
+ @Test
+ fun `getMemberRole should fail for non-member`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ channelManager.createChannel(createTestChannel(id = "role-ch", name = "Channel", ownerId = ownerId))
+
+ val result = membership.getMemberRole(createTestUUID(2), "role-ch")
+ assertTrue(result.isFailure)
+ assertIs<ChannelNotMemberException>(result.exceptionOrNull())
+ }
+
+ @Test
+ fun `getMemberRoleOrNull should return null for non-member`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ channelManager.createChannel(createTestChannel(id = "role-ch", name = "Channel", ownerId = ownerId))
+
+ assertNull(membership.getMemberRoleOrNull(createTestUUID(2), "role-ch"))
+ }
+
+ @Test
+ fun `hasRole OWNER should return true only for owner`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val memberId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "has-ch", name = "Channel", ownerId = ownerId))
+ channelManager.addMember("has-ch", memberId, ChannelRole.MEMBER)
+
+ assertTrue(membership.hasRole(ownerId, "has-ch", ChannelRole.OWNER).getOrThrow())
+ assertFalse(membership.hasRole(memberId, "has-ch", ChannelRole.OWNER).getOrThrow())
+ }
+
+ @Test
+ fun `hasRole MODERATOR should return true for moderator and owner`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val modId = createTestUUID(2)
+ val memberId = createTestUUID(3)
+ channelManager.createChannel(createTestChannel(id = "has-ch", name = "Channel", ownerId = ownerId))
+ channelManager.addMember("has-ch", modId, ChannelRole.MODERATOR)
+ channelManager.addMember("has-ch", memberId, ChannelRole.MEMBER)
+
+ assertTrue(membership.hasRole(ownerId, "has-ch", ChannelRole.MODERATOR).getOrThrow())
+ assertTrue(membership.hasRole(modId, "has-ch", ChannelRole.MODERATOR).getOrThrow())
+ assertFalse(membership.hasRole(memberId, "has-ch", ChannelRole.MODERATOR).getOrThrow())
+ }
+
+ @Test
+ fun `hasRole MEMBER should return true for all members`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ val memberId = createTestUUID(2)
+ channelManager.createChannel(createTestChannel(id = "has-ch", name = "Channel", ownerId = ownerId))
+ channelManager.addMember("has-ch", memberId, ChannelRole.MEMBER)
+
+ assertTrue(membership.hasRole(ownerId, "has-ch", ChannelRole.MEMBER).getOrThrow())
+ assertTrue(membership.hasRole(memberId, "has-ch", ChannelRole.MEMBER).getOrThrow())
+ }
+
+ @Test
+ fun `hasRole should return false for non-member`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId = createTestUUID(1)
+ channelManager.createChannel(createTestChannel(id = "has-ch", name = "Channel", ownerId = ownerId))
+
+ assertFalse(membership.hasRole(createTestUUID(99), "has-ch", ChannelRole.MEMBER).getOrThrow())
+ }
+
+ @Test
+ fun `getPlayerChannels should return all channels player is member of`() {
+ val (membership, channelManager, _) = createManagers()
+
+ val ownerId1 = createTestUUID(1)
+ val ownerId2 = createTestUUID(2)
+ val playerId = createTestUUID(3)
+
+ channelManager.createChannel(createTestChannel(id = "ch1", name = "Ch1", ownerId = ownerId1))
+ channelManager.createChannel(createTestChannel(id = "ch2", name = "Ch2", ownerId = ownerId2))
+
+ membership.joinChannel(playerId, "ch1")
+ channelManager.setPlayerChannel(playerId, null)
+ membership.joinChannel(playerId, "ch2")
+
+ val channels = membership.getPlayerChannels(playerId).getOrThrow()
+ assertEquals(2, channels.size)
+ assertTrue(channels.contains("ch1"))
+ assertTrue(channels.contains("ch2"))
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKeyTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKeyTest.kt
new file mode 100644
index 0000000..635d7e1
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKeyTest.kt
@@ -0,0 +1,50 @@
+package dev.m1sk9.lunaticChat.paper.command.setting
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNull
+
+class SettingKeyTest {
+ @Test
+ fun `fromString should resolve japanese key`() {
+ val result = SettingKey.fromString("japanese")
+ assertIs<SettingKey.Japanese>(result)
+ }
+
+ @Test
+ fun `fromString should resolve notice key`() {
+ val result = SettingKey.fromString("notice")
+ assertIs<SettingKey.Notice>(result)
+ }
+
+ @Test
+ fun `fromString should resolve chNotice key`() {
+ val result = SettingKey.fromString("chNotice")
+ assertIs<SettingKey.ChNotice>(result)
+ }
+
+ @Test
+ fun `fromString should be case insensitive`() {
+ assertIs<SettingKey.Japanese>(SettingKey.fromString("JAPANESE"))
+ assertIs<SettingKey.Notice>(SettingKey.fromString("NOTICE"))
+ assertIs<SettingKey.ChNotice>(SettingKey.fromString("CHNOTICE"))
+ assertIs<SettingKey.Japanese>(SettingKey.fromString("Japanese"))
+ }
+
+ @Test
+ fun `fromString should return null for unknown key`() {
+ assertNull(SettingKey.fromString("unknown"))
+ assertNull(SettingKey.fromString(""))
+ assertNull(SettingKey.fromString("invalid"))
+ }
+
+ @Test
+ fun `values should return all setting keys`() {
+ val values = SettingKey.values()
+ assertEquals(3, values.size)
+ assertIs<SettingKey.Japanese>(values[0])
+ assertIs<SettingKey.Notice>(values[1])
+ assertIs<SettingKey.ChNotice>(values[2])
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt
new file mode 100644
index 0000000..bc716c9
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt
@@ -0,0 +1,159 @@
+package dev.m1sk9.lunaticChat.paper.settings
+
+import dev.m1sk9.lunaticChat.engine.settings.PlayerChatSettings
+import dev.m1sk9.lunaticChat.engine.settings.PlayerSettingsData
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.TestUtils.createTestUUID
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class PlayerSettingsManagerTest {
+ private fun createManager(
+ initialData: PlayerSettingsData = PlayerSettingsData(),
+ ): Triple<PlayerSettingsManager, YamlPlayerSettingsStorage, TestUtils.TestLogger> {
+ val logger = TestUtils.TestLogger()
+ val storage = mockk<YamlPlayerSettingsStorage>(relaxed = true)
+
+ every { storage.loadFromDisk() } returns initialData
+
+ val manager = PlayerSettingsManager(storage, logger)
+ return Triple(manager, storage, logger)
+ }
+
+ @Test
+ fun `initialize should load settings from storage`() {
+ val playerId = createTestUUID(1)
+ val data =
+ PlayerSettingsData(
+ japaneseConversion = mapOf(playerId to false),
+ directMessageNotification = mapOf(playerId to false),
+ channelMessageNotification = mapOf(playerId to false),
+ )
+
+ val (manager, _, logger) = createManager(initialData = data)
+ manager.initialize()
+
+ val settings = manager.getSettings(playerId)
+ assertFalse(settings.japaneseConversionEnabled)
+ assertFalse(settings.directMessageNotificationEnabled)
+ assertFalse(settings.channelMessageNotificationEnabled)
+ assertTrue(logger.infoMessages.any { it.contains("1 players") })
+ }
+
+ @Test
+ fun `getSettings should return defaults for unknown player`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val settings = manager.getSettings(createTestUUID(99))
+
+ assertTrue(settings.japaneseConversionEnabled)
+ assertTrue(settings.directMessageNotificationEnabled)
+ assertTrue(settings.channelMessageNotificationEnabled)
+ }
+
+ @Test
+ fun `updateSettings should update cache and queue save`() {
+ val (manager, storage, _) = createManager()
+ manager.initialize()
+
+ val playerId = createTestUUID(1)
+ val settings =
+ PlayerChatSettings(
+ uuid = playerId,
+ japaneseConversionEnabled = false,
+ directMessageNotificationEnabled = false,
+ channelMessageNotificationEnabled = true,
+ )
+
+ manager.updateSettings(settings)
+
+ val retrieved = manager.getSettings(playerId)
+ assertFalse(retrieved.japaneseConversionEnabled)
+ assertFalse(retrieved.directMessageNotificationEnabled)
+ assertTrue(retrieved.channelMessageNotificationEnabled)
+
+ verify(exactly = 1) { storage.queueAsyncSave(any()) }
+ }
+
+ @Test
+ fun `updateSettings should overwrite existing settings`() {
+ val playerId = createTestUUID(1)
+ val data =
+ PlayerSettingsData(
+ japaneseConversion = mapOf(playerId to true),
+ directMessageNotification = mapOf(playerId to true),
+ channelMessageNotification = mapOf(playerId to true),
+ )
+
+ val (manager, _, _) = createManager(initialData = data)
+ manager.initialize()
+
+ manager.updateSettings(
+ PlayerChatSettings(
+ uuid = playerId,
+ japaneseConversionEnabled = false,
+ directMessageNotificationEnabled = false,
+ channelMessageNotificationEnabled = false,
+ ),
+ )
+
+ val settings = manager.getSettings(playerId)
+ assertFalse(settings.japaneseConversionEnabled)
+ assertFalse(settings.directMessageNotificationEnabled)
+ assertFalse(settings.channelMessageNotificationEnabled)
+ }
+
+ @Test
+ fun `getSettings should return correct UUID`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val playerId = createTestUUID(1)
+ val settings = manager.getSettings(playerId)
+
+ assertEquals(playerId, settings.uuid)
+ }
+
+ @Test
+ fun `saveToDisk should call storage saveToDisk`() {
+ val (manager, storage, _) = createManager()
+ manager.initialize()
+
+ manager.saveToDisk()
+
+ verify(exactly = 1) { storage.saveToDisk(any()) }
+ }
+
+ @Test
+ fun `multiple players should have independent settings`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val player1 = createTestUUID(1)
+ val player2 = createTestUUID(2)
+
+ manager.updateSettings(
+ PlayerChatSettings(uuid = player1, japaneseConversionEnabled = false),
+ )
+ manager.updateSettings(
+ PlayerChatSettings(uuid = player2, japaneseConversionEnabled = true),
+ )
+
+ assertFalse(manager.getSettings(player1).japaneseConversionEnabled)
+ assertTrue(manager.getSettings(player2).japaneseConversionEnabled)
+ }
+
+ @Test
+ fun `initialize with empty data should work`() {
+ val (manager, _, logger) = createManager(PlayerSettingsData())
+ manager.initialize()
+
+ assertTrue(logger.infoMessages.any { it.contains("0 players") })
+ }
+}