summaryrefslogtreecommitdiff
path: root/platform-paper/src
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-27 22:09:22 +0900
committerSho Sakuma <me@m1sk9.dev>2026-01-27 22:33:43 +0900
commit5171dc41dc633e51470ea2e9f92b3434547a465c (patch)
tree046ac63c25077673bec55697c0cfb46857f77cb0 /platform-paper/src
parent21d17ca948193a9f4b7ef080d2b949240e64e538 (diff)
downloadLunaticChat-5171dc41dc633e51470ea2e9f92b3434547a465c.tar.gz
LunaticChat-5171dc41dc633e51470ea2e9f92b3434547a465c.tar.bz2
LunaticChat-5171dc41dc633e51470ea2e9f92b3434547a465c.zip
test: Add more test-case
Diffstat (limited to 'platform-paper/src')
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt22
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt49
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt34
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt34
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt14
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt15
-rw-r--r--platform-paper/src/main/resources/languages/en.yml1
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml1
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt208
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManagerTest.kt223
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt134
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt288
13 files changed, 980 insertions, 47 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
index ba2eca6..31a6886 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
@@ -22,7 +22,7 @@ import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.listener.EventListenerRegistry
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.launch
import org.bukkit.event.Listener
import org.bukkit.plugin.java.JavaPlugin
import java.util.concurrent.atomic.AtomicBoolean
@@ -43,13 +43,15 @@ class LunaticChat :
private lateinit var services: ServiceContainer
private lateinit var configuration: LunaticChatConfiguration
private lateinit var serviceInitializer: ServiceInitializer
+ private lateinit var pluginScope: PluginCoroutineScope
private var updateChecker: UpdateChecker? = null
private val updateAvailable = AtomicBoolean(false)
override fun onEnable() {
saveDefaultConfig()
- configuration = ConfigManager.loadConfiguration(config)
+ val configManager = ConfigManager()
+ configuration = configManager.loadConfiguration(config)
if (configuration.debug) {
logger.warning("LunaticChat is running in debug mode.")
@@ -58,6 +60,9 @@ class LunaticChat :
val httpClient = HttpClient(CIO)
+ // Initialize plugin coroutine scope
+ pluginScope = PluginCoroutineScope(this, logger)
+
// Initialize all services
serviceInitializer =
ServiceInitializer(
@@ -93,6 +98,7 @@ class LunaticChat :
}
override fun onDisable() {
+ pluginScope.cancel()
serviceInitializer.shutdown(services)
logger.info("LunaticChat disabled.")
}
@@ -157,6 +163,7 @@ class LunaticChat :
/**
* Initializes the update checker.
+ * Uses plugin coroutine scope instead of runBlocking for non-blocking async execution.
*/
private fun initializeUpdateChecker(httpClient: HttpClient) {
updateChecker =
@@ -165,14 +172,9 @@ class LunaticChat :
logger = logger,
httpClient = httpClient,
)
- server.scheduler.runTaskAsynchronously(
- this,
- Runnable {
- runBlocking {
- checkUpdates()
- }
- },
- )
+ pluginScope.scope.launch {
+ checkUpdates()
+ }
}
private suspend fun checkUpdates() {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt
new file mode 100644
index 0000000..34ae0fc
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt
@@ -0,0 +1,49 @@
+package dev.m1sk9.lunaticChat.paper
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import org.bukkit.plugin.java.JavaPlugin
+import java.util.logging.Logger
+
+/**
+ * Provides a coroutine scope tied to the plugin lifecycle.
+ *
+ * This scope:
+ * - Uses Dispatchers.Default for background CPU-bound work
+ * - Uses SupervisorJob to prevent child failures from canceling the entire scope
+ * - Is properly cancelled when the plugin disables
+ *
+ * Usage:
+ * ```kotlin
+ * pluginScope.launch {
+ * val result = withTimeout(5000) {
+ * someAsyncOperation()
+ * }
+ * // Handle result...
+ * }
+ * ```
+ */
+class PluginCoroutineScope(
+ private val plugin: JavaPlugin,
+ private val logger: Logger,
+) {
+ private val job = SupervisorJob()
+ val scope = CoroutineScope(Dispatchers.Default + job)
+
+ /**
+ * Cancels all coroutines in this scope.
+ * Should be called during plugin disable.
+ */
+ fun cancel() {
+ logger.info("Cancelling plugin coroutine scope...")
+ scope.cancel()
+ logger.info("Plugin coroutine scope cancelled.")
+ }
+
+ /**
+ * Returns true if the scope is still active.
+ */
+ fun isActive(): Boolean = job.isActive
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
index cea20bf..daba3dd 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
@@ -100,8 +100,10 @@ class ServiceInitializer(
// 5. Initialize handlers
val directMessageHandler =
DirectMessageHandler(
+ configuration = configuration,
settingsManager = playerSettingsManager,
romanjiConverter = romajiConverter,
+ languageManager = languageManager,
)
return ServiceContainer(
@@ -227,9 +229,11 @@ class ServiceInitializer(
val messageHandler =
ChannelMessageHandler(
+ configuration = configuration,
settingsManager = settingsManager,
channelManager = manager,
romanjiConverter = romajiConverter,
+ languageManager = languageManager,
logger =
io.ktor.util.logging
.KtorSimpleLogger("ChannelMessageHandler"),
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
index 89adb6f..12426cf 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
@@ -4,22 +4,24 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
import dev.m1sk9.lunaticChat.paper.common.playChannelReceiveNotification
import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification
-import dev.m1sk9.lunaticChat.paper.config.ConfigManager
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import io.ktor.util.logging.Logger
import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.event.HoverEvent
import org.bukkit.Bukkit
import org.bukkit.entity.Player
class ChannelMessageHandler(
+ private val configuration: LunaticChatConfiguration,
private val settingsManager: PlayerSettingsManager?,
private val channelManager: ChannelManager,
private val romanjiConverter: RomanjiConverter?,
+ private val languageManager: LanguageManager,
private val logger: Logger,
) {
- private var lunaticChatConfiguration = ConfigManager.getConfiguration()
-
fun sendChannelMessage(
player: Player,
message: String,
@@ -31,17 +33,19 @@ class ChannelMessageHandler(
val senderSettings = settingsManager?.getSettings(playerId)
- // Handle romaji conversion if enabled (requires blocking for HTTP call)
+ // Handle romaji conversion if enabled
+ // Uses explicit timeout to prevent long blocking (1s max instead of 3s)
val displayMessage =
if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) {
runCatching {
kotlinx.coroutines.runBlocking {
- romanjiConverter
- ?.convert(message)
- ?.let { "$message §e($it)" }
- ?: message
+ kotlinx.coroutines
+ .withTimeoutOrNull(1000) {
+ romanjiConverter!!
+ .convert(message)
+ }?.let { "$message §e($it)" } ?: message
}
- }.getOrNull() ?: message
+ }.getOrElse { message }
} else {
message
}
@@ -60,7 +64,15 @@ class ChannelMessageHandler(
.getDirectMessageSpyPlayers()
.values
.filter { it.isOnline && it.uniqueId != playerId && it.uniqueId !in memberIds }
- .forEach { it.sendMessage(spyMessage) }
+ .forEach {
+ it.sendMessage(
+ spyMessage.hoverEvent(
+ HoverEvent.showText(
+ Component.text(languageManager.getMessage("general.spyMessage")),
+ ),
+ ),
+ )
+ }
context.members.forEach { member ->
Bukkit.getPlayer(member.playerId)?.let { memberPlayer ->
if (memberPlayer.isOnline) {
@@ -88,7 +100,7 @@ class ChannelMessageHandler(
channelName: String,
message: String,
): Component {
- val format = lunaticChatConfiguration.messageFormat.channelMessageFormat
+ val format = configuration.messageFormat.channelMessageFormat
val text =
format
.replace("{sender}", senderName)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
index 6155381..3f7950a 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
@@ -3,11 +3,13 @@ package dev.m1sk9.lunaticChat.paper.chat.handler
import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
import dev.m1sk9.lunaticChat.paper.common.playDirectMessageNotification
import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification
-import dev.m1sk9.lunaticChat.paper.config.ConfigManager
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.event.ClickEvent
+import net.kyori.adventure.text.event.HoverEvent
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import java.util.UUID
@@ -18,11 +20,11 @@ import java.util.concurrent.ConcurrentHashMap
* Tracks the last player who messaged each player for /reply functionality.
*/
class DirectMessageHandler(
+ private val configuration: LunaticChatConfiguration,
private val settingsManager: PlayerSettingsManager?,
private val romanjiConverter: RomanjiConverter?,
+ private val languageManager: LanguageManager,
) {
- private var lunaticChatConfiguration = ConfigManager.getConfiguration()
-
private val lastMessager: ConcurrentHashMap<UUID, UUID> = ConcurrentHashMap()
private val lastRecipient: ConcurrentHashMap<UUID, UUID> = ConcurrentHashMap()
@@ -90,29 +92,39 @@ class DirectMessageHandler(
val senderSettings = settingsManager?.getSettings(sender.uniqueId)
val recipientSettings = settingsManager?.getSettings(recipient.uniqueId)
- // Handle romaji conversion if enabled (requires blocking for HTTP call)
+ // Handle romaji conversion if enabled
+ // Uses explicit timeout to prevent long blocking (1s max instead of 3s)
val displayMessage =
if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) {
runCatching {
kotlinx.coroutines.runBlocking {
- romanjiConverter
- ?.convert(message)
- ?.let { "$message §e($it)" }
- ?: message
+ kotlinx.coroutines
+ .withTimeoutOrNull(1000) {
+ romanjiConverter!!
+ .convert(message)
+ }?.let { "$message §e($it)" } ?: message
}
- }.getOrNull() ?: message
+ }.getOrElse { message }
} else {
message
}
- val format = lunaticChatConfiguration.messageFormat.directMessageFormat
+ val format = configuration.messageFormat.directMessageFormat
val spyMessage = formatMessage(format, sender.name, recipient.name, message)
SpyPermissionManager
.getDirectMessageSpyPlayers()
.values
.filter { it.isOnline && it.uniqueId !in setOf(sender.uniqueId, recipient.uniqueId) }
- .forEach { it.sendMessage(spyMessage) }
+ .forEach {
+ it.sendMessage(
+ spyMessage.hoverEvent(
+ HoverEvent.showText(
+ Component.text(languageManager.getMessage("general.spyMessage")),
+ ),
+ ),
+ )
+ }
val userMessage = formatMessage(format, sender.name, recipient.name, displayMessage)
sender.apply {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
index c53d154..1bd969d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
@@ -8,14 +8,11 @@ import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig
import dev.m1sk9.lunaticChat.paper.i18n.Language
import org.bukkit.configuration.file.FileConfiguration
-// FIXME: ConfigManager uses mutable static state which makes testing difficult
-// and creates hidden global dependencies. Consider refactoring to dependency injection.
-object ConfigManager {
- private var lunaticChatConfiguration: LunaticChatConfiguration? = null
-
- fun getConfiguration(): LunaticChatConfiguration =
- lunaticChatConfiguration ?: IllegalStateException("LunaticChat Config not loaded").let { throw it }
-
+/**
+ * Manages loading and parsing of plugin configuration.
+ * Converted from singleton to dependency injection pattern for better testability.
+ */
+class ConfigManager {
fun loadConfiguration(configFile: FileConfiguration): LunaticChatConfiguration {
val loadedConfig =
LunaticChatConfiguration(
@@ -81,7 +78,6 @@ object ConfigManager {
),
)
- lunaticChatConfiguration = loadedConfig
return loadedConfig
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
index 0fa4fa1..ec8804d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
@@ -54,17 +54,20 @@ class PlayerChatListener(
chatModeManager.getChatMode(player.uniqueId)
}
- // Handle romaji conversion if enabled (requires blocking for HTTP call)
+ // Handle romaji conversion if enabled
+ // Uses explicit timeout to prevent long blocking (1s max instead of 3s)
+ // Note: AsyncChatEvent runs on async thread, so runBlocking here doesn't block main thread
val displayMessage =
if (settings.japaneseConversionEnabled) {
runCatching {
runBlocking {
- romajiConverter
- .convert(messageWithoutPrefix)
- ?.let { "$messageWithoutPrefix §e($it)" }
- ?: messageWithoutPrefix
+ kotlinx.coroutines
+ .withTimeoutOrNull(1000) {
+ romajiConverter
+ .convert(messageWithoutPrefix)
+ }?.let { "$messageWithoutPrefix §e($it)" } ?: messageWithoutPrefix
}
- }.getOrNull() ?: messageWithoutPrefix
+ }.getOrElse { messageWithoutPrefix }
} else {
messageWithoutPrefix
}
diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml
index 2cbff48..767a9b3 100644
--- a/platform-paper/src/main/resources/languages/en.yml
+++ b/platform-paper/src/main/resources/languages/en.yml
@@ -191,6 +191,7 @@ general:
playerOnlyCommand: "This command can only be executed by players."
newUpdateAvailable: "The new version of LunaticChat is now available! You can download it from GitHub or Modrinth."
noPermission: "You do not have permission to execute this command."
+ spyMessage: "You have been granted permission, so this message is displayed in spy mode."
toggle:
off: "Disabled"
diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml
index 766e7e2..a6f2526 100644
--- a/platform-paper/src/main/resources/languages/ja.yml
+++ b/platform-paper/src/main/resources/languages/ja.yml
@@ -191,6 +191,7 @@ general:
playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます"
newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます"
noPermission: "このコマンドを実行する権限がありません"
+ spyMessage: "あなたに権限が付与されているため、このメッセージはスパイ状態で表示されています"
toggle:
on: "有効"
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt
new file mode 100644
index 0000000..66fdbd4
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt
@@ -0,0 +1,208 @@
+package dev.m1sk9.lunaticChat.paper
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMember
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.settings.PlayerChatSettings
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
+import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig
+import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig
+import dev.m1sk9.lunaticChat.paper.config.key.JapaneseConversionFeatureConfig
+import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig
+import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig
+import dev.m1sk9.lunaticChat.paper.i18n.Language
+import io.mockk.mockk
+import org.bukkit.entity.Player
+import org.bukkit.plugin.java.JavaPlugin
+import java.util.UUID
+import java.util.logging.Logger
+
+/**
+ * Common test utilities for LunaticChat tests.
+ * Provides mock factories, test data builders, and assertion helpers.
+ */
+object TestUtils {
+ /**
+ * Creates a test logger that collects log messages for verification.
+ */
+ class TestLogger : Logger("test", null) {
+ val infoMessages = mutableListOf<String>()
+ val warningMessages = mutableListOf<String>()
+ val severeMessages = mutableListOf<String>()
+
+ override fun info(msg: String) {
+ infoMessages.add(msg)
+ }
+
+ override fun warning(msg: String) {
+ warningMessages.add(msg)
+ }
+
+ override fun severe(msg: String) {
+ severeMessages.add(msg)
+ }
+
+ fun clear() {
+ infoMessages.clear()
+ warningMessages.clear()
+ severeMessages.clear()
+ }
+ }
+
+ /**
+ * Creates a default test configuration with sensible defaults.
+ */
+ fun createTestConfiguration(
+ quickRepliesEnabled: Boolean = true,
+ japaneseConversionEnabled: Boolean = false,
+ channelChatEnabled: Boolean = false,
+ maxChannelsPerServer: Int = 10,
+ maxMembersPerChannel: Int = 50,
+ maxMembershipPerPlayer: Int = 5,
+ debug: Boolean = false,
+ checkForUpdates: Boolean = false,
+ language: Language = Language.EN,
+ ): LunaticChatConfiguration =
+ LunaticChatConfiguration(
+ features =
+ FeaturesConfig(
+ quickReplies = QuickRepliesFeatureConfig(enabled = quickRepliesEnabled),
+ japaneseConversion =
+ JapaneseConversionFeatureConfig(
+ enabled = japaneseConversionEnabled,
+ cacheMaxEntries = 500,
+ cacheSaveIntervalSeconds = 300,
+ cacheFilePath = "test-conversion-cache.json",
+ apiTimeout = 3000,
+ apiRetryAttempts = 2,
+ ),
+ channelChat =
+ ChannelChatFeatureConfig(
+ enabled = channelChatEnabled,
+ maxChannelsPerServer = maxChannelsPerServer,
+ maxMembersPerChannel = maxMembersPerChannel,
+ maxMembershipPerPlayer = maxMembershipPerPlayer,
+ ),
+ ),
+ messageFormat =
+ MessageFormatConfig(
+ directMessageFormat = "§7[§e{sender} §7>> §e{recipient}§7] §f{message}",
+ channelMessageFormat = "§7[§b#{channel}§7] §e{sender}: §f{message}",
+ ),
+ debug = debug,
+ checkForUpdates = checkForUpdates,
+ userSettingsFilePath = "test-player-settings.yaml",
+ language = language,
+ )
+
+ /**
+ * Creates a default player settings for testing.
+ */
+ fun createTestPlayerSettings(
+ uuid: UUID = UUID.randomUUID(),
+ japaneseConversionEnabled: Boolean = true,
+ directMessageNotificationEnabled: Boolean = true,
+ channelMessageNotificationEnabled: Boolean = true,
+ ): PlayerChatSettings =
+ PlayerChatSettings(
+ uuid = uuid,
+ japaneseConversionEnabled = japaneseConversionEnabled,
+ directMessageNotificationEnabled = directMessageNotificationEnabled,
+ channelMessageNotificationEnabled = channelMessageNotificationEnabled,
+ )
+
+ /**
+ * Creates a test channel with default values.
+ */
+ fun createTestChannel(
+ id: String = "test-channel-1",
+ name: String = "Test Channel",
+ description: String? = null,
+ ownerId: UUID = UUID.randomUUID(),
+ isPrivate: Boolean = false,
+ createdAt: Long = System.currentTimeMillis(),
+ bannedPlayers: Set<UUID> = emptySet(),
+ ): Channel =
+ Channel(
+ id = id,
+ name = name,
+ description = description,
+ ownerId = ownerId,
+ createdAt = createdAt,
+ isPrivate = isPrivate,
+ bannedPlayers = bannedPlayers,
+ )
+
+ /**
+ * Creates a test channel member.
+ */
+ fun createTestChannelMember(
+ channelId: String = "test-channel-1",
+ playerId: UUID = UUID.randomUUID(),
+ role: ChannelRole = ChannelRole.MEMBER,
+ joinedAt: Long = System.currentTimeMillis(),
+ ): ChannelMember =
+ ChannelMember(
+ channelId = channelId,
+ playerId = playerId,
+ role = role,
+ joinedAt = joinedAt,
+ )
+
+ /**
+ * Creates a mock Player with the given UUID and name.
+ */
+ fun createMockPlayer(
+ uuid: UUID = UUID.randomUUID(),
+ name: String = "TestPlayer",
+ isOnline: Boolean = true,
+ ): Player {
+ val player = mockk<Player>(relaxed = true)
+ io.mockk.every { player.uniqueId } returns uuid
+ io.mockk.every { player.name } returns name
+ io.mockk.every { player.isOnline } returns isOnline
+ return player
+ }
+
+ /**
+ * Creates a mock JavaPlugin for testing.
+ */
+ fun createMockPlugin(): JavaPlugin = mockk<JavaPlugin>(relaxed = true)
+
+ /**
+ * Creates a test UUID from an integer for deterministic testing.
+ */
+ fun createTestUUID(value: Int): UUID =
+ UUID.fromString(
+ String.format(
+ "%08x-0000-0000-0000-000000000000",
+ value,
+ ),
+ )
+
+ /**
+ * Assertion helper to check if a string contains all given substrings.
+ */
+ fun assertContainsAll(
+ actual: String,
+ vararg expected: String,
+ ) {
+ expected.forEach { substring ->
+ if (!actual.contains(substring)) {
+ throw AssertionError("Expected '$actual' to contain '$substring'")
+ }
+ }
+ }
+
+ /**
+ * Assertion helper to check if a list contains items matching a predicate.
+ */
+ fun <T> assertAny(
+ list: List<T>,
+ predicate: (T) -> Boolean,
+ ) {
+ if (!list.any(predicate)) {
+ throw AssertionError("Expected list to contain at least one matching item")
+ }
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManagerTest.kt
new file mode 100644
index 0000000..d548f60
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManagerTest.kt
@@ -0,0 +1,223 @@
+package dev.m1sk9.lunaticChat.paper.chat
+
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
+import dev.m1sk9.lunaticChat.engine.chat.ChatModeData
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+/**
+ * Tests for ChatModeManager.
+ * Verifies chat mode management, toggling, and persistence.
+ */
+class ChatModeManagerTest {
+ private fun createChatModeManager(
+ initialData: ChatModeData = ChatModeData(),
+ ): Triple<ChatModeManager, ChatModeStorage, TestUtils.TestLogger> {
+ val logger = TestUtils.TestLogger()
+ val storage = mockk<ChatModeStorage>(relaxed = true)
+
+ every { storage.loadFromDisk() } returns initialData
+
+ val manager = ChatModeManager(storage, logger)
+ return Triple(manager, storage, logger)
+ }
+
+ @Test
+ fun `initialize should load data from storage`() {
+ val playerId = UUID.randomUUID()
+ val initialData = ChatModeData(modes = mapOf(playerId to ChatMode.CHANNEL))
+ val (manager, _, logger) = createChatModeManager(initialData)
+
+ manager.initialize()
+
+ assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId))
+ assert(logger.infoMessages.any { it.contains("ChatModeManager initialized with 1 saved modes") })
+ }
+
+ @Test
+ fun `getChatMode should return DEFAULT for unknown player`() {
+ val (manager, _, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ val mode = manager.getChatMode(playerId)
+
+ assertEquals(ChatMode.DEFAULT, mode)
+ assertEquals(ChatMode.GLOBAL, mode) // DEFAULT is GLOBAL
+ }
+
+ @Test
+ fun `getChatMode should return set mode`() {
+ val (manager, _, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ manager.setChatMode(playerId, ChatMode.CHANNEL)
+
+ assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId))
+ }
+
+ @Test
+ fun `setChatMode should save to storage`() {
+ val (manager, storage, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ manager.setChatMode(playerId, ChatMode.CHANNEL)
+
+ verify(exactly = 1) { storage.queueAsyncSave(any()) }
+ }
+
+ @Test
+ fun `toggleChatMode should switch from GLOBAL to CHANNEL`() {
+ val (manager, _, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ // Initial mode is GLOBAL (default)
+ assertEquals(ChatMode.GLOBAL, manager.getChatMode(playerId))
+
+ val newMode = manager.toggleChatMode(playerId)
+
+ assertEquals(ChatMode.CHANNEL, newMode)
+ assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId))
+ }
+
+ @Test
+ fun `toggleChatMode should switch from CHANNEL to GLOBAL`() {
+ val (manager, _, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ manager.setChatMode(playerId, ChatMode.CHANNEL)
+
+ val newMode = manager.toggleChatMode(playerId)
+
+ assertEquals(ChatMode.GLOBAL, newMode)
+ assertEquals(ChatMode.GLOBAL, manager.getChatMode(playerId))
+ }
+
+ @Test
+ fun `toggleChatMode should persist changes`() {
+ val (manager, storage, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ manager.toggleChatMode(playerId)
+
+ verify(atLeast = 1) { storage.queueAsyncSave(any()) }
+ }
+
+ @Test
+ fun `removeChatMode should revert to default`() {
+ val (manager, _, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ manager.setChatMode(playerId, ChatMode.CHANNEL)
+ assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId))
+
+ manager.removeChatMode(playerId)
+
+ assertEquals(ChatMode.GLOBAL, manager.getChatMode(playerId))
+ }
+
+ @Test
+ fun `removeChatMode should save to storage`() {
+ val (manager, storage, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ manager.setChatMode(playerId, ChatMode.CHANNEL)
+ manager.removeChatMode(playerId)
+
+ verify(atLeast = 2) { storage.queueAsyncSave(any()) } // Once for set, once for remove
+ }
+
+ @Test
+ fun `saveToDisk should synchronously save`() {
+ val (manager, storage, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+ manager.setChatMode(playerId, ChatMode.CHANNEL)
+ manager.saveToDisk()
+
+ verify { storage.saveToDisk(any()) }
+ }
+
+ @Test
+ fun `shutdown should save and shutdown storage`() {
+ val (manager, storage, _) = createChatModeManager()
+ manager.initialize()
+
+ manager.shutdown()
+
+ verify { storage.saveToDisk(any()) }
+ verify { storage.shutdown() }
+ }
+
+ @Test
+ fun `manager should handle multiple players independently`() {
+ val (manager, _, _) = createChatModeManager()
+ manager.initialize()
+
+ val player1 = UUID.randomUUID()
+ val player2 = UUID.randomUUID()
+
+ manager.setChatMode(player1, ChatMode.CHANNEL)
+ manager.setChatMode(player2, ChatMode.GLOBAL)
+
+ assertEquals(ChatMode.CHANNEL, manager.getChatMode(player1))
+ assertEquals(ChatMode.GLOBAL, manager.getChatMode(player2))
+ }
+
+ @Test
+ fun `manager should support rapid mode changes`() {
+ val (manager, storage, _) = createChatModeManager()
+ manager.initialize()
+
+ val playerId = UUID.randomUUID()
+
+ // Toggle multiple times
+ manager.toggleChatMode(playerId) // GLOBAL -> CHANNEL
+ manager.toggleChatMode(playerId) // CHANNEL -> GLOBAL
+ manager.toggleChatMode(playerId) // GLOBAL -> CHANNEL
+
+ assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId))
+ verify(atLeast = 3) { storage.queueAsyncSave(any()) }
+ }
+
+ @Test
+ fun `manager should handle empty initial data`() {
+ val (manager, _, logger) = createChatModeManager(ChatModeData())
+ manager.initialize()
+
+ assert(logger.infoMessages.any { it.contains("ChatModeManager initialized with 0 saved modes") })
+ }
+
+ @Test
+ fun `manager should restore modes after initialization`() {
+ val player1 = TestUtils.createTestUUID(1)
+ val player2 = TestUtils.createTestUUID(2)
+ val initialData =
+ ChatModeData(
+ modes =
+ mapOf(
+ player1 to ChatMode.CHANNEL,
+ player2 to ChatMode.GLOBAL,
+ ),
+ )
+
+ val (manager, _, _) = createChatModeManager(initialData)
+ manager.initialize()
+
+ assertEquals(ChatMode.CHANNEL, manager.getChatMode(player1))
+ assertEquals(ChatMode.GLOBAL, manager.getChatMode(player2))
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
new file mode 100644
index 0000000..b07636b
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
@@ -0,0 +1,134 @@
+package dev.m1sk9.lunaticChat.paper.chat.handler
+
+import dev.m1sk9.lunaticChat.paper.TestUtils
+import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
+import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+/**
+ * Tests for DirectMessageHandler.
+ * Validates message handling with dependency injection and conversion features.
+ */
+class DirectMessageHandlerTest {
+ private fun createHandler(
+ configuration: dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration? = null,
+ settingsManager: PlayerSettingsManager? = null,
+ romanjiConverter: RomanjiConverter? = null,
+ ): DirectMessageHandler {
+ val config = configuration ?: TestUtils.createTestConfiguration()
+ return DirectMessageHandler(config, settingsManager, romanjiConverter)
+ }
+
+ @Test
+ fun `sendDirectMessage should return true on success`() {
+ val handler = createHandler()
+ val sender = TestUtils.createMockPlayer()
+ val recipient = TestUtils.createMockPlayer()
+
+ val result = handler.sendDirectMessage(sender, recipient, "Test message")
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `sendDirectMessage should use custom message format from configuration`() {
+ val customConfig =
+ TestUtils.createTestConfiguration().copy(
+ messageFormat =
+ TestUtils
+ .createTestConfiguration()
+ .messageFormat
+ .copy(
+ directMessageFormat = "[DM] {sender} -> {recipient}: {message}",
+ ),
+ )
+
+ val handler = createHandler(configuration = customConfig)
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+ val recipient = TestUtils.createMockPlayer(name = "Bob")
+
+ val result = handler.sendDirectMessage(sender, recipient, "Test")
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `sendDirectMessage without conversion should send original message`() {
+ val settingsManager = mockk<PlayerSettingsManager>()
+ val senderSettings =
+ TestUtils.createTestPlayerSettings(
+ japaneseConversionEnabled = false,
+ )
+
+ every { settingsManager.getSettings(any()) } returns senderSettings
+
+ val handler = createHandler(settingsManager = settingsManager)
+ val sender = TestUtils.createMockPlayer()
+ val recipient = TestUtils.createMockPlayer()
+
+ val result = handler.sendDirectMessage(sender, recipient, "konnichiwa")
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `sendDirectMessage with conversion should handle conversion timeout gracefully`() {
+ val settingsManager = mockk<PlayerSettingsManager>()
+ val senderSettings =
+ TestUtils.createTestPlayerSettings(
+ japaneseConversionEnabled = true,
+ )
+ val romanjiConverter = mockk<RomanjiConverter>()
+
+ every { settingsManager.getSettings(any()) } returns senderSettings
+ // Simulate a slow conversion that would timeout
+ coEvery { romanjiConverter.convert(any()) } coAnswers {
+ kotlinx.coroutines.delay(2000) // Exceeds 1s timeout
+ "こんにちは"
+ }
+
+ val handler = createHandler(settingsManager = settingsManager, romanjiConverter = romanjiConverter)
+ val sender = TestUtils.createMockPlayer()
+ val recipient = TestUtils.createMockPlayer()
+
+ // Should not throw exception and should complete quickly (within timeout)
+ val result = handler.sendDirectMessage(sender, recipient, "konnichiwa")
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `handler can be created with injected configuration`() {
+ val config = TestUtils.createTestConfiguration(debug = true)
+ val handler = DirectMessageHandler(config, null, null)
+
+ // Handler should accept configuration via constructor (DI pattern)
+ // This validates Issue #1 refactoring - ConfigManager DI
+ val sender = TestUtils.createMockPlayer()
+ val recipient = TestUtils.createMockPlayer()
+ val result = handler.sendDirectMessage(sender, recipient, "Test")
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `handler can be created with all dependencies`() {
+ val config = TestUtils.createTestConfiguration()
+ val settingsManager = mockk<PlayerSettingsManager>(relaxed = true)
+ val romanjiConverter = mockk<RomanjiConverter>(relaxed = true)
+
+ every { settingsManager.getSettings(any()) } returns TestUtils.createTestPlayerSettings()
+
+ val handler = DirectMessageHandler(config, settingsManager, romanjiConverter)
+
+ val sender = TestUtils.createMockPlayer()
+ val recipient = TestUtils.createMockPlayer()
+ val result = handler.sendDirectMessage(sender, recipient, "Test")
+
+ assertTrue(result)
+ }
+}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt
new file mode 100644
index 0000000..3f936f6
--- /dev/null
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt
@@ -0,0 +1,288 @@
+package dev.m1sk9.lunaticChat.paper.config
+
+import dev.m1sk9.lunaticChat.paper.i18n.Language
+import io.mockk.every
+import io.mockk.mockk
+import org.bukkit.configuration.file.FileConfiguration
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+/**
+ * Tests for ConfigManager to ensure proper configuration loading and parsing.
+ */
+class ConfigManagerTest {
+ private fun createMockConfig(values: Map<String, Any> = emptyMap()): FileConfiguration {
+ val config = mockk<FileConfiguration>(relaxed = true)
+
+ // Set up default values
+ every { config.getBoolean(any(), any()) } answers {
+ val key = firstArg<String>()
+ val default = secondArg<Boolean>()
+ (values[key] as? Boolean) ?: default
+ }
+
+ every { config.getInt(any(), any()) } answers {
+ val key = firstArg<String>()
+ val default = secondArg<Int>()
+ (values[key] as? Int) ?: default
+ }
+
+ every { config.getLong(any(), any()) } answers {
+ val key = firstArg<String>()
+ val default = secondArg<Long>()
+ (values[key] as? Long) ?: default
+ }
+
+ every { config.getString(any(), any()) } answers {
+ val key = firstArg<String>()
+ val default = secondArg<String>()
+ (values[key] as? String) ?: default
+ }
+
+ every { config.getString(any()) } answers {
+ val key = firstArg<String>()
+ values[key] as? String
+ }
+
+ return config
+ }
+
+ @Test
+ fun `loadConfiguration should load all default values`() {
+ val configManager = ConfigManager()
+ val mockConfig = createMockConfig()
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertNotNull(configuration)
+ assertTrue(configuration.features.quickReplies.enabled)
+ assertFalse(configuration.features.japaneseConversion.enabled)
+ assertFalse(configuration.features.channelChat.enabled)
+ assertFalse(configuration.debug)
+ assertFalse(configuration.checkForUpdates)
+ assertEquals(Language.EN, configuration.language)
+ }
+
+ @Test
+ fun `loadConfiguration should load quick replies configuration`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "features.quickReplies.enabled" to false,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertFalse(configuration.features.quickReplies.enabled)
+ }
+
+ @Test
+ fun `loadConfiguration should load Japanese conversion configuration`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "features.japaneseConversion.enabled" to true,
+ "features.japaneseConversion.cache.maxEntries" to 1000,
+ "features.japaneseConversion.cache.saveIntervalSeconds" to 600,
+ "features.japaneseConversion.cache.filePath" to "custom_cache.json",
+ "features.japaneseConversion.api.timeout" to 5000L,
+ "features.japaneseConversion.api.retryAttempts" to 3,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertTrue(configuration.features.japaneseConversion.enabled)
+ assertEquals(1000, configuration.features.japaneseConversion.cacheMaxEntries)
+ assertEquals(600, configuration.features.japaneseConversion.cacheSaveIntervalSeconds)
+ assertEquals("custom_cache.json", configuration.features.japaneseConversion.cacheFilePath)
+ assertEquals(5000L, configuration.features.japaneseConversion.apiTimeout)
+ assertEquals(3, configuration.features.japaneseConversion.apiRetryAttempts)
+ }
+
+ @Test
+ fun `loadConfiguration should load channel chat configuration`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "features.channelChat.enabled" to true,
+ "features.channelChat.maxChannelsPerServer" to 20,
+ "features.channelChat.maxMembersPerChannel" to 100,
+ "features.channelChat.maxMembershipPerPlayer" to 10,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertTrue(configuration.features.channelChat.enabled)
+ assertEquals(20, configuration.features.channelChat.maxChannelsPerServer)
+ assertEquals(100, configuration.features.channelChat.maxMembersPerChannel)
+ assertEquals(10, configuration.features.channelChat.maxMembershipPerPlayer)
+ }
+
+ @Test
+ fun `loadConfiguration should load message format configuration`() {
+ val configManager = ConfigManager()
+ val customDMFormat = "DM: {sender} -> {recipient}: {message}"
+ val customChannelFormat = "[{channel}] {sender}: {message}"
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "messageFormat.directMessageFormat" to customDMFormat,
+ "messageFormat.channelMessageFormat" to customChannelFormat,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertEquals(customDMFormat, configuration.messageFormat.directMessageFormat)
+ assertEquals(customChannelFormat, configuration.messageFormat.channelMessageFormat)
+ }
+
+ @Test
+ fun `loadConfiguration should load debug and update check settings`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "debug" to true,
+ "checkForUpdates" to true,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertTrue(configuration.debug)
+ assertTrue(configuration.checkForUpdates)
+ }
+
+ @Test
+ fun `loadConfiguration should load custom settings file path`() {
+ val configManager = ConfigManager()
+ val customPath = "custom-player-settings.yaml"
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "userSettingsFilePath" to customPath,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertEquals(customPath, configuration.userSettingsFilePath)
+ }
+
+ @Test
+ fun `loadConfiguration should load Japanese language`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "language" to "ja",
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertEquals(Language.JA, configuration.language)
+ }
+
+ @Test
+ fun `loadConfiguration should handle unknown language code`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "language" to "fr", // French not supported
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ // Should fall back to English
+ assertEquals(Language.EN, configuration.language)
+ }
+
+ @Test
+ fun `loadConfiguration should use default values when keys are missing`() {
+ val configManager = ConfigManager()
+ val mockConfig = createMockConfig(emptyMap())
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ // All features should have their defaults
+ assertTrue(configuration.features.quickReplies.enabled)
+ assertEquals(500, configuration.features.japaneseConversion.cacheMaxEntries)
+ assertEquals(300, configuration.features.japaneseConversion.cacheSaveIntervalSeconds)
+ assertEquals("conversion_cache.json", configuration.features.japaneseConversion.cacheFilePath)
+ assertEquals(3000L, configuration.features.japaneseConversion.apiTimeout)
+ assertEquals(2, configuration.features.japaneseConversion.apiRetryAttempts)
+ assertEquals(0, configuration.features.channelChat.maxChannelsPerServer)
+ assertEquals(0, configuration.features.channelChat.maxMembersPerChannel)
+ assertEquals(0, configuration.features.channelChat.maxMembershipPerPlayer)
+ }
+
+ @Test
+ fun `loadConfiguration can be called multiple times`() {
+ val configManager = ConfigManager()
+ val mockConfig1 =
+ createMockConfig(
+ mapOf("debug" to true),
+ )
+ val mockConfig2 =
+ createMockConfig(
+ mapOf("debug" to false),
+ )
+
+ val config1 = configManager.loadConfiguration(mockConfig1)
+ val config2 = configManager.loadConfiguration(mockConfig2)
+
+ assertTrue(config1.debug)
+ assertFalse(config2.debug)
+ }
+
+ @Test
+ fun `loadConfiguration should handle all features enabled`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "features.quickReplies.enabled" to true,
+ "features.japaneseConversion.enabled" to true,
+ "features.channelChat.enabled" to true,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertTrue(configuration.features.quickReplies.enabled)
+ assertTrue(configuration.features.japaneseConversion.enabled)
+ assertTrue(configuration.features.channelChat.enabled)
+ }
+
+ @Test
+ fun `loadConfiguration should handle all features disabled`() {
+ val configManager = ConfigManager()
+ val mockConfig =
+ createMockConfig(
+ mapOf(
+ "features.quickReplies.enabled" to false,
+ "features.japaneseConversion.enabled" to false,
+ "features.channelChat.enabled" to false,
+ ),
+ )
+
+ val configuration = configManager.loadConfiguration(mockConfig)
+
+ assertFalse(configuration.features.quickReplies.enabled)
+ assertFalse(configuration.features.japaneseConversion.enabled)
+ assertFalse(configuration.features.channelChat.enabled)
+ }
+}