diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-03 15:16:39 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 01:16:03 +0900 |
| commit | 576c057bb98dab27c2b97931fc5635603fe1bf57 (patch) | |
| tree | 115faef961f9fb8cf801370e77f33921f5b436b4 /platform-paper/src/test | |
| parent | daf9060561656f468a0ae366e5695f5eb74f48f7 (diff) | |
| download | LunaticChat-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.kt | 7 | ||||
| -rw-r--r-- | platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt | 343 |
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")) } } |
