summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.github/scripts/post-test-results.sh101
-rw-r--r--.github/workflows/ci.yaml22
-rw-r--r--README.md1
-rw-r--r--build.gradle.kts11
-rw-r--r--codecov.yml15
-rw-r--r--engine/build.gradle.kts2
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeTest.kt33
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt200
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelTest.kt139
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResultTest.kt54
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelExceptionTest.kt279
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt82
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt233
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt57
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/SettingsDataClassesTest.kt167
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt100
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt2
-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/impl/lc/StatusCommandTest.kt125
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt137
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommandTest.kt128
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommandTest.kt95
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommandTest.kt123
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt152
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommandTest.kt151
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt136
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommandTest.kt125
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommandTest.kt107
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt147
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt139
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt142
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommandTest.kt108
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt121
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommandTest.kt88
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt101
-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/command/setting/handler/SettingHandlerTest.kt191
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt159
-rw-r--r--platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelayTest.kt136
-rw-r--r--platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt211
56 files changed, 5170 insertions, 127 deletions
diff --git a/.github/scripts/post-test-results.sh b/.github/scripts/post-test-results.sh
deleted file mode 100755
index f8fbdf2..0000000
--- a/.github/scripts/post-test-results.sh
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/bin/bash
-set -e
-
-MARKER="<!-- test-results-comment -->"
-
-TOTAL_TESTS=0
-PASSED_TESTS=0
-FAILED_TESTS=0
-SKIPPED_TESTS=0
-FAILED_TEST_NAMES=""
-
-for xml_file in */build/test-results/test/*.xml; do
- if [ -f "$xml_file" ]; then
- tests=$(sed -n 's/.*tests="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1)
- failures=$(sed -n 's/.*failures="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1)
- skipped=$(sed -n 's/.*skipped="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1)
-
- tests=${tests:-0}
- failures=${failures:-0}
- skipped=${skipped:-0}
-
- TOTAL_TESTS=$((TOTAL_TESTS + tests))
- FAILED_TESTS=$((FAILED_TESTS + failures))
- SKIPPED_TESTS=$((SKIPPED_TESTS + skipped))
-
- if [ "$failures" -gt 0 ]; then
- failed_names=$(sed -n 's/.*<testcase name="\([^"]*\)".*<failure.*/\1/p' "$xml_file" || echo "")
- if [ -n "$failed_names" ]; then
- FAILED_TEST_NAMES="${FAILED_TEST_NAMES}${failed_names}\n"
- fi
- fi
- fi
-done
-
-PASSED_TESTS=$((TOTAL_TESTS - FAILED_TESTS - SKIPPED_TESTS))
-
-if [ "$FAILED_TESTS" -gt 0 ]; then
- STATUS="❌ Failed"
- STATUS_EMOJI="❌"
-else
- STATUS="✅ Passed"
- STATUS_EMOJI="✅"
-fi
-
-COMMENT_BODY="${MARKER}
-## ${STATUS_EMOJI} Test Results
-
-| Status | Count |
-|--------|-------|
-| ✅ Passed | ${PASSED_TESTS} |
-| ❌ Failed | ${FAILED_TESTS} |
-| ⏭️ Skipped | ${SKIPPED_TESTS} |
-| **Total** | **${TOTAL_TESTS}** |
-
-**Overall Status:** ${STATUS}
-"
-
-if [ "$FAILED_TESTS" -gt 0 ] && [ -n "$FAILED_TEST_NAMES" ]; then
- COMMENT_BODY="${COMMENT_BODY}
-
-<details>
-<summary>❌ Failed Tests</summary>
-
-$(echo -e "$FAILED_TEST_NAMES" | sed 's/^/- /')
-
-</details>
-"
-fi
-
-COMMENT_BODY="${COMMENT_BODY}
-
----
-*Updated at $(date -u '+%Y-%m-%d %H:%M:%S UTC')*"
-
-echo "$COMMENT_BODY" > /tmp/test-comment.md
-
-PR_NUMBER="${GITHUB_REF#refs/pull/}"
-PR_NUMBER="${PR_NUMBER%/merge}"
-
-if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "$GITHUB_REF" ]; then
- echo "Not a pull request, skipping comment"
- exit 0
-fi
-
-EXISTING_COMMENT_ID=$(gh api \
- "repos/$GITHUB_REPOSITORY/issues/${PR_NUMBER}/comments" \
- --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" \
- | head -n 1 || echo "")
-
-if [ -n "$EXISTING_COMMENT_ID" ]; then
- echo "Updating existing comment (ID: $EXISTING_COMMENT_ID)"
- gh api \
- "repos/$GITHUB_REPOSITORY/issues/comments/${EXISTING_COMMENT_ID}" \
- -X PATCH \
- -f body="$(cat /tmp/test-comment.md)"
-else
- echo "Creating new comment"
- gh pr comment "$PR_NUMBER" --body-file /tmp/test-comment.md
-fi
-
-echo "Test results comment posted successfully"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 2f3a9e8..7ab33b6 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -29,16 +29,18 @@ jobs:
run: ./gradlew ktlintCheck --no-daemon
- name: Run tests
- run: ./gradlew test --no-daemon
- continue-on-error: true
-
- - name: Post test results to PR
- if: github.event_name == 'pull_request' && always()
- env:
- GH_TOKEN: ${{ github.token }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_REF: ${{ github.ref }}
- run: .github/scripts/post-test-results.sh
+ run: ./gradlew test jacocoTestReport --no-daemon
+
+ - name: Upload coverage to Codecov
+ if: always()
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: |
+ engine/build/reports/jacoco/test/jacocoTestReport.xml,
+ platform-paper/build/reports/jacoco/test/jacocoTestReport.xml,
+ platform-velocity/build/reports/jacoco/test/jacocoTestReport.xml
+ fail_ci_if_error: false
- name: Build with shadowJar
run: ./gradlew :platform-paper:shadowJar :platform-velocity:shadowJar --parallel --no-daemon
diff --git a/README.md b/README.md
index 0ce71c6..faf3877 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
[![Deploy Dokka](https://github.com/m1sk9/LunaticChat/actions/workflows/dokka.yaml/badge.svg)](https://github.com/m1sk9/LunaticChat/actions/workflows/dokka.yaml)
[![GNU General Public License v3.0](https://img.shields.io/github/license/m1sk9/LunaticChat?color=%239944ee)](https://github.com/m1sk9/LunaticChat/blob/main/LICENSE)
![Modrinth Downloads](https://img.shields.io/modrinth/dt/MBeAdO4L)
+[![codecov](https://codecov.io/github/m1sk9/LunaticChat/graph/badge.svg?token=M3CJYTADYD)](https://codecov.io/github/m1sk9/LunaticChat)
A next-generation chat plugin for Paper, Folia and Velocity.
diff --git a/build.gradle.kts b/build.gradle.kts
index 36906c9..d2bd265 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,3 +1,4 @@
+import org.gradle.testing.jacoco.tasks.JacocoReport
import org.jetbrains.dokka.gradle.DokkaExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -27,6 +28,7 @@ subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jlleitschuh.gradle.ktlint")
apply(plugin = "org.jetbrains.dokka")
+ apply(plugin = "jacoco")
tasks.withType<KotlinCompile> {
compilerOptions {
@@ -36,6 +38,15 @@ subprojects {
tasks.withType<Test> {
useJUnitPlatform()
+ finalizedBy(tasks.named("jacocoTestReport"))
+ }
+
+ tasks.withType<JacocoReport> {
+ dependsOn(tasks.withType<Test>())
+ reports {
+ xml.required.set(true)
+ html.required.set(false)
+ }
}
configure<DokkaExtension> {
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..dcf139a
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,15 @@
+coverage:
+ status:
+ project:
+ default:
+ target: auto
+ threshold: 1%
+ patch:
+ default:
+ target: 80%
+comment:
+ layout: "condensed_header, condensed_files, condensed_footer"
+ behavior: default
+ require_changes: false
+ignore:
+ - "dokka/**"
diff --git a/engine/build.gradle.kts b/engine/build.gradle.kts
index c178870..0f4e419 100644
--- a/engine/build.gradle.kts
+++ b/engine/build.gradle.kts
@@ -12,4 +12,6 @@ dependencies {
// Adventure API (provided by platform implementations)
compileOnly("net.kyori:adventure-api:4.26.1")
+
+ testImplementation("net.kyori:adventure-api:4.26.1")
}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeTest.kt
new file mode 100644
index 0000000..5656a14
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeTest.kt
@@ -0,0 +1,33 @@
+package dev.m1sk9.lunaticChat.engine.chat
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ChatModeTest {
+ @Test
+ fun `toggle should switch GLOBAL to CHANNEL`() {
+ assertEquals(ChatMode.CHANNEL, ChatMode.GLOBAL.toggle())
+ }
+
+ @Test
+ fun `toggle should switch CHANNEL to GLOBAL`() {
+ assertEquals(ChatMode.GLOBAL, ChatMode.CHANNEL.toggle())
+ }
+
+ @Test
+ fun `DEFAULT should be GLOBAL`() {
+ assertEquals(ChatMode.GLOBAL, ChatMode.DEFAULT)
+ }
+
+ @Test
+ fun `double toggle should return original mode`() {
+ assertEquals(ChatMode.GLOBAL, ChatMode.GLOBAL.toggle().toggle())
+ assertEquals(ChatMode.CHANNEL, ChatMode.CHANNEL.toggle().toggle())
+ }
+
+ @Test
+ fun `enum should have exactly two values`() {
+ assertEquals(2, ChatMode.entries.size)
+ assertEquals(setOf(ChatMode.GLOBAL, ChatMode.CHANNEL), ChatMode.entries.toSet())
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt
new file mode 100644
index 0000000..4684432
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt
@@ -0,0 +1,200 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import kotlinx.serialization.json.Json
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+class ChannelDataClassesTest {
+ private val json = Json { ignoreUnknownKeys = true }
+ private val testOwnerId = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val testPlayerId = UUID.fromString("00000002-0000-0000-0000-000000000000")
+
+ // --- ChannelRole ---
+
+ @Test
+ fun `ChannelRole should have 3 entries`() {
+ assertEquals(3, ChannelRole.entries.size)
+ }
+
+ @Test
+ fun `ChannelRole entries should be OWNER, MODERATOR, MEMBER`() {
+ val expected = setOf("OWNER", "MODERATOR", "MEMBER")
+ val actual = ChannelRole.entries.map { it.name }.toSet()
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `ChannelRole serialization round-trip should preserve value`() {
+ ChannelRole.entries.forEach { role ->
+ val serialized = json.encodeToString(ChannelRole.serializer(), role)
+ val deserialized = json.decodeFromString(ChannelRole.serializer(), serialized)
+ assertEquals(role, deserialized)
+ }
+ }
+
+ // --- ChannelMember ---
+
+ @Test
+ fun `ChannelMember should have default joinedAt`() {
+ val before = System.currentTimeMillis()
+ val member = ChannelMember(channelId = "ch-1", playerId = testPlayerId, role = ChannelRole.MEMBER)
+ val after = System.currentTimeMillis()
+
+ assertTrue(member.joinedAt in before..after)
+ }
+
+ @Test
+ fun `ChannelMember serialization round-trip should preserve all fields`() {
+ val original =
+ ChannelMember(
+ channelId = "ch-1",
+ playerId = testPlayerId,
+ role = ChannelRole.MODERATOR,
+ joinedAt = 12345L,
+ )
+
+ val serialized = json.encodeToString(ChannelMember.serializer(), original)
+ val deserialized = json.decodeFromString(ChannelMember.serializer(), serialized)
+
+ assertEquals(original.channelId, deserialized.channelId)
+ assertEquals(original.playerId, deserialized.playerId)
+ assertEquals(original.role, deserialized.role)
+ assertEquals(original.joinedAt, deserialized.joinedAt)
+ }
+
+ @Test
+ fun `ChannelMember copy should allow role change`() {
+ val original = ChannelMember(channelId = "ch-1", playerId = testPlayerId, role = ChannelRole.MEMBER)
+ val promoted = original.copy(role = ChannelRole.MODERATOR)
+
+ assertEquals(ChannelRole.MODERATOR, promoted.role)
+ assertEquals(original.channelId, promoted.channelId)
+ assertEquals(original.playerId, promoted.playerId)
+ assertEquals(original.joinedAt, promoted.joinedAt)
+ }
+
+ // --- ChannelData ---
+
+ @Test
+ fun `ChannelData default values should be correct`() {
+ val data = ChannelData()
+ assertEquals(1, data.version)
+ assertTrue(data.channels.isEmpty())
+ assertTrue(data.members.isEmpty())
+ assertTrue(data.activeChannels.isEmpty())
+ }
+
+ @Test
+ fun `ChannelData serialization round-trip should preserve all fields`() {
+ val channel = Channel(id = "ch-1", name = "Test Channel", ownerId = testOwnerId, createdAt = 1000L)
+ val member = ChannelMember(channelId = "ch-1", playerId = testPlayerId, role = ChannelRole.MEMBER, joinedAt = 2000L)
+ val original =
+ ChannelData(
+ version = 1,
+ channels = mapOf("ch-1" to channel),
+ members = mapOf("ch-1" to listOf(member)),
+ activeChannels = mapOf("player1" to "ch-1"),
+ )
+
+ val serialized = json.encodeToString(ChannelData.serializer(), original)
+ val deserialized = json.decodeFromString(ChannelData.serializer(), serialized)
+
+ assertEquals(original.version, deserialized.version)
+ assertEquals(original.channels.size, deserialized.channels.size)
+ assertEquals(original.members.size, deserialized.members.size)
+ assertEquals(original.activeChannels, deserialized.activeChannels)
+ }
+
+ // --- ChannelContext ---
+
+ @Test
+ fun `ChannelContext should store all fields`() {
+ val channel = Channel(id = "ch-1", name = "Test", ownerId = testOwnerId)
+ val member = ChannelMember(channelId = "ch-1", playerId = testPlayerId, role = ChannelRole.MEMBER)
+ val context = ChannelContext(channelId = "ch-1", channel = channel, members = listOf(member))
+
+ assertEquals("ch-1", context.channelId)
+ assertEquals(channel, context.channel)
+ assertEquals(1, context.members.size)
+ assertEquals(testPlayerId, context.members[0].playerId)
+ }
+
+ @Test
+ fun `ChannelContext copy should create independent instance`() {
+ val channel = Channel(id = "ch-1", name = "Test", ownerId = testOwnerId)
+ val original = ChannelContext(channelId = "ch-1", channel = channel, members = emptyList())
+ val copied = original.copy(channelId = "ch-2")
+
+ assertEquals("ch-2", copied.channelId)
+ assertNotEquals(original.channelId, copied.channelId)
+ }
+
+ // --- ChannelMessageLogEntry ---
+
+ @Test
+ fun `ChannelMessageLogEntry create should convert UUID to string`() {
+ val entry =
+ ChannelMessageLogEntry.create(
+ playerId = testPlayerId,
+ playerName = "TestPlayer",
+ channelId = "ch-1",
+ message = "Hello world",
+ )
+
+ assertEquals(testPlayerId.toString(), entry.playerId)
+ assertEquals("TestPlayer", entry.playerName)
+ assertEquals("ch-1", entry.channelId)
+ assertEquals("Hello world", entry.message)
+ }
+
+ @Test
+ fun `ChannelMessageLogEntry create should set non-empty timestamp`() {
+ val entry =
+ ChannelMessageLogEntry.create(
+ playerId = testPlayerId,
+ playerName = "TestPlayer",
+ channelId = "ch-1",
+ message = "Hello",
+ )
+
+ assertTrue(entry.timestamp.isNotEmpty())
+ }
+
+ @Test
+ fun `ChannelMessageLogEntry serialization round-trip should preserve all fields`() {
+ val original =
+ ChannelMessageLogEntry(
+ timestamp = "2024-01-01T00:00:00Z",
+ playerId = testPlayerId.toString(),
+ playerName = "TestPlayer",
+ channelId = "ch-1",
+ message = "Test message",
+ )
+
+ val serialized = json.encodeToString(ChannelMessageLogEntry.serializer(), original)
+ val deserialized = json.decodeFromString(ChannelMessageLogEntry.serializer(), serialized)
+
+ assertEquals(original.timestamp, deserialized.timestamp)
+ assertEquals(original.playerId, deserialized.playerId)
+ assertEquals(original.playerName, deserialized.playerName)
+ assertEquals(original.channelId, deserialized.channelId)
+ assertEquals(original.message, deserialized.message)
+ }
+
+ @Test
+ fun `ChannelMessageLogEntry create timestamp should be ISO-8601 format`() {
+ val entry =
+ ChannelMessageLogEntry.create(
+ playerId = testPlayerId,
+ playerName = "TestPlayer",
+ channelId = "ch-1",
+ message = "Hello",
+ )
+
+ // ISO-8601 timestamps contain 'T' separator
+ assertTrue(entry.timestamp.contains("T"), "Timestamp should be ISO-8601 format: ${entry.timestamp}")
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelTest.kt
new file mode 100644
index 0000000..b24e169
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelTest.kt
@@ -0,0 +1,139 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import kotlinx.serialization.json.Json
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ChannelTest {
+ private val testOwnerId = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val json = Json { ignoreUnknownKeys = true }
+
+ @Test
+ fun `valid channel ID should be accepted`() {
+ val channel = Channel(id = "test-channel", name = "Test", ownerId = testOwnerId)
+ assertEquals("test-channel", channel.id)
+ }
+
+ @Test
+ fun `channel ID with underscores and hyphens should be accepted`() {
+ val channel = Channel(id = "my_test-channel", name = "Test", ownerId = testOwnerId)
+ assertEquals("my_test-channel", channel.id)
+ }
+
+ @Test
+ fun `channel ID with exactly 3 characters should be accepted`() {
+ val channel = Channel(id = "abc", name = "Test", ownerId = testOwnerId)
+ assertEquals("abc", channel.id)
+ }
+
+ @Test
+ fun `channel ID with exactly 30 characters should be accepted`() {
+ val id = "a".repeat(30)
+ val channel = Channel(id = id, name = "Test", ownerId = testOwnerId)
+ assertEquals(id, channel.id)
+ }
+
+ @Test
+ fun `channel ID with less than 3 characters should be rejected`() {
+ assertFailsWith<IllegalArgumentException> {
+ Channel(id = "ab", name = "Test", ownerId = testOwnerId)
+ }
+ }
+
+ @Test
+ fun `channel ID with more than 30 characters should be rejected`() {
+ assertFailsWith<IllegalArgumentException> {
+ Channel(id = "a".repeat(31), name = "Test", ownerId = testOwnerId)
+ }
+ }
+
+ @Test
+ fun `channel ID with spaces should be rejected`() {
+ assertFailsWith<IllegalArgumentException> {
+ Channel(id = "test channel", name = "Test", ownerId = testOwnerId)
+ }
+ }
+
+ @Test
+ fun `channel ID with special characters should be rejected`() {
+ assertFailsWith<IllegalArgumentException> {
+ Channel(id = "test@channel", name = "Test", ownerId = testOwnerId)
+ }
+ }
+
+ @Test
+ fun `empty channel ID should be rejected`() {
+ assertFailsWith<IllegalArgumentException> {
+ Channel(id = "", name = "Test", ownerId = testOwnerId)
+ }
+ }
+
+ @Test
+ fun `blank channel name should be rejected`() {
+ assertFailsWith<IllegalArgumentException> {
+ Channel(id = "valid-id", name = " ", ownerId = testOwnerId)
+ }
+ }
+
+ @Test
+ fun `empty channel name should be rejected`() {
+ assertFailsWith<IllegalArgumentException> {
+ Channel(id = "valid-id", name = "", ownerId = testOwnerId)
+ }
+ }
+
+ @Test
+ fun `default values should be correct`() {
+ val channel = Channel(id = "test-ch", name = "Test Channel", ownerId = testOwnerId)
+ assertNull(channel.description)
+ assertFalse(channel.isPrivate)
+ assertTrue(channel.bannedPlayers.isEmpty())
+ }
+
+ @Test
+ fun `serialization round-trip should preserve all fields`() {
+ val bannedPlayer = UUID.fromString("00000002-0000-0000-0000-000000000000")
+ val original =
+ Channel(
+ id = "test-channel",
+ name = "Test Channel",
+ description = "A test channel",
+ isPrivate = true,
+ ownerId = testOwnerId,
+ createdAt = 1000L,
+ bannedPlayers = setOf(bannedPlayer),
+ )
+
+ val jsonString = json.encodeToString(Channel.serializer(), original)
+ val decoded = json.decodeFromString(Channel.serializer(), jsonString)
+
+ assertEquals(original.id, decoded.id)
+ assertEquals(original.name, decoded.name)
+ assertEquals(original.description, decoded.description)
+ assertEquals(original.isPrivate, decoded.isPrivate)
+ assertEquals(original.ownerId, decoded.ownerId)
+ assertEquals(original.createdAt, decoded.createdAt)
+ assertEquals(original.bannedPlayers, decoded.bannedPlayers)
+ }
+
+ @Test
+ fun `CHANNEL_ID_PATTERN should match valid patterns`() {
+ assertTrue("abc".matches(Channel.CHANNEL_ID_PATTERN))
+ assertTrue("test-123".matches(Channel.CHANNEL_ID_PATTERN))
+ assertTrue("my_channel".matches(Channel.CHANNEL_ID_PATTERN))
+ assertTrue("ABC123".matches(Channel.CHANNEL_ID_PATTERN))
+ }
+
+ @Test
+ fun `CHANNEL_ID_PATTERN should reject invalid patterns`() {
+ assertFalse("ab".matches(Channel.CHANNEL_ID_PATTERN))
+ assertFalse("".matches(Channel.CHANNEL_ID_PATTERN))
+ assertFalse("test channel".matches(Channel.CHANNEL_ID_PATTERN))
+ assertFalse("test@ch".matches(Channel.CHANNEL_ID_PATTERN))
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResultTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResultTest.kt
new file mode 100644
index 0000000..b80f116
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResultTest.kt
@@ -0,0 +1,54 @@
+package dev.m1sk9.lunaticChat.engine.command
+
+import net.kyori.adventure.text.Component
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+
+class CommandResultTest {
+ @Test
+ fun `Success toBrigadierResult should return 1`() {
+ assertEquals(1, CommandResult.Success.toBrigadierResult())
+ }
+
+ @Test
+ fun `SuccessWithMessage toBrigadierResult should return 1`() {
+ val result = CommandResult.SuccessWithMessage(Component.text("ok"))
+ assertEquals(1, result.toBrigadierResult())
+ }
+
+ @Test
+ fun `Failure toBrigadierResult should return 0`() {
+ val result = CommandResult.Failure(Component.text("error"))
+ assertEquals(0, result.toBrigadierResult())
+ }
+
+ @Test
+ fun `InvalidUsage toBrigadierResult should return 0`() {
+ val result = CommandResult.InvalidUsage("/cmd <arg>")
+ assertEquals(0, result.toBrigadierResult())
+ }
+
+ @Test
+ fun `SuccessWithMessage should preserve message`() {
+ val message = Component.text("Test message")
+ val result = CommandResult.SuccessWithMessage(message)
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ assertEquals(message, result.message)
+ }
+
+ @Test
+ fun `Failure should preserve message`() {
+ val message = Component.text("Error message")
+ val result = CommandResult.Failure(message)
+ assertIs<CommandResult.Failure>(result)
+ assertEquals(message, result.message)
+ }
+
+ @Test
+ fun `InvalidUsage should preserve usage hint`() {
+ val result = CommandResult.InvalidUsage("/lc setting <key> <on|off>")
+ assertIs<CommandResult.InvalidUsage>(result)
+ assertEquals("/lc setting <key> <on|off>", result.usageHint)
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelExceptionTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelExceptionTest.kt
new file mode 100644
index 0000000..955c96b
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelExceptionTest.kt
@@ -0,0 +1,279 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ChannelExceptionTest {
+ private val testPlayerId = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val testChannelId = "test-channel"
+
+ // --- ChannelAlreadyActiveException ---
+
+ @Test
+ fun `ChannelAlreadyActiveException should contain playerId and channelId in message`() {
+ val ex = ChannelAlreadyActiveException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains(testChannelId))
+ assertEquals(testPlayerId, ex.playerId)
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelCannotInviteSelfException ---
+
+ @Test
+ fun `ChannelCannotInviteSelfException should contain playerId in message`() {
+ val ex = ChannelCannotInviteSelfException(testPlayerId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ }
+
+ // --- ChannelLimitExceededException ---
+
+ @Test
+ fun `ChannelLimitExceededException should contain limit in message`() {
+ val ex = ChannelLimitExceededException(10)
+ assertTrue(ex.message!!.contains("10"))
+ assertEquals(10, ex.limit)
+ }
+
+ // --- ChannelMemberAlreadyException ---
+
+ @Test
+ fun `ChannelMemberAlreadyException should contain playerId and channelId in message`() {
+ val ex = ChannelMemberAlreadyException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains(testChannelId))
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelMemberLimitExceededException ---
+
+ @Test
+ fun `ChannelMemberLimitExceededException should contain channelId and limit in message`() {
+ val ex = ChannelMemberLimitExceededException(testChannelId, 50)
+ assertTrue(ex.message!!.contains(testChannelId))
+ assertTrue(ex.message!!.contains("50"))
+ assertEquals(testChannelId, ex.channelId)
+ assertEquals(50, ex.limit)
+ }
+
+ // --- ChannelMemberNotFoundException ---
+
+ @Test
+ fun `ChannelMemberNotFoundException should contain channelId in message`() {
+ val ex = ChannelMemberNotFoundException(testChannelId)
+ assertTrue(ex.message!!.contains(testChannelId))
+ }
+
+ // --- ChannelNoOwnerPermissionException ---
+
+ @Test
+ fun `ChannelNoOwnerPermissionException should contain ownerId in message`() {
+ val ex = ChannelNoOwnerPermissionException(testPlayerId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ }
+
+ // --- ChannelNotFoundException ---
+
+ @Test
+ fun `ChannelNotFoundException should contain channelId in message`() {
+ val ex = ChannelNotFoundException(testChannelId)
+ assertTrue(ex.message!!.contains(testChannelId))
+ }
+
+ // --- ChannelNotMemberException ---
+
+ @Test
+ fun `ChannelNotMemberException should contain playerId and channelId in message`() {
+ val ex = ChannelNotMemberException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains(testChannelId))
+ }
+
+ // --- ChannelPlayerAlreadyBannedException ---
+
+ @Test
+ fun `ChannelPlayerAlreadyBannedException should contain playerId and channelId in message`() {
+ val ex = ChannelPlayerAlreadyBannedException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains(testChannelId))
+ assertEquals(testPlayerId, ex.playerId)
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelPlayerBannedException ---
+
+ @Test
+ fun `ChannelPlayerBannedException should contain playerId and channelId in message`() {
+ val ex = ChannelPlayerBannedException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains(testChannelId))
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelPlayerBypassBanException ---
+
+ @Test
+ fun `ChannelPlayerBypassBanException should contain playerId in message and store channelId`() {
+ val ex = ChannelPlayerBypassBanException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelPlayerBypassKickException ---
+
+ @Test
+ fun `ChannelPlayerBypassKickException should contain playerId in message and store channelId`() {
+ val ex = ChannelPlayerBypassKickException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelPlayerMembershipLimitExceededException ---
+
+ @Test
+ fun `ChannelPlayerMembershipLimitExceededException should contain playerId and limit in message`() {
+ val ex = ChannelPlayerMembershipLimitExceededException(testPlayerId, 5)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains("5"))
+ assertEquals(testPlayerId, ex.playerId)
+ assertEquals(5, ex.limit)
+ }
+
+ // --- ChannelPlayerNotBannedException ---
+
+ @Test
+ fun `ChannelPlayerNotBannedException should contain playerId and channelId in message`() {
+ val ex = ChannelPlayerNotBannedException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains(testChannelId))
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelPrivateRequiresInvitationException ---
+
+ @Test
+ fun `ChannelPrivateRequiresInvitationException should contain playerId and channelId in message`() {
+ val ex = ChannelPrivateRequiresInvitationException(testPlayerId, testChannelId)
+ assertTrue(ex.message!!.contains(testPlayerId.toString()))
+ assertTrue(ex.message!!.contains(testChannelId))
+ assertEquals(testPlayerId, ex.playerId)
+ assertEquals(testChannelId, ex.channelId)
+ }
+
+ // --- ChannelRuntimeException ---
+
+ @Test
+ fun `ChannelRuntimeException should preserve message`() {
+ val ex = ChannelRuntimeException("runtime error")
+ assertEquals("runtime error", ex.message)
+ assertNull(ex.cause)
+ }
+
+ @Test
+ fun `ChannelRuntimeException should preserve cause`() {
+ val cause = RuntimeException("root cause")
+ val ex = ChannelRuntimeException("runtime error", cause)
+ assertEquals("runtime error", ex.message)
+ assertEquals(cause, ex.cause)
+ }
+
+ // --- ChannelStorageLoadException ---
+
+ @Test
+ fun `ChannelStorageLoadException should preserve message`() {
+ val ex = ChannelStorageLoadException("load error")
+ assertEquals("load error", ex.message)
+ assertNull(ex.cause)
+ }
+
+ @Test
+ fun `ChannelStorageLoadException should preserve cause`() {
+ val cause = RuntimeException("io error")
+ val ex = ChannelStorageLoadException("load error", cause)
+ assertEquals("load error", ex.message)
+ assertEquals(cause, ex.cause)
+ }
+
+ // --- ChannelStorageSaveException ---
+
+ @Test
+ fun `ChannelStorageSaveException should preserve message`() {
+ val ex = ChannelStorageSaveException("save error")
+ assertEquals("save error", ex.message)
+ assertNull(ex.cause)
+ }
+
+ @Test
+ fun `ChannelStorageSaveException should preserve cause`() {
+ val cause = RuntimeException("io error")
+ val ex = ChannelStorageSaveException("save error", cause)
+ assertEquals("save error", ex.message)
+ assertEquals(cause, ex.cause)
+ }
+
+ // --- ChatModeStorageException ---
+
+ @Test
+ fun `ChatModeStorageException should preserve message`() {
+ val ex = ChatModeStorageException("chatmode error")
+ assertEquals("chatmode error", ex.message)
+ assertNull(ex.cause)
+ }
+
+ @Test
+ fun `ChatModeStorageException should preserve cause`() {
+ val cause = RuntimeException("io error")
+ val ex = ChatModeStorageException("chatmode error", cause)
+ assertEquals("chatmode error", ex.message)
+ assertEquals(cause, ex.cause)
+ }
+
+ // --- RequirePermissionException ---
+
+ @Test
+ fun `RequirePermissionException should join permission nodes in message`() {
+ val permissions = listOf(LunaticChatPermissionNode.Lc, LunaticChatPermissionNode.Tell)
+ val ex = RequirePermissionException(permissions)
+ assertTrue(ex.message!!.contains("lunaticchat.command.lc"))
+ assertTrue(ex.message!!.contains("lunaticchat.command.tell"))
+ }
+
+ @Test
+ fun `RequirePermissionException with single permission should contain that node`() {
+ val permissions = listOf(LunaticChatPermissionNode.Spy)
+ val ex = RequirePermissionException(permissions)
+ assertTrue(ex.message!!.contains("lunaticchat.spy"))
+ }
+
+ // --- All exceptions are instances of Exception ---
+
+ @Test
+ fun `all exceptions should be instances of Exception`() {
+ assertIs<Exception>(ChannelAlreadyActiveException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelCannotInviteSelfException(testPlayerId))
+ assertIs<Exception>(ChannelLimitExceededException(10))
+ assertIs<Exception>(ChannelMemberAlreadyException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelMemberLimitExceededException(testChannelId, 50))
+ assertIs<Exception>(ChannelMemberNotFoundException(testChannelId))
+ assertIs<Exception>(ChannelNoOwnerPermissionException(testPlayerId))
+ assertIs<Exception>(ChannelNotFoundException(testChannelId))
+ assertIs<Exception>(ChannelNotMemberException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelPlayerAlreadyBannedException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelPlayerBannedException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelPlayerBypassBanException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelPlayerBypassKickException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelPlayerMembershipLimitExceededException(testPlayerId, 5))
+ assertIs<Exception>(ChannelPlayerNotBannedException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelPrivateRequiresInvitationException(testPlayerId, testChannelId))
+ assertIs<Exception>(ChannelRuntimeException("test"))
+ assertIs<Exception>(ChannelStorageLoadException("test"))
+ assertIs<Exception>(ChannelStorageSaveException("test"))
+ assertIs<Exception>(ChatModeStorageException("test"))
+ assertIs<Exception>(RequirePermissionException(listOf(LunaticChatPermissionNode.Lc)))
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt
new file mode 100644
index 0000000..c9838f2
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt
@@ -0,0 +1,82 @@
+package dev.m1sk9.lunaticChat.engine.permission
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class LunaticChatPermissionNodeTest {
+ private val allNodes =
+ listOf(
+ LunaticChatPermissionNode.Lc,
+ LunaticChatPermissionNode.Tell,
+ LunaticChatPermissionNode.Reply,
+ LunaticChatPermissionNode.Settings,
+ LunaticChatPermissionNode.Status,
+ LunaticChatPermissionNode.Channel,
+ LunaticChatPermissionNode.ChannelCreate,
+ LunaticChatPermissionNode.ChannelList,
+ LunaticChatPermissionNode.ChannelJoin,
+ LunaticChatPermissionNode.ChannelLeave,
+ LunaticChatPermissionNode.ChannelSwitch,
+ LunaticChatPermissionNode.ChannelStatus,
+ LunaticChatPermissionNode.ChannelInfo,
+ LunaticChatPermissionNode.ChannelDelete,
+ LunaticChatPermissionNode.ChannelInvite,
+ LunaticChatPermissionNode.ChannelKick,
+ LunaticChatPermissionNode.ChannelBan,
+ LunaticChatPermissionNode.ChannelUnban,
+ LunaticChatPermissionNode.ChannelMod,
+ LunaticChatPermissionNode.ChannelOwnership,
+ LunaticChatPermissionNode.ChatMode,
+ LunaticChatPermissionNode.ChatModeToggle,
+ LunaticChatPermissionNode.VelocityStatus,
+ LunaticChatPermissionNode.Spy,
+ LunaticChatPermissionNode.NoticeUpdate,
+ LunaticChatPermissionNode.ChannelBypass,
+ )
+
+ @Test
+ fun `all permission nodes should have lunaticchat prefix`() {
+ allNodes.forEach { node ->
+ assertTrue(
+ node.permissionNode.startsWith("lunaticchat."),
+ "Expected '${node.permissionNode}' to start with 'lunaticchat.'",
+ )
+ }
+ }
+
+ @Test
+ fun `should have 26 permission nodes`() {
+ assertEquals(26, allNodes.size)
+ }
+
+ @Test
+ fun `spot check specific permission node values`() {
+ assertEquals("lunaticchat.command.lc", LunaticChatPermissionNode.Lc.permissionNode)
+ assertEquals("lunaticchat.command.tell", LunaticChatPermissionNode.Tell.permissionNode)
+ assertEquals("lunaticchat.command.reply", LunaticChatPermissionNode.Reply.permissionNode)
+ assertEquals("lunaticchat.command.lc.settings", LunaticChatPermissionNode.Settings.permissionNode)
+ assertEquals("lunaticchat.command.lc.channel.create", LunaticChatPermissionNode.ChannelCreate.permissionNode)
+ assertEquals("lunaticchat.command.lc.chatmode.toggle", LunaticChatPermissionNode.ChatModeToggle.permissionNode)
+ assertEquals("lunaticchat.command.lcv.status", LunaticChatPermissionNode.VelocityStatus.permissionNode)
+ assertEquals("lunaticchat.spy", LunaticChatPermissionNode.Spy.permissionNode)
+ assertEquals("lunaticchat.noticeupdate", LunaticChatPermissionNode.NoticeUpdate.permissionNode)
+ assertEquals("lunaticchat.channelbypass", LunaticChatPermissionNode.ChannelBypass.permissionNode)
+ }
+
+ @Test
+ fun `all permission node strings should be non-empty`() {
+ allNodes.forEach { node ->
+ assertTrue(
+ node.permissionNode.isNotEmpty(),
+ "Permission node should not be empty",
+ )
+ }
+ }
+
+ @Test
+ fun `all permission node strings should be unique`() {
+ val nodeStrings = allNodes.map { it.permissionNode }
+ assertEquals(nodeStrings.size, nodeStrings.toSet().size, "All permission nodes should be unique")
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt
new file mode 100644
index 0000000..c792aad
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt
@@ -0,0 +1,233 @@
+package dev.m1sk9.lunaticChat.engine.protocol
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertIs
+
+class PluginMessageCodecTest {
+ @Test
+ fun `encode and decode Handshake round-trip`() {
+ val original =
+ PluginMessage.Handshake(
+ pluginVersion = "0.10.0",
+ protocolMajor = 1,
+ protocolMinor = 0,
+ protocolPatch = 0,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.Handshake>(decoded)
+ assertEquals(original.pluginVersion, decoded.pluginVersion)
+ assertEquals(original.protocolMajor, decoded.protocolMajor)
+ assertEquals(original.protocolMinor, decoded.protocolMinor)
+ assertEquals(original.protocolPatch, decoded.protocolPatch)
+ }
+
+ @Test
+ fun `encode and decode HandshakeResponse compatible round-trip`() {
+ val original =
+ PluginMessage.HandshakeResponse(
+ compatible = true,
+ velocityVersion = "0.10.0",
+ error = null,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.HandshakeResponse>(decoded)
+ assertEquals(original.compatible, decoded.compatible)
+ assertEquals(original.velocityVersion, decoded.velocityVersion)
+ assertEquals(original.error, decoded.error)
+ }
+
+ @Test
+ fun `encode and decode HandshakeResponse incompatible round-trip`() {
+ val original =
+ PluginMessage.HandshakeResponse(
+ compatible = false,
+ velocityVersion = "0.10.0",
+ error = "Version mismatch",
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.HandshakeResponse>(decoded)
+ assertEquals(false, decoded.compatible)
+ assertEquals("Version mismatch", decoded.error)
+ }
+
+ @Test
+ fun `encode and decode StatusRequest round-trip`() {
+ val original = PluginMessage.StatusRequest
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.StatusRequest>(decoded)
+ }
+
+ @Test
+ fun `encode and decode StatusResponse round-trip`() {
+ val original =
+ PluginMessage.StatusResponse(
+ velocityVersion = "0.10.0",
+ protocolVersion = "1.0.0",
+ online = true,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.StatusResponse>(decoded)
+ assertEquals(original.velocityVersion, decoded.velocityVersion)
+ assertEquals(original.protocolVersion, decoded.protocolVersion)
+ assertEquals(original.online, decoded.online)
+ }
+
+ @Test
+ fun `encode and decode GlobalChatMessage round-trip`() {
+ val original =
+ PluginMessage.GlobalChatMessage(
+ messageId = "test-id-123",
+ serverName = "lobby",
+ playerId = "00000001-0000-0000-0000-000000000000",
+ playerName = "TestPlayer",
+ message = "Hello, world!",
+ timestamp = 1000L,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.GlobalChatMessage>(decoded)
+ assertEquals(original.messageId, decoded.messageId)
+ assertEquals(original.serverName, decoded.serverName)
+ assertEquals(original.playerId, decoded.playerId)
+ assertEquals(original.playerName, decoded.playerName)
+ assertEquals(original.message, decoded.message)
+ assertEquals(original.timestamp, decoded.timestamp)
+ }
+
+ @Test
+ fun `decode should throw on unknown sub-channel`() {
+ val out = java.io.ByteArrayOutputStream()
+ val dataOut = java.io.DataOutputStream(out)
+ dataOut.writeUTF("unknown_channel")
+ dataOut.writeUTF("{}")
+
+ assertFailsWith<IllegalArgumentException> {
+ PluginMessageCodec.decode(out.toByteArray())
+ }
+ }
+
+ @Test
+ fun `decode should throw on empty data`() {
+ assertFailsWith<Exception> {
+ PluginMessageCodec.decode(byteArrayOf())
+ }
+ }
+
+ @Test
+ fun `encode and decode GlobalChatMessage with special characters`() {
+ val original =
+ PluginMessage.GlobalChatMessage(
+ messageId = "msg-special",
+ serverName = "survival",
+ playerId = "00000002-0000-0000-0000-000000000000",
+ playerName = "Player_With-Dash",
+ message = "Hello! こんにちは 🎉 \"quotes\" & <tags>",
+ timestamp = 2000L,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.GlobalChatMessage>(decoded)
+ assertEquals(original.message, decoded.message)
+ }
+
+ @Test
+ fun `SubChannel constants should have correct values`() {
+ assertEquals("handshake", PluginMessageCodec.SubChannel.HANDSHAKE)
+ assertEquals("handshake_response", PluginMessageCodec.SubChannel.HANDSHAKE_RESPONSE)
+ assertEquals("status_request", PluginMessageCodec.SubChannel.STATUS_REQUEST)
+ assertEquals("status_response", PluginMessageCodec.SubChannel.STATUS_RESPONSE)
+ assertEquals("global_chat", PluginMessageCodec.SubChannel.GLOBAL_CHAT)
+ }
+
+ @Test
+ fun `encode should produce non-empty byte array for all message types`() {
+ val messages =
+ listOf(
+ PluginMessage.Handshake("1.0.0", 1, 0, 0),
+ PluginMessage.HandshakeResponse(true, "1.0.0"),
+ PluginMessage.StatusRequest,
+ PluginMessage.StatusResponse("1.0.0", "1.0.0", true),
+ PluginMessage.GlobalChatMessage("id", "srv", "pid", "name", "msg", 0L),
+ )
+
+ messages.forEach { message ->
+ val encoded = PluginMessageCodec.encode(message)
+ assert(encoded.isNotEmpty()) { "Encoded ${message::class.simpleName} should not be empty" }
+ }
+ }
+
+ @Test
+ fun `StatusResponse with online false round-trip`() {
+ val original =
+ PluginMessage.StatusResponse(
+ velocityVersion = "0.10.0",
+ protocolVersion = "1.0.0",
+ online = false,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.StatusResponse>(decoded)
+ assertEquals(false, decoded.online)
+ }
+
+ @Test
+ fun `Handshake with various protocol versions round-trip`() {
+ val original =
+ PluginMessage.Handshake(
+ pluginVersion = "2.5.3",
+ protocolMajor = 99,
+ protocolMinor = 42,
+ protocolPatch = 7,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.Handshake>(decoded)
+ assertEquals(99, decoded.protocolMajor)
+ assertEquals(42, decoded.protocolMinor)
+ assertEquals(7, decoded.protocolPatch)
+ }
+
+ @Test
+ fun `GlobalChatMessage with empty message round-trip`() {
+ val original =
+ PluginMessage.GlobalChatMessage(
+ messageId = "msg-empty",
+ serverName = "lobby",
+ playerId = "00000003-0000-0000-0000-000000000000",
+ playerName = "Player",
+ message = "",
+ timestamp = 3000L,
+ )
+
+ val encoded = PluginMessageCodec.encode(original)
+ val decoded = PluginMessageCodec.decode(encoded)
+
+ assertIs<PluginMessage.GlobalChatMessage>(decoded)
+ assertEquals("", decoded.message)
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt
new file mode 100644
index 0000000..7f05039
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt
@@ -0,0 +1,57 @@
+package dev.m1sk9.lunaticChat.engine.protocol
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class ProtocolVersionTest {
+ @Test
+ fun `version string should match MAJOR MINOR PATCH format`() {
+ assertEquals("${ProtocolVersion.MAJOR}.${ProtocolVersion.MINOR}.${ProtocolVersion.PATCH}", ProtocolVersion.version)
+ }
+
+ @Test
+ fun `isCompatible with matching major and minor should return true`() {
+ assertTrue(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR))
+ }
+
+ @Test
+ fun `isCompatible with different major should return false`() {
+ assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR + 1, ProtocolVersion.MINOR))
+ }
+
+ @Test
+ fun `isCompatible with different minor should return false`() {
+ assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR + 1))
+ }
+
+ @Test
+ fun `isCompatible string with matching version should return true`() {
+ assertTrue(ProtocolVersion.isCompatible("${ProtocolVersion.MAJOR}.${ProtocolVersion.MINOR}.0"))
+ }
+
+ @Test
+ fun `isCompatible string with different patch should return true`() {
+ assertTrue(ProtocolVersion.isCompatible("${ProtocolVersion.MAJOR}.${ProtocolVersion.MINOR}.99"))
+ }
+
+ @Test
+ fun `isCompatible string with different major should return false`() {
+ assertFalse(ProtocolVersion.isCompatible("${ProtocolVersion.MAJOR + 1}.${ProtocolVersion.MINOR}.0"))
+ }
+
+ @Test
+ fun `isCompatible string with malformed version should return false`() {
+ assertFalse(ProtocolVersion.isCompatible("invalid"))
+ assertFalse(ProtocolVersion.isCompatible("1.0"))
+ assertFalse(ProtocolVersion.isCompatible(""))
+ assertFalse(ProtocolVersion.isCompatible("a.b.c"))
+ assertFalse(ProtocolVersion.isCompatible("1.0.0.0"))
+ }
+
+ @Test
+ fun `isCompatible string with two parts should return false`() {
+ assertFalse(ProtocolVersion.isCompatible("1.0"))
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/SettingsDataClassesTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/SettingsDataClassesTest.kt
new file mode 100644
index 0000000..8743af9
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/SettingsDataClassesTest.kt
@@ -0,0 +1,167 @@
+package dev.m1sk9.lunaticChat.engine.settings
+
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
+import dev.m1sk9.lunaticChat.engine.chat.ChatModeData
+import dev.m1sk9.lunaticChat.engine.converter.CacheData
+import kotlinx.serialization.json.Json
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class SettingsDataClassesTest {
+ private val json = Json { ignoreUnknownKeys = true }
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val testUUID2 = UUID.fromString("00000002-0000-0000-0000-000000000000")
+
+ // --- PlayerChatSettings ---
+
+ @Test
+ fun `PlayerChatSettings default values should all be true`() {
+ val settings = PlayerChatSettings(uuid = testUUID)
+ assertTrue(settings.japaneseConversionEnabled)
+ assertTrue(settings.directMessageNotificationEnabled)
+ assertTrue(settings.channelMessageNotificationEnabled)
+ }
+
+ @Test
+ fun `PlayerChatSettings serialization round-trip should preserve all fields`() {
+ val original =
+ PlayerChatSettings(
+ uuid = testUUID,
+ japaneseConversionEnabled = false,
+ directMessageNotificationEnabled = true,
+ channelMessageNotificationEnabled = false,
+ )
+
+ val serialized = json.encodeToString(PlayerChatSettings.serializer(), original)
+ val deserialized = json.decodeFromString(PlayerChatSettings.serializer(), serialized)
+
+ assertEquals(original.uuid, deserialized.uuid)
+ assertEquals(original.japaneseConversionEnabled, deserialized.japaneseConversionEnabled)
+ assertEquals(original.directMessageNotificationEnabled, deserialized.directMessageNotificationEnabled)
+ assertEquals(original.channelMessageNotificationEnabled, deserialized.channelMessageNotificationEnabled)
+ }
+
+ @Test
+ fun `PlayerChatSettings copy should allow flag toggle`() {
+ val original = PlayerChatSettings(uuid = testUUID)
+ val modified = original.copy(japaneseConversionEnabled = false)
+
+ assertFalse(modified.japaneseConversionEnabled)
+ assertTrue(modified.directMessageNotificationEnabled)
+ assertTrue(modified.channelMessageNotificationEnabled)
+ assertEquals(original.uuid, modified.uuid)
+ }
+
+ @Test
+ fun `PlayerChatSettings copy should allow toggling all flags`() {
+ val original = PlayerChatSettings(uuid = testUUID)
+ val modified =
+ original.copy(
+ japaneseConversionEnabled = false,
+ directMessageNotificationEnabled = false,
+ channelMessageNotificationEnabled = false,
+ )
+
+ assertFalse(modified.japaneseConversionEnabled)
+ assertFalse(modified.directMessageNotificationEnabled)
+ assertFalse(modified.channelMessageNotificationEnabled)
+ }
+
+ // --- PlayerSettingsData ---
+
+ @Test
+ fun `PlayerSettingsData default values should be correct`() {
+ val data = PlayerSettingsData()
+ assertEquals(1, data.version)
+ assertTrue(data.japaneseConversion.isEmpty())
+ assertTrue(data.directMessageNotification.isEmpty())
+ assertTrue(data.channelMessageNotification.isEmpty())
+ }
+
+ @Test
+ fun `PlayerSettingsData serialization round-trip should preserve UUID keys`() {
+ val original =
+ PlayerSettingsData(
+ version = 1,
+ japaneseConversion = mapOf(testUUID to true, testUUID2 to false),
+ directMessageNotification = mapOf(testUUID to false),
+ channelMessageNotification = mapOf(testUUID2 to true),
+ )
+
+ val serialized = json.encodeToString(PlayerSettingsData.serializer(), original)
+ val deserialized = json.decodeFromString(PlayerSettingsData.serializer(), serialized)
+
+ assertEquals(original.version, deserialized.version)
+ assertEquals(original.japaneseConversion, deserialized.japaneseConversion)
+ assertEquals(original.directMessageNotification, deserialized.directMessageNotification)
+ assertEquals(original.channelMessageNotification, deserialized.channelMessageNotification)
+ }
+
+ @Test
+ fun `PlayerSettingsData serialized UUID keys should be string format`() {
+ val data = PlayerSettingsData(japaneseConversion = mapOf(testUUID to true))
+ val serialized = json.encodeToString(PlayerSettingsData.serializer(), data)
+ assertTrue(serialized.contains(testUUID.toString()))
+ }
+
+ // --- CacheData ---
+
+ @Test
+ fun `CacheData should store version and entries`() {
+ val data = CacheData(version = "1.0", entries = mapOf("hello" to "こんにちは"))
+ assertEquals("1.0", data.version)
+ assertEquals("こんにちは", data.entries["hello"])
+ }
+
+ @Test
+ fun `CacheData serialization round-trip should preserve all fields`() {
+ val original =
+ CacheData(
+ version = "2.0",
+ entries = mapOf("hello" to "こんにちは", "world" to "世界"),
+ )
+
+ val serialized = json.encodeToString(CacheData.serializer(), original)
+ val deserialized = json.decodeFromString(CacheData.serializer(), serialized)
+
+ assertEquals(original.version, deserialized.version)
+ assertEquals(original.entries, deserialized.entries)
+ }
+
+ // --- ChatModeData ---
+
+ @Test
+ fun `ChatModeData default should have empty modes map`() {
+ val data = ChatModeData()
+ assertTrue(data.modes.isEmpty())
+ }
+
+ @Test
+ fun `ChatModeData serialization round-trip should preserve UUID-keyed map`() {
+ val original =
+ ChatModeData(
+ modes =
+ mapOf(
+ testUUID to ChatMode.CHANNEL,
+ testUUID2 to ChatMode.GLOBAL,
+ ),
+ )
+
+ val serialized = json.encodeToString(ChatModeData.serializer(), original)
+ val deserialized = json.decodeFromString(ChatModeData.serializer(), serialized)
+
+ assertEquals(original.modes.size, deserialized.modes.size)
+ assertEquals(ChatMode.CHANNEL, deserialized.modes[testUUID])
+ assertEquals(ChatMode.GLOBAL, deserialized.modes[testUUID2])
+ }
+
+ @Test
+ fun `ChatModeData serialized should contain UUID strings`() {
+ val data = ChatModeData(modes = mapOf(testUUID to ChatMode.GLOBAL))
+ val serialized = json.encodeToString(ChatModeData.serializer(), data)
+ assertTrue(serialized.contains(testUUID.toString()))
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt
new file mode 100644
index 0000000..599a372
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt
@@ -0,0 +1,100 @@
+package dev.m1sk9.lunaticChat.engine.settings
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class UUIDSerializerTest {
+ private val json = Json
+
+ @Serializable
+ private data class UUIDHolder(
+ @Serializable(with = UUIDSerializer::class)
+ val uuid: UUID,
+ )
+
+ @Serializable
+ private data class UUIDAsStringHolder(
+ @Serializable(with = UUIDASStringSerializer::class)
+ val uuid: UUID,
+ )
+
+ @Test
+ fun `UUIDSerializer should serialize UUID to string`() {
+ val uuid = UUID.fromString("12345678-1234-1234-1234-123456789abc")
+ val holder = UUIDHolder(uuid)
+
+ val jsonString = json.encodeToString(UUIDHolder.serializer(), holder)
+
+ assert(jsonString.contains("12345678-1234-1234-1234-123456789abc"))
+ }
+
+ @Test
+ fun `UUIDSerializer should deserialize string to UUID`() {
+ val jsonString = """{"uuid":"12345678-1234-1234-1234-123456789abc"}"""
+ val holder = json.decodeFromString(UUIDHolder.serializer(), jsonString)
+
+ assertEquals(UUID.fromString("12345678-1234-1234-1234-123456789abc"), holder.uuid)
+ }
+
+ @Test
+ fun `UUIDSerializer round-trip should preserve UUID`() {
+ val originalUuid = UUID.randomUUID()
+ val holder = UUIDHolder(originalUuid)
+
+ val jsonString = json.encodeToString(UUIDHolder.serializer(), holder)
+ val decoded = json.decodeFromString(UUIDHolder.serializer(), jsonString)
+
+ assertEquals(originalUuid, decoded.uuid)
+ }
+
+ @Test
+ fun `UUIDASStringSerializer should serialize UUID to string`() {
+ val uuid = UUID.fromString("abcdef01-2345-6789-abcd-ef0123456789")
+ val holder = UUIDAsStringHolder(uuid)
+
+ val jsonString = json.encodeToString(UUIDAsStringHolder.serializer(), holder)
+
+ assert(jsonString.contains("abcdef01-2345-6789-abcd-ef0123456789"))
+ }
+
+ @Test
+ fun `UUIDASStringSerializer should deserialize string to UUID`() {
+ val jsonString = """{"uuid":"abcdef01-2345-6789-abcd-ef0123456789"}"""
+ val holder = json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString)
+
+ assertEquals(UUID.fromString("abcdef01-2345-6789-abcd-ef0123456789"), holder.uuid)
+ }
+
+ @Test
+ fun `UUIDASStringSerializer round-trip should preserve UUID`() {
+ val originalUuid = UUID.randomUUID()
+ val holder = UUIDAsStringHolder(originalUuid)
+
+ val jsonString = json.encodeToString(UUIDAsStringHolder.serializer(), holder)
+ val decoded = json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString)
+
+ assertEquals(originalUuid, decoded.uuid)
+ }
+
+ @Test
+ fun `UUIDSerializer should fail on invalid UUID string`() {
+ val jsonString = """{"uuid":"not-a-valid-uuid"}"""
+
+ assertFailsWith<Exception> {
+ json.decodeFromString(UUIDHolder.serializer(), jsonString)
+ }
+ }
+
+ @Test
+ fun `UUIDASStringSerializer should fail on invalid UUID string`() {
+ val jsonString = """{"uuid":"not-a-valid-uuid"}"""
+
+ assertFailsWith<Exception> {
+ json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString)
+ }
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt
index a3dea37..82dcd9f 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt
@@ -39,7 +39,7 @@ class StatusCommand(
handleResult(context, result)
}
- private fun execute(ctx: CommandContext): CommandResult {
+ internal fun execute(ctx: CommandContext): CommandResult {
val sender = ctx.requirePlayer()
val features = configuration.features
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt
index 90d4e6d..5a6b52f 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt
@@ -68,7 +68,7 @@ class ChannelBanCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
playerName: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt
index cad2231..fa97a46 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt
@@ -81,7 +81,7 @@ class ChannelCreateCommand(
),
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
channelId: String,
name: String,
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt
index 8ca3a55..f39d8e8 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt
@@ -72,7 +72,7 @@ class ChannelDeleteCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
channelId: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt
index cb040d6..fc32f9b 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt
@@ -63,7 +63,7 @@ class ChannelInfoCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
channelIdArg: String?,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt
index d4ce07a..37c8d7a 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt
@@ -58,7 +58,7 @@ class ChannelInviteCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
playerName: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt
index 83a40b9..2edb0da 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt
@@ -62,7 +62,7 @@ class ChannelJoinCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
channelId: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt
index 672cf2d..452c48d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt
@@ -66,7 +66,7 @@ class ChannelKickCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
playerName: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt
index 5e45d27..d763853 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt
@@ -40,7 +40,7 @@ class ChannelLeaveCommand(
handleResult(context, result)
}
- private fun execute(ctx: CommandContext): CommandResult {
+ internal fun execute(ctx: CommandContext): CommandResult {
val sender = ctx.requirePlayer()
// Get current channel before leaving
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt
index 1004baf..c0df8bf 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt
@@ -58,7 +58,7 @@ class ChannelListCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
page: Int,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt
index 82caa39..a368abb 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt
@@ -64,7 +64,7 @@ class ChannelModCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
playerName: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt
index 576b072..6724052 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt
@@ -64,7 +64,7 @@ class ChannelOwnershipCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
playerName: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
index 3b65765..7fb41c6 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
@@ -48,7 +48,7 @@ class ChannelStatusCommand(
handleResult(context, result)
}
- private fun execute(ctx: CommandContext): CommandResult {
+ internal fun execute(ctx: CommandContext): CommandResult {
val sender = ctx.requirePlayer()
// Get active channel
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
index 13d44d2..6f8cc8f 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
@@ -58,7 +58,7 @@ class ChannelSwitchCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
channelId: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt
index e7a7295..043ca01 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt
@@ -63,7 +63,7 @@ class ChannelUnbanCommand(
},
)
- private fun execute(
+ internal fun execute(
ctx: CommandContext,
playerName: String,
): CommandResult {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt
index cea1814..e447639 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt
@@ -37,7 +37,7 @@ class ChatModeToggleCommand(
handleResult(context, result)
}
- private fun execute(ctx: CommandContext): CommandResult {
+ internal fun execute(ctx: CommandContext): CommandResult {
val sender = ctx.requirePlayer()
val newMode = chatModeManager.toggleChatMode(sender.uniqueId)
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/impl/lc/StatusCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommandTest.kt
new file mode 100644
index 0000000..95c8b92
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommandTest.kt
@@ -0,0 +1,125 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.BuildInfo
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.unmockkObject
+import io.mockk.verify
+import net.kyori.adventure.text.Component
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class StatusCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private fun createDependencies(configuration: LunaticChatConfiguration = TestUtils.createTestConfiguration()): TestDeps {
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+
+ every { plugin.velocityConnectionManager } returns null
+
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+
+ val command = StatusCommand(plugin, languageManager, configuration)
+ return TestDeps(command, ctx, plugin, languageManager, configuration, mockPlayer)
+ }
+
+ private data class TestDeps(
+ val command: StatusCommand,
+ val ctx: CommandContext,
+ val plugin: LunaticChat,
+ val languageManager: LanguageManager,
+ val configuration: LunaticChatConfiguration,
+ val mockPlayer: org.bukkit.entity.Player,
+ )
+
+ @Test
+ fun `execute should return Success`() {
+ val deps = createDependencies()
+
+ mockkObject(BuildInfo)
+ try {
+ every { BuildInfo.versionWithCommit() } returns "0.10.0-test"
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ } finally {
+ unmockkObject(BuildInfo)
+ }
+ }
+
+ @Test
+ fun `execute should send messages to player`() {
+ val deps = createDependencies()
+
+ mockkObject(BuildInfo)
+ try {
+ every { BuildInfo.versionWithCommit() } returns "0.10.0-test"
+
+ deps.command.execute(deps.ctx)
+
+ verify(atLeast = 1) { deps.mockPlayer.sendMessage(any<Component>()) }
+ } finally {
+ unmockkObject(BuildInfo)
+ }
+ }
+
+ @Test
+ fun `execute with velocity enabled should check connection state`() {
+ val velocityConfig =
+ TestUtils.createTestConfiguration().let { config ->
+ config.copy(
+ features =
+ config.features.copy(
+ velocityIntegration = config.features.velocityIntegration.copy(enabled = true),
+ ),
+ )
+ }
+ val deps = createDependencies(configuration = velocityConfig)
+
+ val velocityManager = mockk<VelocityConnectionManager>(relaxed = true)
+ every { velocityManager.getState() } returns VelocityConnectionManager.ConnectionState.CONNECTED
+ every { deps.plugin.velocityConnectionManager } returns velocityManager
+
+ mockkObject(BuildInfo)
+ try {
+ every { BuildInfo.versionWithCommit() } returns "0.10.0-test"
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ } finally {
+ unmockkObject(BuildInfo)
+ }
+ }
+
+ @Test
+ fun `execute should display feature toggles`() {
+ val deps = createDependencies()
+
+ mockkObject(BuildInfo)
+ try {
+ every { BuildInfo.versionWithCommit() } returns "0.10.0-test"
+
+ deps.command.execute(deps.ctx)
+
+ // Verify multiple sendMessage calls for version, health, features, config, and links
+ verify(atLeast = 5) { deps.mockPlayer.sendMessage(any<Component>()) }
+ } finally {
+ unmockkObject(BuildInfo)
+ }
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt
new file mode 100644
index 0000000..59fc98a
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt
@@ -0,0 +1,137 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerAlreadyBannedException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import java.util.UUID
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelBanCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val targetUUID = UUID.fromString("00000002-0000-0000-0000-000000000000")
+ private val channelId = "test-ch"
+
+ private val plugin = mockk<LunaticChat>(relaxed = true)
+ private val channelManager = mockk<ChannelManager>(relaxed = true)
+ private val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ private val notificationHandler = mockk<ChannelNotificationHandler>(relaxed = true)
+ private val languageManager = mockk<LanguageManager>(relaxed = true)
+
+ private val command = ChannelBanCommand(plugin, channelManager, membershipManager, notificationHandler, languageManager)
+
+ @BeforeTest
+ fun setUp() {
+ mockkStatic(Bukkit::class)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ }
+
+ @AfterTest
+ fun tearDown() {
+ unmockkStatic(Bukkit::class)
+ }
+
+ private fun createContext(): CommandContext {
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+ return ctx
+ }
+
+ private fun setupOfflinePlayer(
+ hasPlayedBefore: Boolean = true,
+ isOnline: Boolean = false,
+ ): OfflinePlayer {
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.uniqueId } returns targetUUID
+ every { offlinePlayer.hasPlayedBefore() } returns hasPlayedBefore
+ every { offlinePlayer.isOnline } returns isOnline
+ every { offlinePlayer.name } returns "TargetPlayer"
+ every { Bukkit.getOfflinePlayer(any<String>()) } returns offlinePlayer
+ return offlinePlayer
+ }
+
+ @Test
+ fun `execute should return SuccessWithMessage on ban`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { Bukkit.getPlayer(any<String>()) } returns null
+
+ val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L)
+ every { channelManager.banPlayer(channelId, targetUUID) } returns Result.success(channel)
+ every { channelManager.getChannel(channelId) } returns Result.success(channel)
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify { channelManager.banPlayer(channelId, targetUUID) }
+ }
+
+ @Test
+ fun `execute should return Failure when no active channel`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when no permission`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when player not found`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer(hasPlayedBefore = false, isOnline = false)
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when already banned`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { Bukkit.getPlayer(any<String>()) } returns null
+
+ every { channelManager.banPlayer(channelId, targetUUID) } returns
+ Result.failure(ChannelPlayerAlreadyBannedException(targetUUID, channelId))
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommandTest.kt
new file mode 100644
index 0000000..9c57eec
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommandTest.kt
@@ -0,0 +1,128 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelLimitExceededException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelCreateCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private fun createDependencies(): Triple<CommandContext, ChannelManager, LanguageManager> {
+ val player = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns player
+
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ every { languageManager.getMessage(any()) } returns "test message"
+
+ return Triple(ctx, channelManager, languageManager)
+ }
+
+ @Test
+ fun `execute with valid ID should return SuccessWithMessage`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelCreateCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.createChannel(any()) } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "test-ch", name = "Test Channel", ownerId = testUUID),
+ )
+
+ val result = command.execute(ctx, "test-ch", "Test Channel", null, false)
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ }
+
+ @Test
+ fun `execute with private channel should return SuccessWithMessage`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelCreateCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.createChannel(any()) } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "private-ch", name = "Private Channel", ownerId = testUUID, isPrivate = true),
+ )
+
+ val result = command.execute(ctx, "private-ch", "Private Channel", null, true)
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ }
+
+ @Test
+ fun `execute with invalid ID pattern should return Failure`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelCreateCommand(plugin, channelManager, languageManager)
+
+ val result = command.execute(ctx, "ab", "Short ID", null, false)
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute when channel already exists should return Failure`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelCreateCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.createChannel(any()) } returns
+ Result.failure(
+ IllegalArgumentException("Channel already exists"),
+ )
+
+ val result = command.execute(ctx, "existing-ch", "Existing Channel", null, false)
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute when limit exceeded should return Failure`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelCreateCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.createChannel(any()) } returns
+ Result.failure(
+ ChannelLimitExceededException(10),
+ )
+
+ val result = command.execute(ctx, "new-channel", "New Channel", null, false)
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute with description should pass it to channel`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelCreateCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.createChannel(any()) } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "desc-ch", name = "Desc Channel", description = "test desc", ownerId = testUUID),
+ )
+
+ val result = command.execute(ctx, "desc-ch", "Desc Channel", "test desc", false)
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify {
+ channelManager.createChannel(
+ match { it.description == "test desc" },
+ )
+ }
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommandTest.kt
new file mode 100644
index 0000000..5b76978
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommandTest.kt
@@ -0,0 +1,95 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelDeleteCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private fun createDependencies(hasBypass: Boolean = false): Triple<CommandContext, ChannelManager, LanguageManager> {
+ val player = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ every { player.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode) } returns hasBypass
+
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns player
+
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ every { languageManager.getMessage(any()) } returns "test message"
+
+ return Triple(ctx, channelManager, languageManager)
+ }
+
+ @Test
+ fun `execute should return SuccessWithMessage on successful delete`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelDeleteCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.deleteChannel("test-ch", testUUID, false) } returns Result.success(Unit)
+
+ val result = command.execute(ctx, "test-ch")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when channel not found`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelDeleteCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.deleteChannel("unknown-ch", testUUID, false) } returns
+ Result.failure(
+ ChannelNotFoundException("unknown-ch"),
+ )
+
+ val result = command.execute(ctx, "unknown-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when no owner permission`() {
+ val (ctx, channelManager, languageManager) = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelDeleteCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.deleteChannel("other-ch", testUUID, false) } returns
+ Result.failure(
+ ChannelNoOwnerPermissionException(testUUID),
+ )
+
+ val result = command.execute(ctx, "other-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute with bypass permission should pass hasBypass true`() {
+ val (ctx, channelManager, languageManager) = createDependencies(hasBypass = true)
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelDeleteCommand(plugin, channelManager, languageManager)
+
+ every { channelManager.deleteChannel("test-ch", testUUID, true) } returns Result.success(Unit)
+
+ val result = command.execute(ctx, "test-ch")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify { channelManager.deleteChannel("test-ch", testUUID, true) }
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommandTest.kt
new file mode 100644
index 0000000..f1cd3a3
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommandTest.kt
@@ -0,0 +1,123 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelInfoCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val channelId = "test-channel-1"
+
+ private fun createDependencies(): TestDeps {
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+
+ val command = ChannelInfoCommand(plugin, channelManager, languageManager)
+ return TestDeps(command, ctx, channelManager, languageManager, mockPlayer)
+ }
+
+ private data class TestDeps(
+ val command: ChannelInfoCommand,
+ val ctx: CommandContext,
+ val channelManager: ChannelManager,
+ val languageManager: LanguageManager,
+ val mockPlayer: org.bukkit.entity.Player,
+ )
+
+ private fun mockBukkitOfflinePlayer() {
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.name } returns "Player1"
+ every { Bukkit.getOfflinePlayer(any<UUID>()) } returns offlinePlayer
+ }
+
+ @Test
+ fun `execute with channelId should return Success`() {
+ val deps = createDependencies()
+ val channel = TestUtils.createTestChannel(id = channelId, ownerId = testUUID)
+ val members =
+ listOf(
+ TestUtils.createTestChannelMember(channelId = channelId, playerId = testUUID),
+ )
+
+ every { deps.channelManager.getChannel(channelId) } returns Result.success(channel)
+ every { deps.channelManager.getChannelMembers(channelId) } returns Result.success(members)
+
+ mockkStatic(Bukkit::class)
+ try {
+ mockBukkitOfflinePlayer()
+
+ val result = deps.command.execute(deps.ctx, channelId)
+
+ assertIs<CommandResult.Success>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute with null channelId should use active channel`() {
+ val deps = createDependencies()
+ val channel = TestUtils.createTestChannel(id = channelId, ownerId = testUUID)
+ val members =
+ listOf(
+ TestUtils.createTestChannelMember(channelId = channelId, playerId = testUUID),
+ )
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { deps.channelManager.getChannel(channelId) } returns Result.success(channel)
+ every { deps.channelManager.getChannelMembers(channelId) } returns Result.success(members)
+
+ mockkStatic(Bukkit::class)
+ try {
+ mockBukkitOfflinePlayer()
+
+ val result = deps.command.execute(deps.ctx, null)
+
+ assertIs<CommandResult.Success>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute should return Failure when no channel specified and no active channel`() {
+ val deps = createDependencies()
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns null
+
+ val result = deps.command.execute(deps.ctx, null)
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when channel not found`() {
+ val deps = createDependencies()
+
+ every { deps.channelManager.getChannel("nonexistent") } returns
+ Result.failure(ChannelNotFoundException("nonexistent"))
+
+ val result = deps.command.execute(deps.ctx, "nonexistent")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt
new file mode 100644
index 0000000..c51f460
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt
@@ -0,0 +1,152 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import org.bukkit.Bukkit
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelInviteCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val targetUUID = UUID.fromString("00000002-0000-0000-0000-000000000000")
+ private val channelId = "test-channel-1"
+
+ private fun createDependencies(): TestDeps {
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+
+ val command = ChannelInviteCommand(plugin, channelManager, membershipManager, languageManager)
+ return TestDeps(command, ctx, channelManager, membershipManager, languageManager, mockPlayer)
+ }
+
+ private data class TestDeps(
+ val command: ChannelInviteCommand,
+ val ctx: CommandContext,
+ val channelManager: ChannelManager,
+ val membershipManager: ChannelMembershipManager,
+ val languageManager: LanguageManager,
+ val mockPlayer: org.bukkit.entity.Player,
+ )
+
+ @Test
+ fun `execute should return SuccessWithMessage on invite`() {
+ val deps = createDependencies()
+ val targetPlayer = TestUtils.createMockPlayer(uuid = targetUUID, name = "TargetPlayer")
+ val channel = TestUtils.createTestChannel(id = channelId, ownerId = testUUID)
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ every { deps.channelManager.isPlayerBanned(channelId, targetUUID) } returns Result.success(false)
+ every { deps.membershipManager.joinChannel(targetUUID, channelId, bypassPrivateCheck = true) } returns Result.success(Unit)
+ every { deps.channelManager.getChannel(channelId) } returns Result.success(channel)
+
+ mockkStatic(Bukkit::class)
+ try {
+ every { Bukkit.getPlayer(any<String>()) } returns targetPlayer
+
+ val result = deps.command.execute(deps.ctx, "TargetPlayer")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute should return Failure when inviting self`() {
+ val deps = createDependencies()
+ val selfTarget = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+
+ mockkStatic(Bukkit::class)
+ try {
+ every { Bukkit.getPlayer(any<String>()) } returns selfTarget
+
+ val result = deps.command.execute(deps.ctx, "Player1")
+
+ assertIs<CommandResult.Failure>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute should return Failure when target is banned`() {
+ val deps = createDependencies()
+ val targetPlayer = TestUtils.createMockPlayer(uuid = targetUUID, name = "TargetPlayer")
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ every { deps.channelManager.isPlayerBanned(channelId, targetUUID) } returns Result.success(true)
+
+ mockkStatic(Bukkit::class)
+ try {
+ every { Bukkit.getPlayer(any<String>()) } returns targetPlayer
+
+ val result = deps.command.execute(deps.ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute should return Failure when player not found`() {
+ val deps = createDependencies()
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+
+ mockkStatic(Bukkit::class)
+ try {
+ every { Bukkit.getPlayer(any<String>()) } returns null
+
+ val result = deps.command.execute(deps.ctx, "OfflinePlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute should return Failure when no permission`() {
+ val deps = createDependencies()
+ val targetPlayer = TestUtils.createMockPlayer(uuid = targetUUID, name = "TargetPlayer")
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER
+
+ mockkStatic(Bukkit::class)
+ try {
+ every { Bukkit.getPlayer(any<String>()) } returns targetPlayer
+
+ val result = deps.command.execute(deps.ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommandTest.kt
new file mode 100644
index 0000000..72f0040
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommandTest.kt
@@ -0,0 +1,151 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+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.ChannelPlayerBannedException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelJoinCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private data class Dependencies(
+ val ctx: CommandContext,
+ val channelManager: ChannelManager,
+ val membershipManager: ChannelMembershipManager,
+ val notificationHandler: ChannelNotificationHandler,
+ val languageManager: LanguageManager,
+ )
+
+ private fun createDependencies(): Dependencies {
+ val player = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns player
+
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ val notificationHandler = mockk<ChannelNotificationHandler>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ every { languageManager.getMessage(any()) } returns "test message"
+
+ every { channelManager.getChannel(any()) } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "test-ch", name = "Test Channel"),
+ )
+
+ return Dependencies(ctx, channelManager, membershipManager, notificationHandler, languageManager)
+ }
+
+ @Test
+ fun `execute should return SuccessWithMessage on join`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelJoinCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.membershipManager.joinChannel(testUUID, "test-ch") } returns Result.success(Unit)
+
+ val result = command.execute(deps.ctx, "test-ch")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when channel not found`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelJoinCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.membershipManager.joinChannel(testUUID, "unknown-ch") } returns
+ Result.failure(
+ ChannelNotFoundException("unknown-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "unknown-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when already active`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelJoinCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.membershipManager.joinChannel(testUUID, "test-ch") } returns
+ Result.failure(
+ ChannelAlreadyActiveException(testUUID, "test-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "test-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when already member`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelJoinCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.membershipManager.joinChannel(testUUID, "test-ch") } returns
+ Result.failure(
+ ChannelMemberAlreadyException(testUUID, "test-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "test-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when banned`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelJoinCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.membershipManager.joinChannel(testUUID, "test-ch") } returns
+ Result.failure(
+ ChannelPlayerBannedException(testUUID, "test-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "test-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when private without invite`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelJoinCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.membershipManager.joinChannel(testUUID, "private-ch") } returns
+ Result.failure(
+ ChannelPrivateRequiresInvitationException(testUUID, "private-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "private-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt
new file mode 100644
index 0000000..cab6ef0
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt
@@ -0,0 +1,136 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import java.util.UUID
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelKickCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val targetUUID = UUID.fromString("00000002-0000-0000-0000-000000000000")
+ private val channelId = "test-ch"
+
+ private val plugin = mockk<LunaticChat>(relaxed = true)
+ private val channelManager = mockk<ChannelManager>(relaxed = true)
+ private val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ private val notificationHandler = mockk<ChannelNotificationHandler>(relaxed = true)
+ private val languageManager = mockk<LanguageManager>(relaxed = true)
+
+ private val command = ChannelKickCommand(plugin, channelManager, membershipManager, notificationHandler, languageManager)
+
+ @BeforeTest
+ fun setUp() {
+ mockkStatic(Bukkit::class)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ }
+
+ @AfterTest
+ fun tearDown() {
+ unmockkStatic(Bukkit::class)
+ }
+
+ private fun createContext(): CommandContext {
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+ return ctx
+ }
+
+ private fun setupOfflinePlayer(
+ hasPlayedBefore: Boolean = true,
+ isOnline: Boolean = false,
+ ): OfflinePlayer {
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.uniqueId } returns targetUUID
+ every { offlinePlayer.hasPlayedBefore() } returns hasPlayedBefore
+ every { offlinePlayer.isOnline } returns isOnline
+ every { offlinePlayer.name } returns "TargetPlayer"
+ every { Bukkit.getOfflinePlayer(any<String>()) } returns offlinePlayer
+ return offlinePlayer
+ }
+
+ @Test
+ fun `execute should return SuccessWithMessage on kick`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { Bukkit.getPlayer(any<String>()) } returns null
+ every { membershipManager.isMember(targetUUID, channelId) } returns Result.success(true)
+
+ val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L)
+ every { channelManager.removeMember(channelId, targetUUID) } returns Result.success(Unit)
+ every { channelManager.getPlayerChannel(targetUUID) } returns channelId
+ every { channelManager.getChannel(channelId) } returns Result.success(channel)
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify { channelManager.removeMember(channelId, targetUUID) }
+ }
+
+ @Test
+ fun `execute should return Failure when no active channel`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when no permission`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when target not member`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { Bukkit.getPlayer(any<String>()) } returns null
+ every { membershipManager.isMember(targetUUID, channelId) } returns Result.success(false)
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when player not found`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer(hasPlayedBefore = false, isOnline = false)
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommandTest.kt
new file mode 100644
index 0000000..b97fe99
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommandTest.kt
@@ -0,0 +1,125 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelLeaveCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private data class Dependencies(
+ val ctx: CommandContext,
+ val channelManager: ChannelManager,
+ val membershipManager: ChannelMembershipManager,
+ val notificationHandler: ChannelNotificationHandler,
+ val languageManager: LanguageManager,
+ )
+
+ private fun createDependencies(): Dependencies {
+ val player = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns player
+
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ val notificationHandler = mockk<ChannelNotificationHandler>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ every { languageManager.getMessage(any()) } returns "test message"
+
+ return Dependencies(ctx, channelManager, membershipManager, notificationHandler, languageManager)
+ }
+
+ @Test
+ fun `execute should return SuccessWithMessage on leave`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelLeaveCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns "ch-1"
+ every { deps.channelManager.getChannel("ch-1") } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "ch-1", name = "Channel One"),
+ )
+ every { deps.membershipManager.leaveChannel(testUUID) } returns Result.success(Unit)
+
+ val result = command.execute(deps.ctx)
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when no active channel`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelLeaveCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns null
+ every { deps.membershipManager.leaveChannel(testUUID) } returns
+ Result.failure(
+ ChannelNotMemberException(testUUID, ""),
+ )
+
+ val result = command.execute(deps.ctx)
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should broadcast leave notification`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelLeaveCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns "ch-1"
+ every { deps.channelManager.getChannel("ch-1") } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "ch-1", name = "Channel One"),
+ )
+ every { deps.membershipManager.leaveChannel(testUUID) } returns Result.success(Unit)
+
+ command.execute(deps.ctx)
+
+ verify { deps.notificationHandler.broadcastLeave("ch-1", "Player1") }
+ }
+
+ @Test
+ fun `execute should show channel name in success message`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command =
+ ChannelLeaveCommand(plugin, deps.channelManager, deps.membershipManager, deps.notificationHandler, deps.languageManager)
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns "ch-1"
+ every { deps.channelManager.getChannel("ch-1") } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "ch-1", name = "My Channel"),
+ )
+ every { deps.membershipManager.leaveChannel(testUUID) } returns Result.success(Unit)
+
+ val result = command.execute(deps.ctx)
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify {
+ deps.languageManager.getMessage(
+ "channel.leave.success",
+ match { it["channelName"] == "My Channel" },
+ )
+ }
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommandTest.kt
new file mode 100644
index 0000000..5ca6184
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommandTest.kt
@@ -0,0 +1,107 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelListCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private fun createDependencies(): TestDeps {
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+
+ val command = ChannelListCommand(plugin, channelManager, languageManager)
+ return TestDeps(command, ctx, channelManager, languageManager, mockPlayer)
+ }
+
+ private data class TestDeps(
+ val command: ChannelListCommand,
+ val ctx: CommandContext,
+ val channelManager: ChannelManager,
+ val languageManager: LanguageManager,
+ val mockPlayer: org.bukkit.entity.Player,
+ )
+
+ @Test
+ fun `execute should return Success with public channels`() {
+ val deps = createDependencies()
+ val channels =
+ listOf(
+ TestUtils.createTestChannel(id = "channel-1", name = "Channel 1", ownerId = testUUID),
+ TestUtils.createTestChannel(id = "channel-2", name = "Channel 2", ownerId = testUUID),
+ )
+ val members =
+ listOf(
+ TestUtils.createTestChannelMember(channelId = "channel-1", playerId = testUUID),
+ )
+
+ every { deps.channelManager.getPublicChannels() } returns Result.success(channels)
+ every { deps.channelManager.getChannelMembers(any()) } returns Result.success(members)
+
+ val result = deps.command.execute(deps.ctx, 1)
+
+ assertIs<CommandResult.Success>(result)
+ }
+
+ @Test
+ fun `execute should return Success with empty list message`() {
+ val deps = createDependencies()
+
+ every { deps.channelManager.getPublicChannels() } returns Result.success(emptyList())
+
+ val result = deps.command.execute(deps.ctx, 1)
+
+ assertIs<CommandResult.Success>(result)
+ }
+
+ @Test
+ fun `execute should return Success on page 1 of multiple`() {
+ val deps = createDependencies()
+ val channels =
+ (1..15).map { i ->
+ TestUtils.createTestChannel(
+ id = "channel-$i",
+ name = "Channel $i",
+ ownerId = testUUID,
+ )
+ }
+ val members =
+ listOf(
+ TestUtils.createTestChannelMember(channelId = "channel-1", playerId = testUUID),
+ )
+
+ every { deps.channelManager.getPublicChannels() } returns Result.success(channels)
+ every { deps.channelManager.getChannelMembers(any()) } returns Result.success(members)
+
+ val result = deps.command.execute(deps.ctx, 1)
+
+ assertIs<CommandResult.Success>(result)
+ }
+
+ @Test
+ fun `execute should return Failure on error`() {
+ val deps = createDependencies()
+
+ every { deps.channelManager.getPublicChannels() } returns Result.failure(ChannelNotFoundException("error"))
+
+ val result = deps.command.execute(deps.ctx, 1)
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt
new file mode 100644
index 0000000..da7c7e2
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt
@@ -0,0 +1,147 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import java.util.UUID
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelModCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val targetUUID = UUID.fromString("00000002-0000-0000-0000-000000000000")
+ private val channelId = "test-ch"
+
+ private val plugin = mockk<LunaticChat>(relaxed = true)
+ private val channelManager = mockk<ChannelManager>(relaxed = true)
+ private val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ private val languageManager = mockk<LanguageManager>(relaxed = true)
+
+ private val command = ChannelModCommand(plugin, channelManager, membershipManager, languageManager)
+
+ @BeforeTest
+ fun setUp() {
+ mockkStatic(Bukkit::class)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ }
+
+ @AfterTest
+ fun tearDown() {
+ unmockkStatic(Bukkit::class)
+ }
+
+ private fun createContext(): CommandContext {
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+ return ctx
+ }
+
+ private fun setupOfflinePlayer(
+ hasPlayedBefore: Boolean = true,
+ isOnline: Boolean = false,
+ ): OfflinePlayer {
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.uniqueId } returns targetUUID
+ every { offlinePlayer.hasPlayedBefore() } returns hasPlayedBefore
+ every { offlinePlayer.isOnline } returns isOnline
+ every { offlinePlayer.name } returns "TargetPlayer"
+ every { Bukkit.getOfflinePlayer(any<String>()) } returns offlinePlayer
+ return offlinePlayer
+ }
+
+ @Test
+ fun `execute should promote to moderator`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MEMBER
+
+ val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L)
+ every { channelManager.updateMemberRole(channelId, targetUUID, ChannelRole.MODERATOR) } returns Result.success(Unit)
+ every { channelManager.getChannel(channelId) } returns Result.success(channel)
+ every { Bukkit.getPlayer(any<String>()) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify { channelManager.updateMemberRole(channelId, targetUUID, ChannelRole.MODERATOR) }
+ }
+
+ @Test
+ fun `execute should demote from moderator`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MODERATOR
+
+ val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L)
+ every { channelManager.updateMemberRole(channelId, targetUUID, ChannelRole.MEMBER) } returns Result.success(Unit)
+ every { channelManager.getChannel(channelId) } returns Result.success(channel)
+ every { Bukkit.getPlayer(any<String>()) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify { channelManager.updateMemberRole(channelId, targetUUID, ChannelRole.MEMBER) }
+ }
+
+ @Test
+ fun `execute should return Failure when modding self`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.uniqueId } returns testUUID
+ every { offlinePlayer.hasPlayedBefore() } returns true
+ every { offlinePlayer.isOnline } returns false
+ every { offlinePlayer.name } returns "Player1"
+ every { Bukkit.getOfflinePlayer(any<String>()) } returns offlinePlayer
+
+ val result = command.execute(ctx, "Player1")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when not owner`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MODERATOR
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when target not member`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt
new file mode 100644
index 0000000..9f25264
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt
@@ -0,0 +1,139 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import java.util.UUID
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelOwnershipCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val targetUUID = UUID.fromString("00000002-0000-0000-0000-000000000000")
+ private val channelId = "test-ch"
+
+ private val plugin = mockk<LunaticChat>(relaxed = true)
+ private val channelManager = mockk<ChannelManager>(relaxed = true)
+ private val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ private val languageManager = mockk<LanguageManager>(relaxed = true)
+
+ private val command = ChannelOwnershipCommand(plugin, channelManager, membershipManager, languageManager)
+
+ @BeforeTest
+ fun setUp() {
+ mockkStatic(Bukkit::class)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ }
+
+ @AfterTest
+ fun tearDown() {
+ unmockkStatic(Bukkit::class)
+ }
+
+ private fun createContext(): CommandContext {
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+ return ctx
+ }
+
+ private fun setupOfflinePlayer(
+ hasPlayedBefore: Boolean = true,
+ isOnline: Boolean = false,
+ ): OfflinePlayer {
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.uniqueId } returns targetUUID
+ every { offlinePlayer.hasPlayedBefore() } returns hasPlayedBefore
+ every { offlinePlayer.isOnline } returns isOnline
+ every { offlinePlayer.name } returns "TargetPlayer"
+ every { Bukkit.getOfflinePlayer(any<String>()) } returns offlinePlayer
+ return offlinePlayer
+ }
+
+ @Test
+ fun `execute should transfer ownership successfully`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MEMBER
+
+ val updatedChannel = Channel(id = channelId, name = "Test Channel", ownerId = targetUUID, createdAt = 1000L)
+ every { channelManager.updateChannelOwner(channelId, targetUUID) } returns Result.success(updatedChannel)
+ every { Bukkit.getPlayer(any<String>()) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify { channelManager.updateChannelOwner(channelId, targetUUID) }
+ }
+
+ @Test
+ fun `execute should return Failure when transferring to self`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.uniqueId } returns testUUID
+ every { offlinePlayer.hasPlayedBefore() } returns true
+ every { offlinePlayer.isOnline } returns false
+ every { offlinePlayer.name } returns "Player1"
+ every { Bukkit.getOfflinePlayer(any<String>()) } returns offlinePlayer
+
+ val result = command.execute(ctx, "Player1")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when target not member`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+ every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when not owner`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MODERATOR
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when player not found`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer(hasPlayedBefore = false, isOnline = false)
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt
new file mode 100644
index 0000000..301160b
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt
@@ -0,0 +1,142 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelStatusCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val channelId = "test-channel-1"
+
+ private fun createDependencies(): TestDeps {
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+
+ val command = ChannelStatusCommand(plugin, channelManager, membershipManager, languageManager)
+ return TestDeps(command, ctx, channelManager, membershipManager, languageManager, mockPlayer)
+ }
+
+ private data class TestDeps(
+ val command: ChannelStatusCommand,
+ val ctx: CommandContext,
+ val channelManager: ChannelManager,
+ val membershipManager: ChannelMembershipManager,
+ val languageManager: LanguageManager,
+ val mockPlayer: org.bukkit.entity.Player,
+ )
+
+ private fun mockBukkitOfflinePlayer() {
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.name } returns "Player1"
+ every { Bukkit.getOfflinePlayer(any<UUID>()) } returns offlinePlayer
+ }
+
+ @Test
+ fun `execute with active channel should return Success`() {
+ val deps = createDependencies()
+ val channel = TestUtils.createTestChannel(id = channelId, ownerId = testUUID)
+ val members =
+ listOf(
+ TestUtils.createTestChannelMember(channelId = channelId, playerId = testUUID),
+ )
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { deps.channelManager.getChannel(channelId) } returns Result.success(channel)
+ every { deps.channelManager.getChannelMembers(channelId) } returns Result.success(members)
+ every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(listOf(channelId))
+
+ mockkStatic(Bukkit::class)
+ try {
+ mockBukkitOfflinePlayer()
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute without active channel should return Success`() {
+ val deps = createDependencies()
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns null
+ every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(emptyList())
+
+ mockkStatic(Bukkit::class)
+ try {
+ mockBukkitOfflinePlayer()
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute with multiple channels should return Success`() {
+ val deps = createDependencies()
+ val channelIds = listOf("channel-1", "channel-2", "channel-3")
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns "channel-1"
+ every { deps.channelManager.getChannel("channel-1") } returns
+ Result.success(TestUtils.createTestChannel(id = "channel-1", name = "Channel 1", ownerId = testUUID))
+ every { deps.channelManager.getChannel("channel-2") } returns
+ Result.success(TestUtils.createTestChannel(id = "channel-2", name = "Channel 2", ownerId = testUUID))
+ every { deps.channelManager.getChannel("channel-3") } returns
+ Result.success(TestUtils.createTestChannel(id = "channel-3", name = "Channel 3", ownerId = testUUID))
+ every { deps.channelManager.getChannelMembers(any()) } returns
+ Result.success(
+ listOf(TestUtils.createTestChannelMember(playerId = testUUID)),
+ )
+ every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(channelIds)
+
+ mockkStatic(Bukkit::class)
+ try {
+ mockBukkitOfflinePlayer()
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ } finally {
+ unmockkStatic(Bukkit::class)
+ }
+ }
+
+ @Test
+ fun `execute should return Failure on error`() {
+ val deps = createDependencies()
+
+ every { deps.channelManager.getPlayerChannel(testUUID) } returns null
+ every { deps.membershipManager.getPlayerChannels(testUUID) } returns
+ Result.failure(ChannelNotFoundException("error"))
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommandTest.kt
new file mode 100644
index 0000000..eb78956
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommandTest.kt
@@ -0,0 +1,108 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelSwitchCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private data class Dependencies(
+ val ctx: CommandContext,
+ val channelManager: ChannelManager,
+ val membershipManager: ChannelMembershipManager,
+ val languageManager: LanguageManager,
+ )
+
+ private fun createDependencies(): Dependencies {
+ val player = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns player
+
+ val channelManager = mockk<ChannelManager>(relaxed = true)
+ val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ every { languageManager.getMessage(any()) } returns "test message"
+
+ every { channelManager.getChannel(any()) } returns
+ Result.success(
+ TestUtils.createTestChannel(id = "test-ch", name = "Test Channel"),
+ )
+
+ return Dependencies(ctx, channelManager, membershipManager, languageManager)
+ }
+
+ @Test
+ fun `execute should return SuccessWithMessage on switch`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelSwitchCommand(plugin, deps.channelManager, deps.membershipManager, deps.languageManager)
+
+ every { deps.membershipManager.switchChannel(testUUID, "test-ch") } returns Result.success(Unit)
+
+ val result = command.execute(deps.ctx, "test-ch")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when channel not found`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelSwitchCommand(plugin, deps.channelManager, deps.membershipManager, deps.languageManager)
+
+ every { deps.membershipManager.switchChannel(testUUID, "unknown-ch") } returns
+ Result.failure(
+ ChannelNotFoundException("unknown-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "unknown-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when already active`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelSwitchCommand(plugin, deps.channelManager, deps.membershipManager, deps.languageManager)
+
+ every { deps.membershipManager.switchChannel(testUUID, "test-ch") } returns
+ Result.failure(
+ ChannelAlreadyActiveException(testUUID, "test-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "test-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when not member`() {
+ val deps = createDependencies()
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val command = ChannelSwitchCommand(plugin, deps.channelManager, deps.membershipManager, deps.languageManager)
+
+ every { deps.membershipManager.switchChannel(testUUID, "other-ch") } returns
+ Result.failure(
+ ChannelNotMemberException(testUUID, "other-ch"),
+ )
+
+ val result = command.execute(deps.ctx, "other-ch")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt
new file mode 100644
index 0000000..16dd383
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt
@@ -0,0 +1,121 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerNotBannedException
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.bukkit.Bukkit
+import org.bukkit.OfflinePlayer
+import java.util.UUID
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChannelUnbanCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+ private val targetUUID = UUID.fromString("00000002-0000-0000-0000-000000000000")
+ private val channelId = "test-ch"
+
+ private val plugin = mockk<LunaticChat>(relaxed = true)
+ private val channelManager = mockk<ChannelManager>(relaxed = true)
+ private val membershipManager = mockk<ChannelMembershipManager>(relaxed = true)
+ private val languageManager = mockk<LanguageManager>(relaxed = true)
+
+ private val command = ChannelUnbanCommand(plugin, channelManager, membershipManager, languageManager)
+
+ @BeforeTest
+ fun setUp() {
+ mockkStatic(Bukkit::class)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ }
+
+ @AfterTest
+ fun tearDown() {
+ unmockkStatic(Bukkit::class)
+ }
+
+ private fun createContext(): CommandContext {
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+ return ctx
+ }
+
+ private fun setupOfflinePlayer(
+ hasPlayedBefore: Boolean = true,
+ isOnline: Boolean = false,
+ ): OfflinePlayer {
+ val offlinePlayer = mockk<OfflinePlayer>(relaxed = true)
+ every { offlinePlayer.uniqueId } returns targetUUID
+ every { offlinePlayer.hasPlayedBefore() } returns hasPlayedBefore
+ every { offlinePlayer.isOnline } returns isOnline
+ every { offlinePlayer.name } returns "TargetPlayer"
+ every { Bukkit.getOfflinePlayer(any<String>()) } returns offlinePlayer
+ return offlinePlayer
+ }
+
+ @Test
+ fun `execute should return SuccessWithMessage on unban`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+
+ val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L)
+ every { channelManager.unbanPlayer(channelId, targetUUID) } returns Result.success(channel)
+ every { channelManager.getChannel(channelId) } returns Result.success(channel)
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.SuccessWithMessage>(result)
+ verify { channelManager.unbanPlayer(channelId, targetUUID) }
+ }
+
+ @Test
+ fun `execute should return Failure when no active channel`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns null
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when no permission`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+
+ @Test
+ fun `execute should return Failure when player not banned`() {
+ val ctx = createContext()
+ every { channelManager.getPlayerChannel(testUUID) } returns channelId
+ every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER
+ setupOfflinePlayer()
+
+ every { channelManager.unbanPlayer(channelId, targetUUID) } returns
+ Result.failure(ChannelPlayerNotBannedException(targetUUID, channelId))
+
+ val result = command.execute(ctx, "TargetPlayer")
+
+ assertIs<CommandResult.Failure>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommandTest.kt
new file mode 100644
index 0000000..d43c867
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommandTest.kt
@@ -0,0 +1,88 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.chatmode
+
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+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 java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class ChatModeToggleCommandTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private fun createDependencies(): TestDeps {
+ val plugin = mockk<LunaticChat>(relaxed = true)
+ val chatModeManager = mockk<ChatModeManager>(relaxed = true)
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+
+ val mockPlayer = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns mockPlayer
+
+ val command = ChatModeToggleCommand(plugin, chatModeManager, languageManager)
+ return TestDeps(command, ctx, chatModeManager, languageManager, mockPlayer)
+ }
+
+ private data class TestDeps(
+ val command: ChatModeToggleCommand,
+ val ctx: CommandContext,
+ val chatModeManager: ChatModeManager,
+ val languageManager: LanguageManager,
+ val mockPlayer: org.bukkit.entity.Player,
+ )
+
+ @Test
+ fun `execute should toggle to CHANNEL mode`() {
+ val deps = createDependencies()
+
+ every { deps.chatModeManager.toggleChatMode(testUUID) } returns ChatMode.CHANNEL
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ verify { deps.chatModeManager.toggleChatMode(testUUID) }
+ }
+
+ @Test
+ fun `execute should toggle to GLOBAL mode`() {
+ val deps = createDependencies()
+
+ every { deps.chatModeManager.toggleChatMode(testUUID) } returns ChatMode.GLOBAL
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ verify { deps.chatModeManager.toggleChatMode(testUUID) }
+ }
+
+ @Test
+ fun `execute should send message to player`() {
+ val deps = createDependencies()
+
+ every { deps.chatModeManager.toggleChatMode(testUUID) } returns ChatMode.CHANNEL
+
+ deps.command.execute(deps.ctx)
+
+ verify { deps.mockPlayer.sendMessage(any<Component>()) }
+ }
+
+ @Test
+ fun `execute should return Success`() {
+ val deps = createDependencies()
+
+ every { deps.chatModeManager.toggleChatMode(testUUID) } returns ChatMode.GLOBAL
+
+ val result = deps.command.execute(deps.ctx)
+
+ assertIs<CommandResult.Success>(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt
new file mode 100644
index 0000000..52927a8
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt
@@ -0,0 +1,101 @@
+package dev.m1sk9.lunaticChat.paper.command.setting
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class SettingHandlerRegistryTest {
+ private fun createMockHandler(settingKey: SettingKey): SettingHandler =
+ object : SettingHandler {
+ override val key: SettingKey = settingKey
+
+ override fun execute(
+ ctx: CommandContext,
+ enable: Boolean,
+ ): CommandResult = CommandResult.Success
+
+ override fun showStatus(ctx: CommandContext): CommandResult = CommandResult.Success
+ }
+
+ @Test
+ fun `register should make handler retrievable`() {
+ val registry = SettingHandlerRegistry()
+ val handler = createMockHandler(SettingKey.Japanese)
+
+ registry.register(handler)
+
+ assertEquals(handler, registry.getHandler(SettingKey.Japanese))
+ }
+
+ @Test
+ fun `registerAll should register multiple handlers`() {
+ val registry = SettingHandlerRegistry()
+ val handler1 = createMockHandler(SettingKey.Japanese)
+ val handler2 = createMockHandler(SettingKey.Notice)
+ val handler3 = createMockHandler(SettingKey.ChNotice)
+
+ registry.registerAll(handler1, handler2, handler3)
+
+ assertEquals(handler1, registry.getHandler(SettingKey.Japanese))
+ assertEquals(handler2, registry.getHandler(SettingKey.Notice))
+ assertEquals(handler3, registry.getHandler(SettingKey.ChNotice))
+ }
+
+ @Test
+ fun `getHandler should return null for unregistered key`() {
+ val registry = SettingHandlerRegistry()
+
+ assertNull(registry.getHandler(SettingKey.Japanese))
+ }
+
+ @Test
+ fun `getAvailableKeys should return registered keys`() {
+ val registry = SettingHandlerRegistry()
+ registry.register(createMockHandler(SettingKey.Japanese))
+ registry.register(createMockHandler(SettingKey.Notice))
+
+ val keys = registry.getAvailableKeys()
+
+ assertEquals(2, keys.size)
+ assertTrue(keys.contains("japanese"))
+ assertTrue(keys.contains("notice"))
+ }
+
+ @Test
+ fun `getAvailableKeys should return empty list for empty registry`() {
+ val registry = SettingHandlerRegistry()
+ assertTrue(registry.getAvailableKeys().isEmpty())
+ }
+
+ @Test
+ fun `register should overwrite handler with same key`() {
+ val registry = SettingHandlerRegistry()
+ val handler1 = createMockHandler(SettingKey.Japanese)
+ val handler2 = createMockHandler(SettingKey.Japanese)
+
+ registry.register(handler1)
+ registry.register(handler2)
+
+ assertEquals(handler2, registry.getHandler(SettingKey.Japanese))
+ assertEquals(1, registry.getAvailableKeys().size)
+ }
+
+ @Test
+ fun `register should return registry for method chaining`() {
+ val registry = SettingHandlerRegistry()
+ val result = registry.register(createMockHandler(SettingKey.Japanese))
+
+ assertEquals(registry, result)
+ }
+
+ @Test
+ fun `registerAll should return registry for method chaining`() {
+ val registry = SettingHandlerRegistry()
+ val result = registry.registerAll(createMockHandler(SettingKey.Japanese))
+
+ assertEquals(registry, result)
+ }
+}
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/command/setting/handler/SettingHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/SettingHandlerTest.kt
new file mode 100644
index 0000000..1967e1c
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/SettingHandlerTest.kt
@@ -0,0 +1,191 @@
+package dev.m1sk9.lunaticChat.paper.command.setting.handler
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import net.kyori.adventure.text.Component
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+class SettingHandlerTest {
+ private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000")
+
+ private fun createDependencies(): Triple<CommandContext, PlayerSettingsManager, LanguageManager> {
+ val player = TestUtils.createMockPlayer(uuid = testUUID, name = "TestPlayer")
+ val ctx = mockk<CommandContext>(relaxed = true)
+ every { ctx.requirePlayer() } returns player
+
+ val settingsManager = mockk<PlayerSettingsManager>(relaxed = true)
+ every { settingsManager.getSettings(testUUID) } returns
+ TestUtils.createTestPlayerSettings(uuid = testUUID)
+
+ val languageManager = mockk<LanguageManager>(relaxed = true)
+ every { languageManager.getMessage(any(), any()) } returns "test message"
+ every { languageManager.getToggleText(any()) } returns "ON"
+
+ return Triple(ctx, settingsManager, languageManager)
+ }
+
+ // --- JapaneseConversionSettingHandler ---
+
+ @Test
+ fun `JapaneseConversionSettingHandler key should be Japanese`() {
+ val (_, settingsManager, languageManager) = createDependencies()
+ val handler = JapaneseConversionSettingHandler(settingsManager, languageManager)
+ assertIs<SettingKey.Japanese>(handler.key)
+ }
+
+ @Test
+ fun `JapaneseConversionSettingHandler execute enable should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = JapaneseConversionSettingHandler(settingsManager, languageManager)
+
+ val result = handler.execute(ctx, true)
+
+ assertIs<CommandResult.Success>(result)
+ verify { settingsManager.updateSettings(match { it.japaneseConversionEnabled }) }
+ }
+
+ @Test
+ fun `JapaneseConversionSettingHandler execute disable should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = JapaneseConversionSettingHandler(settingsManager, languageManager)
+
+ val result = handler.execute(ctx, false)
+
+ assertIs<CommandResult.Success>(result)
+ verify { settingsManager.updateSettings(match { !it.japaneseConversionEnabled }) }
+ }
+
+ @Test
+ fun `JapaneseConversionSettingHandler showStatus should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = JapaneseConversionSettingHandler(settingsManager, languageManager)
+
+ val result = handler.showStatus(ctx)
+
+ assertIs<CommandResult.Success>(result)
+ }
+
+ @Test
+ fun `JapaneseConversionSettingHandler execute should send message to player`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = JapaneseConversionSettingHandler(settingsManager, languageManager)
+
+ handler.execute(ctx, true)
+
+ val player = ctx.requirePlayer()
+ verify { player.sendMessage(any<Component>()) }
+ }
+
+ // --- DirectMessageNoticeSettingHandler ---
+
+ @Test
+ fun `DirectMessageNoticeSettingHandler key should be Notice`() {
+ val (_, settingsManager, languageManager) = createDependencies()
+ val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager)
+ assertIs<SettingKey.Notice>(handler.key)
+ }
+
+ @Test
+ fun `DirectMessageNoticeSettingHandler execute enable should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ val result = handler.execute(ctx, true)
+
+ assertIs<CommandResult.Success>(result)
+ verify { settingsManager.updateSettings(match { it.directMessageNotificationEnabled }) }
+ }
+
+ @Test
+ fun `DirectMessageNoticeSettingHandler execute disable should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ val result = handler.execute(ctx, false)
+
+ assertIs<CommandResult.Success>(result)
+ verify { settingsManager.updateSettings(match { !it.directMessageNotificationEnabled }) }
+ }
+
+ @Test
+ fun `DirectMessageNoticeSettingHandler showStatus should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ val result = handler.showStatus(ctx)
+
+ assertIs<CommandResult.Success>(result)
+ }
+
+ @Test
+ fun `DirectMessageNoticeSettingHandler execute should send message to player`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ handler.execute(ctx, true)
+
+ val player = ctx.requirePlayer()
+ verify { player.sendMessage(any<Component>()) }
+ }
+
+ // --- ChannelMessageNoticeSettingHandler ---
+
+ @Test
+ fun `ChannelMessageNoticeSettingHandler key should be ChNotice`() {
+ val (_, settingsManager, languageManager) = createDependencies()
+ val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager)
+ assertIs<SettingKey.ChNotice>(handler.key)
+ }
+
+ @Test
+ fun `ChannelMessageNoticeSettingHandler execute enable should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ val result = handler.execute(ctx, true)
+
+ assertIs<CommandResult.Success>(result)
+ verify { settingsManager.updateSettings(match { it.channelMessageNotificationEnabled }) }
+ }
+
+ @Test
+ fun `ChannelMessageNoticeSettingHandler execute disable should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ val result = handler.execute(ctx, false)
+
+ assertIs<CommandResult.Success>(result)
+ verify { settingsManager.updateSettings(match { !it.channelMessageNotificationEnabled }) }
+ }
+
+ @Test
+ fun `ChannelMessageNoticeSettingHandler showStatus should return Success`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ val result = handler.showStatus(ctx)
+
+ assertIs<CommandResult.Success>(result)
+ }
+
+ @Test
+ fun `ChannelMessageNoticeSettingHandler execute should send message to player`() {
+ val (ctx, settingsManager, languageManager) = createDependencies()
+ val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager)
+
+ handler.execute(ctx, true)
+
+ val player = ctx.requirePlayer()
+ verify { player.sendMessage(any<Component>()) }
+ }
+}
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") })
+ }
+}
diff --git a/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelayTest.kt b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelayTest.kt
new file mode 100644
index 0000000..50b0db2
--- /dev/null
+++ b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelayTest.kt
@@ -0,0 +1,136 @@
+package dev.m1sk9.lunaticChat.velocity.messaging
+
+import com.velocitypowered.api.proxy.ProxyServer
+import com.velocitypowered.api.proxy.messages.ChannelIdentifier
+import com.velocitypowered.api.proxy.server.RegisteredServer
+import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.slf4j.Logger
+import kotlin.test.Test
+
+class CrossServerChatRelayTest {
+ private fun createRelay(): Triple<CrossServerChatRelay, ProxyServer, Logger> {
+ val server = mockk<ProxyServer>(relaxed = true)
+ val logger = mockk<Logger>(relaxed = true)
+ val relay = CrossServerChatRelay(server, logger)
+ return Triple(relay, server, logger)
+ }
+
+ private fun createRegisteredServer(name: String): RegisteredServer {
+ val server = mockk<RegisteredServer>(relaxed = true)
+ val serverInfo = mockk<com.velocitypowered.api.proxy.server.ServerInfo>(relaxed = true)
+ every { serverInfo.name } returns name
+ every { server.serverInfo } returns serverInfo
+ return server
+ }
+
+ private fun createTestMessage(
+ messageId: String = "msg-1",
+ serverName: String = "lobby",
+ ): PluginMessage.GlobalChatMessage =
+ PluginMessage.GlobalChatMessage(
+ messageId = messageId,
+ serverName = serverName,
+ playerId = "00000001-0000-0000-0000-000000000000",
+ playerName = "TestPlayer",
+ message = "Hello!",
+ timestamp = 1000L,
+ )
+
+ @Test
+ fun `relayGlobalMessage should exclude source server`() {
+ val (relay, proxyServer, _) = createRelay()
+ val sourceServer = createRegisteredServer("lobby")
+ val targetServer = createRegisteredServer("survival")
+
+ every { proxyServer.allServers } returns listOf(sourceServer, targetServer)
+
+ relay.relayGlobalMessage(createTestMessage(), sourceServer)
+
+ verify(exactly = 1) { targetServer.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ verify(exactly = 0) { sourceServer.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ }
+
+ @Test
+ fun `relayGlobalMessage should relay to multiple servers`() {
+ val (relay, proxyServer, _) = createRelay()
+ val sourceServer = createRegisteredServer("lobby")
+ val target1 = createRegisteredServer("survival")
+ val target2 = createRegisteredServer("creative")
+
+ every { proxyServer.allServers } returns listOf(sourceServer, target1, target2)
+
+ relay.relayGlobalMessage(createTestMessage(), sourceServer)
+
+ verify(exactly = 1) { target1.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ verify(exactly = 1) { target2.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ }
+
+ @Test
+ fun `relayGlobalMessage with only source server should relay to zero`() {
+ val (relay, proxyServer, logger) = createRelay()
+ val sourceServer = createRegisteredServer("lobby")
+
+ every { proxyServer.allServers } returns listOf(sourceServer)
+
+ relay.relayGlobalMessage(createTestMessage(), sourceServer)
+
+ verify(exactly = 0) { sourceServer.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ verify { logger.info(match { it.contains("0 servers") }) }
+ }
+
+ @Test
+ fun `relayGlobalMessage should log with messageId and playerName`() {
+ val (relay, proxyServer, logger) = createRelay()
+ val sourceServer = createRegisteredServer("lobby")
+ val targetServer = createRegisteredServer("survival")
+
+ every { proxyServer.allServers } returns listOf(sourceServer, targetServer)
+
+ val message = createTestMessage(messageId = "test-msg-123")
+ relay.relayGlobalMessage(message, sourceServer)
+
+ verify {
+ logger.info(
+ match { msg ->
+ msg.contains("test-msg-123") && msg.contains("TestPlayer")
+ },
+ )
+ }
+ }
+
+ @Test
+ fun `relayGlobalMessage should log error on exception`() {
+ val (relay, proxyServer, logger) = createRelay()
+ val sourceServer = createRegisteredServer("lobby")
+ val targetServer = createRegisteredServer("survival")
+
+ every { proxyServer.allServers } returns listOf(sourceServer, targetServer)
+ every { targetServer.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) } throws
+ RuntimeException("connection failed")
+
+ relay.relayGlobalMessage(createTestMessage(), sourceServer)
+
+ verify { logger.error(match { it.contains("Failed to relay") }, any<Exception>()) }
+ }
+
+ @Test
+ fun `relayGlobalMessage should send encoded message data`() {
+ val (relay, proxyServer, _) = createRelay()
+ val sourceServer = createRegisteredServer("lobby")
+ val targetServer = createRegisteredServer("survival")
+
+ every { proxyServer.allServers } returns listOf(sourceServer, targetServer)
+
+ relay.relayGlobalMessage(createTestMessage(), sourceServer)
+
+ verify {
+ targetServer.sendPluginMessage(
+ any<ChannelIdentifier>(),
+ match<ByteArray> { it.isNotEmpty() },
+ )
+ }
+ }
+}
diff --git a/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt
new file mode 100644
index 0000000..876eec6
--- /dev/null
+++ b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt
@@ -0,0 +1,211 @@
+package dev.m1sk9.lunaticChat.velocity.messaging
+
+import com.velocitypowered.api.event.connection.PluginMessageEvent
+import com.velocitypowered.api.proxy.ProxyServer
+import com.velocitypowered.api.proxy.ServerConnection
+import com.velocitypowered.api.proxy.messages.ChannelIdentifier
+import com.velocitypowered.api.proxy.messages.ChannelMessageSink
+import com.velocitypowered.api.proxy.messages.ChannelMessageSource
+import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier
+import com.velocitypowered.api.proxy.server.RegisteredServer
+import com.velocitypowered.api.proxy.server.ServerInfo
+import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage
+import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec
+import dev.m1sk9.lunaticChat.engine.protocol.ProtocolVersion
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.slf4j.Logger
+import kotlin.test.Test
+
+class PluginMessageHandlerTest {
+ private val channel = MinecraftChannelIdentifier.create("lunaticchat", "main")
+
+ private fun createHandler(pluginVersion: String = "0.10.0"): Triple<PluginMessageHandler, ProxyServer, CrossServerChatRelay> {
+ val plugin = Any()
+ val server = mockk<ProxyServer>(relaxed = true)
+ val logger = mockk<Logger>(relaxed = true)
+ val relay = mockk<CrossServerChatRelay>(relaxed = true)
+
+ val handler = PluginMessageHandler(plugin, server, logger, pluginVersion, relay)
+ return Triple(handler, server, relay)
+ }
+
+ private fun createServerConnection(serverName: String = "lobby"): ServerConnection {
+ val connection = mockk<ServerConnection>(relaxed = true)
+ val serverInfo = mockk<ServerInfo>(relaxed = true)
+ val registeredServer = mockk<RegisteredServer>(relaxed = true)
+ every { serverInfo.name } returns serverName
+ every { connection.serverInfo } returns serverInfo
+ every { connection.server } returns registeredServer
+ return connection
+ }
+
+ private fun createPluginMessageEvent(
+ source: ChannelMessageSource,
+ target: ChannelMessageSink,
+ identifier: ChannelIdentifier,
+ data: ByteArray,
+ ): PluginMessageEvent = PluginMessageEvent(source, target, identifier, data)
+
+ @Test
+ fun `onPluginMessage should handle successful handshake`() {
+ val (handler, _, _) = createHandler(pluginVersion = "0.10.0")
+ val connection = createServerConnection()
+
+ val handshake =
+ PluginMessage.Handshake(
+ pluginVersion = "0.10.0",
+ protocolMajor = ProtocolVersion.MAJOR,
+ protocolMinor = ProtocolVersion.MINOR,
+ protocolPatch = ProtocolVersion.PATCH,
+ )
+ val data = PluginMessageCodec.encode(handshake)
+ val event = createPluginMessageEvent(connection, mockk(relaxed = true), channel, data)
+
+ handler.onPluginMessage(event)
+
+ verify { connection.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ }
+
+ @Test
+ fun `onPluginMessage should reject version mismatch handshake`() {
+ val (handler, _, _) = createHandler(pluginVersion = "0.10.0")
+ val connection = createServerConnection()
+
+ val handshake =
+ PluginMessage.Handshake(
+ pluginVersion = "0.9.0",
+ protocolMajor = ProtocolVersion.MAJOR,
+ protocolMinor = ProtocolVersion.MINOR,
+ protocolPatch = ProtocolVersion.PATCH,
+ )
+ val data = PluginMessageCodec.encode(handshake)
+ val event = createPluginMessageEvent(connection, mockk(relaxed = true), channel, data)
+
+ handler.onPluginMessage(event)
+
+ verify { connection.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ }
+
+ @Test
+ fun `onPluginMessage should reject protocol mismatch handshake`() {
+ val (handler, _, _) = createHandler(pluginVersion = "0.10.0")
+ val connection = createServerConnection()
+
+ val handshake =
+ PluginMessage.Handshake(
+ pluginVersion = "0.10.0",
+ protocolMajor = ProtocolVersion.MAJOR + 1,
+ protocolMinor = ProtocolVersion.MINOR,
+ protocolPatch = ProtocolVersion.PATCH,
+ )
+ val data = PluginMessageCodec.encode(handshake)
+ val event = createPluginMessageEvent(connection, mockk(relaxed = true), channel, data)
+
+ handler.onPluginMessage(event)
+
+ verify { connection.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ }
+
+ @Test
+ fun `onPluginMessage should handle status request`() {
+ val (handler, _, _) = createHandler()
+ val connection = createServerConnection()
+
+ val data = PluginMessageCodec.encode(PluginMessage.StatusRequest)
+ val event = createPluginMessageEvent(connection, mockk(relaxed = true), channel, data)
+
+ handler.onPluginMessage(event)
+
+ verify { connection.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ }
+
+ @Test
+ fun `onPluginMessage should relay global chat message`() {
+ val (handler, _, relay) = createHandler()
+ val connection = createServerConnection()
+
+ val chatMessage =
+ PluginMessage.GlobalChatMessage(
+ messageId = "msg-1",
+ serverName = "lobby",
+ playerId = "00000001-0000-0000-0000-000000000000",
+ playerName = "TestPlayer",
+ message = "Hello!",
+ timestamp = 1000L,
+ )
+ val data = PluginMessageCodec.encode(chatMessage)
+ val event = createPluginMessageEvent(connection, mockk(relaxed = true), channel, data)
+
+ handler.onPluginMessage(event)
+
+ verify { relay.relayGlobalMessage(any<PluginMessage.GlobalChatMessage>(), any<RegisteredServer>()) }
+ }
+
+ @Test
+ fun `onPluginMessage should ignore messages from wrong channel`() {
+ val (handler, _, relay) = createHandler()
+ val connection = createServerConnection()
+
+ val wrongChannel = MinecraftChannelIdentifier.create("other", "channel")
+ val data = PluginMessageCodec.encode(PluginMessage.StatusRequest)
+ val event = createPluginMessageEvent(connection, mockk(relaxed = true), wrongChannel, data)
+
+ handler.onPluginMessage(event)
+
+ verify(exactly = 0) { relay.relayGlobalMessage(any<PluginMessage.GlobalChatMessage>(), any<RegisteredServer>()) }
+ }
+
+ @Test
+ fun `onPluginMessage should ignore messages from non-server source`() {
+ val (handler, _, relay) = createHandler()
+ val source = mockk<ChannelMessageSource>(relaxed = true)
+
+ val data = PluginMessageCodec.encode(PluginMessage.StatusRequest)
+ val event = createPluginMessageEvent(source, mockk(relaxed = true), channel, data)
+
+ handler.onPluginMessage(event)
+
+ verify(exactly = 0) { relay.relayGlobalMessage(any<PluginMessage.GlobalChatMessage>(), any<RegisteredServer>()) }
+ }
+
+ @Test
+ fun `initialize should register channel and listener`() {
+ val (handler, server, _) = createHandler()
+
+ handler.initialize()
+
+ verify { server.channelRegistrar.register(any<ChannelIdentifier>()) }
+ verify { server.eventManager.register(any(), handler) }
+ }
+
+ @Test
+ fun `shutdown should unregister listener`() {
+ val (handler, server, _) = createHandler()
+
+ handler.shutdown()
+
+ verify { server.eventManager.unregisterListener(any(), handler) }
+ }
+
+ @Test
+ fun `handshake with matching patch difference should succeed`() {
+ val (handler, _, _) = createHandler(pluginVersion = "0.10.0")
+ val connection = createServerConnection()
+
+ val handshake =
+ PluginMessage.Handshake(
+ pluginVersion = "0.10.0",
+ protocolMajor = ProtocolVersion.MAJOR,
+ protocolMinor = ProtocolVersion.MINOR,
+ protocolPatch = ProtocolVersion.PATCH + 5,
+ )
+ val data = PluginMessageCodec.encode(handshake)
+ val event = createPluginMessageEvent(connection, mockk(relaxed = true), channel, data)
+
+ handler.onPluginMessage(event)
+
+ verify { connection.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) }
+ }
+}