summaryrefslogtreecommitdiff
path: root/platform-paper/src/test
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-03 15:16:39 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 01:16:03 +0900
commit576c057bb98dab27c2b97931fc5635603fe1bf57 (patch)
tree115faef961f9fb8cf801370e77f33921f5b436b4 /platform-paper/src/test
parentdaf9060561656f468a0ae366e5695f5eb74f48f7 (diff)
downloadLunaticChat-576c057bb98dab27c2b97931fc5635603fe1bf57.tar.gz
LunaticChat-576c057bb98dab27c2b97931fc5635603fe1bf57.tar.bz2
LunaticChat-576c057bb98dab27c2b97931fc5635603fe1bf57.zip
refactor: read config.yml into the config classes directly
Every setting's default was written three times - in config.yml, in the ConfigManager getter call, and on the data class - and copying the file key by key is what made that necessary. They had already drifted: checkForUpdates defaulted to false in ConfigManager while both config.yml and the data class said true. Worse, features.channelChat.messageLogging was documented in config.yml with three settings and never parsed at all. ConfigManager did not build it, so ChannelMessageLoggingConfig() always won and an operator editing retentionDays or maxFileSizeMB changed nothing. Those settings now take effect - the documented behaviour, but a real change for anyone whose file disagrees with the defaults. KAML deserializes the file straight into the tree, the same way player settings and channel data are already read, so a default now lives only on the data class. Two consequences worth stating: - japaneseConversion.cache and .api are nested classes now, because the data has to match the file rather than the file being flattened by hand on the way in. The YAML is unchanged. - api.retryAttempts is gone from config.yml. It was parsed and stored, but never reached GoogleIMEClient or RomanjiConverter, so it documented a knob that did nothing. Unknown keys are ignored and a malformed file falls back to defaults with a log line, so neither an old config nor a typo stops the server booting. The tests parse real YAML instead of a mocked FileConfiguration, which lets them cover what the mock could not: a partial file, a retired key, a malformed document, and - the one that would have caught the drift above - that the bundled config.yml equals the declared defaults. Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'platform-paper/src/test')
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt7
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt343
2 files changed, 121 insertions, 229 deletions
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
index 2d2a7c7..4bca21f 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt
@@ -6,6 +6,7 @@ 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.ConversionCacheConfig
import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig
import dev.m1sk9.lunaticChat.paper.config.key.JapaneseConversionFeatureConfig
import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig
@@ -86,11 +87,7 @@ object TestUtils {
japaneseConversion =
JapaneseConversionFeatureConfig(
enabled = japaneseConversionEnabled,
- cacheMaxEntries = 500,
- cacheSaveIntervalSeconds = 300,
- cacheFilePath = "test-conversion-cache.json",
- apiTimeout = 3000,
- apiRetryAttempts = 2,
+ cache = ConversionCacheConfig(filePath = "test-conversion-cache.json"),
),
channelChat =
ChannelChatFeatureConfig(
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
index 3f936f6..e76eeb9 100644
--- 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
@@ -1,288 +1,183 @@
package dev.m1sk9.lunaticChat.paper.config
+import dev.m1sk9.lunaticChat.paper.TestUtils
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.
+ * Tests for ConfigManager.
+ *
+ * These parse real YAML rather than a mocked Bukkit FileConfiguration, so they exercise the same
+ * path the plugin uses at startup - including what happens to a file that is incomplete, stale or
+ * malformed.
*/
class ConfigManagerTest {
- private fun createMockConfig(values: Map<String, Any> = emptyMap()): FileConfiguration {
- val config = mockk<FileConfiguration>(relaxed = true)
+ private fun load(yaml: String) = ConfigManager(TestUtils.TestLogger()).loadConfiguration(yaml)
- // 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
- }
+ /** The file shipped in resources, which is what a fresh install actually reads. */
+ private val bundledConfig: String =
+ checkNotNull(javaClass.classLoader.getResourceAsStream("config.yml")) {
+ "config.yml missing from resources"
+ }.bufferedReader().use { it.readText() }
@Test
- fun `loadConfiguration should load all default values`() {
- val configManager = ConfigManager()
- val mockConfig = createMockConfig()
-
- val configuration = configManager.loadConfiguration(mockConfig)
+ fun `the bundled config parses`() {
+ val config = load(bundledConfig)
- 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)
+ assertFalse(config.debug)
+ assertEquals("player-settings.yaml", config.userSettingsFilePath)
+ assertEquals(Language.EN, config.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)
+ fun `the bundled config agrees with the declared defaults`() {
+ // The data class is meant to be the single source of every default. If config.yml ships a
+ // different value for a key, one of the two is lying to the operator.
+ assertEquals(LunaticChatConfiguration(), load(bundledConfig))
}
@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)
+ fun `an empty document falls back to defaults`() {
+ assertEquals(LunaticChatConfiguration(), load("{}"))
}
@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)
+ fun `a config missing a section keeps that section's defaults`() {
+ val config = load("debug: true")
- assertTrue(configuration.features.channelChat.enabled)
- assertEquals(20, configuration.features.channelChat.maxChannelsPerServer)
- assertEquals(100, configuration.features.channelChat.maxMembersPerChannel)
- assertEquals(10, configuration.features.channelChat.maxMembershipPerPlayer)
+ assertTrue(config.debug)
+ assertEquals(LunaticChatConfiguration().features, config.features)
+ assertEquals(LunaticChatConfiguration().messageFormat, config.messageFormat)
}
@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,
- ),
+ fun `values present in the file win over the defaults`() {
+ val config =
+ load(
+ """
+ debug: true
+ checkForUpdates: false
+ userSettingsFilePath: "custom.yaml"
+ language: "ja"
+ """.trimIndent(),
)
- val configuration = configManager.loadConfiguration(mockConfig)
-
- assertEquals(customDMFormat, configuration.messageFormat.directMessageFormat)
- assertEquals(customChannelFormat, configuration.messageFormat.channelMessageFormat)
+ assertTrue(config.debug)
+ assertFalse(config.checkForUpdates)
+ assertEquals("custom.yaml", config.userSettingsFilePath)
+ assertEquals(Language.JA, config.language)
}
@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)
+ fun `an unknown language code falls back to English`() {
+ assertEquals(Language.EN, load("""language: "kl"""").language)
}
@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)
+ fun `a language code is matched regardless of case`() {
+ assertEquals(Language.JA, load("""language: "JA"""").language)
}
@Test
- fun `loadConfiguration should load Japanese language`() {
- val configManager = ConfigManager()
- val mockConfig =
- createMockConfig(
- mapOf(
- "language" to "ja",
- ),
+ fun `nested japanese conversion settings are read`() {
+ val config =
+ load(
+ """
+ features:
+ japaneseConversion:
+ enabled: true
+ cache:
+ maxEntries: 1000
+ saveIntervalSeconds: 600
+ filePath: "custom_cache.json"
+ api:
+ timeout: 5000
+ """.trimIndent(),
)
- val configuration = configManager.loadConfiguration(mockConfig)
-
- assertEquals(Language.JA, configuration.language)
+ val japanese = config.features.japaneseConversion
+ assertTrue(japanese.enabled)
+ assertEquals(1000, japanese.cache.maxEntries)
+ assertEquals(600, japanese.cache.saveIntervalSeconds)
+ assertEquals("custom_cache.json", japanese.cache.filePath)
+ assertEquals(5000L, japanese.api.timeout)
}
@Test
- fun `loadConfiguration should handle unknown language code`() {
- val configManager = ConfigManager()
- val mockConfig =
- createMockConfig(
- mapOf(
- "language" to "fr", // French not supported
- ),
+ fun `channel message logging is read from the file`() {
+ // This block was documented in config.yml but never parsed, so editing it did nothing.
+ val config =
+ load(
+ """
+ features:
+ channelChat:
+ enabled: true
+ messageLogging:
+ enabled: false
+ retentionDays: 7
+ maxFileSizeMB: 20
+ """.trimIndent(),
)
- val configuration = configManager.loadConfiguration(mockConfig)
-
- // Should fall back to English
- assertEquals(Language.EN, configuration.language)
+ val logging = config.features.channelChat.messageLogging
+ assertFalse(logging.enabled)
+ assertEquals(7, logging.retentionDays)
+ assertEquals(20, logging.maxFileSizeMB)
}
@Test
- fun `loadConfiguration should use default values when keys are missing`() {
- val configManager = ConfigManager()
- val mockConfig = createMockConfig(emptyMap())
-
- val configuration = configManager.loadConfiguration(mockConfig)
+ fun `velocity integration settings are read from the file`() {
+ val config =
+ load(
+ """
+ features:
+ velocityIntegration:
+ enabled: true
+ crossServerGlobalChat: true
+ crossServerDirectMessage: true
+ serverName: "survival"
+ messageDeduplicationCacheSize: 250
+ """.trimIndent(),
+ )
- // 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)
+ val velocity = config.features.velocityIntegration
+ assertTrue(velocity.enabled)
+ assertTrue(velocity.crossServerGlobalChat)
+ assertTrue(velocity.crossServerDirectMessage)
+ assertEquals("survival", velocity.serverName)
+ assertEquals(250, velocity.messageDeduplicationCacheSize)
}
@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),
+ fun `message formats are read from the file`() {
+ val config =
+ load(
+ """
+ messageFormat:
+ directMessageFormat: "DM {sender} {message}"
+ channelMessageFormat: "CH {channel} {message}"
+ crossServerGlobalChatFormat: "GL {server} {message}"
+ """.trimIndent(),
)
- val config1 = configManager.loadConfiguration(mockConfig1)
- val config2 = configManager.loadConfiguration(mockConfig2)
-
- assertTrue(config1.debug)
- assertFalse(config2.debug)
+ assertEquals("DM {sender} {message}", config.messageFormat.directMessageFormat)
+ assertEquals("CH {channel} {message}", config.messageFormat.channelMessageFormat)
+ assertEquals("GL {server} {message}", config.messageFormat.crossServerGlobalChatFormat)
}
@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,
- ),
- )
+ fun `a key this build no longer knows is ignored`() {
+ // An operator upgrading from a build that had extra keys must still be able to start.
+ val config = load("debug: true\nsomeRetiredOption: 42")
- val configuration = configManager.loadConfiguration(mockConfig)
-
- assertTrue(configuration.features.quickReplies.enabled)
- assertTrue(configuration.features.japaneseConversion.enabled)
- assertTrue(configuration.features.channelChat.enabled)
+ assertTrue(config.debug)
}
@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)
+ fun `a malformed file falls back to defaults instead of failing startup`() {
+ assertEquals(LunaticChatConfiguration(), load("features: [this is not a map"))
}
}