From 09b63eb5bd122e63177e97f25f6e67011451930a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 09:42:50 +0000 Subject: [PATCH] Add tests for LocalizationManager broadcastMessage and PAPI branch coverage - New LocalizationManagerBroadcastTest with 10 tests covering both broadcastMessage(Route) and broadcastMessage(Route, Map) overloads, including loaded/unloaded player paths, console, and mixed scenarios - Add 4 PAPI branch coverage tests to LocalizationManagerTest for getLocalizedMessage and getLocalizedMessages with hook present/absent - Add multi-locale message format test to NoLocalizationContainsMessageExceptionTest - LocalizationManager line coverage: 100%, branch coverage: 95.7% Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01Fb5ztpqybNTKEmSLYH6R6F --- ...alizationContainsMessageExceptionTest.java | 13 + .../LocalizationManagerBroadcastTest.java | 302 ++++++++++++++++++ .../localization/LocalizationManagerTest.java | 74 +++++ 3 files changed, 389 insertions(+) create mode 100644 src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerBroadcastTest.java diff --git a/src/test/java/com/diamonddagger590/mccore/exception/localization/NoLocalizationContainsMessageExceptionTest.java b/src/test/java/com/diamonddagger590/mccore/exception/localization/NoLocalizationContainsMessageExceptionTest.java index 62ad367..5197e63 100644 --- a/src/test/java/com/diamonddagger590/mccore/exception/localization/NoLocalizationContainsMessageExceptionTest.java +++ b/src/test/java/com/diamonddagger590/mccore/exception/localization/NoLocalizationContainsMessageExceptionTest.java @@ -63,6 +63,19 @@ void getCheckedLocales_returnsEmptySet_whenConstructedWithEmptyLocales() { assertTrue(ex.getCheckedLocales().isEmpty()); } + @Test + @DisplayName("Given multiple locales, when calling getMessage, then all locale names are joined with commas") + void getMessage_joinsMultipleLocaleNames_whenMultipleLocalesProvided() { + Route route = Route.from("messages", "multi"); + Set locales = Set.of(Locale.ENGLISH, Locale.FRENCH); + NoLocalizationContainsMessageException ex = new NoLocalizationContainsMessageException(route, locales); + + String message = ex.getMessage(); + assertNotNull(message); + assertTrue(message.contains(Locale.ENGLISH.getDisplayName())); + assertTrue(message.contains(Locale.FRENCH.getDisplayName())); + } + @Test @DisplayName("Given a NoLocalizationContainsMessageException, when checking type, then it is a RuntimeException") void noLocalizationContainsMessageException_isRuntimeException_always() { diff --git a/src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerBroadcastTest.java b/src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerBroadcastTest.java new file mode 100644 index 0000000..44ebc20 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerBroadcastTest.java @@ -0,0 +1,302 @@ +package com.diamonddagger590.mccore.localization; + +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.configuration.ReloadableContent; +import com.diamonddagger590.mccore.configuration.ReloadableContentManager; +import com.diamonddagger590.mccore.player.CorePlayer; +import com.diamonddagger590.mccore.player.PlayerManager; +import com.diamonddagger590.mccore.registry.RegistryAccess; +import com.diamonddagger590.mccore.registry.RegistryKey; +import com.diamonddagger590.mccore.registry.manager.ManagerRegistry; +import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import com.diamonddagger590.mccore.testing.TestCorePlugin; +import com.diamonddagger590.mccore.util.LinkedNode; +import dev.dejvokep.boostedyaml.YamlDocument; +import dev.dejvokep.boostedyaml.route.Route; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LocalizationManagerBroadcastTest { + + private static class TestCorePlayer extends CorePlayer { + private final Player bukkitPlayer; + + TestCorePlayer(@NotNull UUID uuid, @NotNull CorePlugin plugin, Player bukkitPlayer) { + super(uuid, plugin); + this.bukkitPlayer = bukkitPlayer; + } + + @Override + public boolean useMutex() { + return false; + } + + @Override + @NotNull + public Optional getAsBukkitPlayer() { + return Optional.ofNullable(bukkitPlayer); + } + } + + private static class TestLocalizationManager extends LocalizationManager { + + private final LinkedNode defaultChain; + + TestLocalizationManager(@NotNull CorePlugin plugin, @NotNull LinkedNode chain) { + super(plugin); + this.defaultChain = chain; + } + + @Override + @NotNull + protected ReloadableContent> generateLocaleChain() { + if (defaultChain == null) { + LinkedNode sentinel = new LinkedNode<>(Locale.ENGLISH); + LinkedNode englishNode = new LinkedNode<>(Locale.ENGLISH, sentinel); + return new ReloadableContent<>(mock(YamlDocument.class), Route.from("locale"), (doc, route) -> englishNode, englishNode); + } + return new ReloadableContent<>( + mock(YamlDocument.class), + Route.from("locale"), + (doc, route) -> defaultChain, + defaultChain + ); + } + } + + private ServerMock server; + private TestCorePlugin plugin; + private TestLocalizationManager localizationManager; + private YamlDocument englishDoc; + private Route testRoute; + private PlayerManager playerManager; + + @BeforeEach + void setUp() { + server = MockBukkit.mock(); + plugin = MockBukkit.load(TestCorePlugin.class); + RegistryResetExtension.setupRegistry(); + + ManagerRegistry managerRegistry = RegistryAccess.registryAccess().registry(RegistryKey.MANAGER); + managerRegistry.register(new ReloadableContentManager(plugin)); + + LinkedNode sentinel = new LinkedNode<>(Locale.ENGLISH); + LinkedNode chain = new LinkedNode<>(Locale.ENGLISH, sentinel); + localizationManager = new TestLocalizationManager(plugin, chain); + + englishDoc = mock(YamlDocument.class); + testRoute = Route.from("messages", "broadcast_test"); + + Localization localization = mock(Localization.class); + when(localization.getLocale()).thenReturn(Locale.ENGLISH); + when(localization.getConfigurationFile()).thenReturn(englishDoc); + localizationManager.registerLanguageFile(localization); + + playerManager = new PlayerManager<>(plugin); + managerRegistry.register(playerManager); + } + + @AfterEach + void tearDown() { + RegistryResetExtension.resetRegistry(); + MockBukkit.unmock(); + } + + private String componentToPlain(@NotNull Component component) { + return PlainTextComponentSerializer.plainText().serialize(component); + } + + @Nested + @DisplayName("broadcastMessage(Route)") + class BroadcastMessageRoute { + + @Test + @DisplayName("Loaded player receives message via their locale chain") + void loadedPlayerReceivesLocalizedMessage() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Server announcement"); + + PlayerMock bukkitPlayer = server.addPlayer(); + TestCorePlayer corePlayer = new TestCorePlayer(bukkitPlayer.getUniqueId(), plugin, bukkitPlayer); + playerManager.addPlayer(corePlayer); + + localizationManager.broadcastMessage(testRoute); + + String msg = bukkitPlayer.nextMessage(); + assertNotNull(msg, "Player should have received a message"); + assertEquals("Server announcement", msg); + } + + @Test + @DisplayName("Unloaded player receives message via default locale") + void unloadedPlayerReceivesDefaultMessage() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Default broadcast"); + + PlayerMock bukkitPlayer = server.addPlayer(); + + localizationManager.broadcastMessage(testRoute); + + String msg = bukkitPlayer.nextMessage(); + assertNotNull(msg, "Unloaded player should still receive a message"); + assertEquals("Default broadcast", msg); + } + + @Test + @DisplayName("Console receives message via default locale") + void consoleReceivesDefaultMessage() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Console broadcast"); + + localizationManager.broadcastMessage(testRoute); + + String msg = server.getConsoleSender().nextMessage(); + assertNotNull(msg, "Console should have received a message"); + assertEquals("Console broadcast", msg); + } + + @Test + @DisplayName("Mix of loaded and unloaded players all receive messages") + void mixedPlayersBothReceiveMessages() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Mixed broadcast"); + + PlayerMock loadedBukkitPlayer = server.addPlayer(); + TestCorePlayer loadedCorePlayer = new TestCorePlayer(loadedBukkitPlayer.getUniqueId(), plugin, loadedBukkitPlayer); + playerManager.addPlayer(loadedCorePlayer); + + PlayerMock unloadedBukkitPlayer = server.addPlayer(); + + localizationManager.broadcastMessage(testRoute); + + String loadedMsg = loadedBukkitPlayer.nextMessage(); + String unloadedMsg = unloadedBukkitPlayer.nextMessage(); + assertNotNull(loadedMsg, "Loaded player should receive message"); + assertNotNull(unloadedMsg, "Unloaded player should receive message"); + assertEquals("Mixed broadcast", loadedMsg); + assertEquals("Mixed broadcast", unloadedMsg); + } + + @Test + @DisplayName("No online players sends only to console") + void noOnlinePlayersSendsToConsole() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Console only"); + + localizationManager.broadcastMessage(testRoute); + + String msg = server.getConsoleSender().nextMessage(); + assertNotNull(msg, "Console should receive message even with no online players"); + assertEquals("Console only", msg); + } + } + + @Nested + @DisplayName("broadcastMessage(Route, Map)") + class BroadcastMessageRouteWithPlaceholders { + + @Test + @DisplayName("Loaded player receives message with placeholders substituted") + void loadedPlayerReceivesPlaceholderMessage() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Hello , you have items"); + + PlayerMock bukkitPlayer = server.addPlayer(); + TestCorePlayer corePlayer = new TestCorePlayer(bukkitPlayer.getUniqueId(), plugin, bukkitPlayer); + playerManager.addPlayer(corePlayer); + + localizationManager.broadcastMessage(testRoute, Map.of("name", "Steve", "count", "5")); + + Component msg = bukkitPlayer.nextComponentMessage(); + assertNotNull(msg, "Player should have received a message"); + assertEquals("Hello Steve, you have 5 items", componentToPlain(msg)); + } + + @Test + @DisplayName("Unloaded player receives message with placeholders substituted via default locale") + void unloadedPlayerReceivesPlaceholderMessage() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Welcome "); + + PlayerMock bukkitPlayer = server.addPlayer(); + + localizationManager.broadcastMessage(testRoute, Map.of("player", "Alex")); + + Component msg = bukkitPlayer.nextComponentMessage(); + assertNotNull(msg, "Unloaded player should receive message"); + assertEquals("Welcome Alex", componentToPlain(msg)); + } + + @Test + @DisplayName("Console receives message with placeholders substituted") + void consoleReceivesPlaceholderMessage() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Event: "); + + localizationManager.broadcastMessage(testRoute, Map.of("event", "Raid")); + + Component msg = server.getConsoleSender().nextComponentMessage(); + assertNotNull(msg, "Console should receive message"); + assertEquals("Event: Raid", componentToPlain(msg)); + } + + @Test + @DisplayName("Empty placeholders map sends message without substitution") + void emptyPlaceholdersPassesThrough() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("No placeholders here"); + + PlayerMock bukkitPlayer = server.addPlayer(); + TestCorePlayer corePlayer = new TestCorePlayer(bukkitPlayer.getUniqueId(), plugin, bukkitPlayer); + playerManager.addPlayer(corePlayer); + + localizationManager.broadcastMessage(testRoute, Map.of()); + + Component msg = bukkitPlayer.nextComponentMessage(); + assertNotNull(msg, "Player should receive message"); + assertEquals("No placeholders here", componentToPlain(msg)); + } + + @Test + @DisplayName("Mix of loaded and unloaded players both receive placeholder messages") + void mixedPlayersReceivePlaceholderMessages() { + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Score: "); + + PlayerMock loadedBukkitPlayer = server.addPlayer(); + TestCorePlayer corePlayer = new TestCorePlayer(loadedBukkitPlayer.getUniqueId(), plugin, loadedBukkitPlayer); + playerManager.addPlayer(corePlayer); + + PlayerMock unloadedBukkitPlayer = server.addPlayer(); + + localizationManager.broadcastMessage(testRoute, Map.of("score", "100")); + + Component loadedMsg = loadedBukkitPlayer.nextComponentMessage(); + Component unloadedMsg = unloadedBukkitPlayer.nextComponentMessage(); + assertNotNull(loadedMsg, "Loaded player should receive message"); + assertNotNull(unloadedMsg, "Unloaded player should receive message"); + assertEquals("Score: 100", componentToPlain(loadedMsg)); + assertEquals("Score: 100", componentToPlain(unloadedMsg)); + } + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerTest.java b/src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerTest.java index 37fc2ec..3addeac 100644 --- a/src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerTest.java +++ b/src/test/java/com/diamonddagger590/mccore/localization/LocalizationManagerTest.java @@ -3,6 +3,7 @@ import com.diamonddagger590.mccore.CorePlugin; import com.diamonddagger590.mccore.configuration.ReloadableContent; import com.diamonddagger590.mccore.exception.localization.NoLocalizationContainsMessageException; +import com.diamonddagger590.mccore.external.papi.CorePapiHook; import com.diamonddagger590.mccore.player.CorePlayer; import com.diamonddagger590.mccore.player.PlayerManager; import com.diamonddagger590.mccore.registry.RegistryAccess; @@ -280,6 +281,79 @@ void getLocalizedMessage_player_throwsWhenMissing() { () -> localizationManager.getLocalizedMessage(player, testRoute)); } + // --- getLocalizedMessage(CorePlayer, Route) PAPI branch --- + + @Test + @DisplayName("Given PAPI hook registered and player online, when getLocalizedMessage(player, route), then PAPI translates message") + void getLocalizedMessage_player_withPapiHook_translatesViaPapi() { + registerEnglishDoc(); + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Hello %player_name%"); + + TestCorePlayer player = createPlayerWithLocale(Locale.ENGLISH); + + CorePapiHook papiHook = mock(CorePapiHook.class); + when(papiHook.translateMessage(player.getAsBukkitPlayer().get(), "Hello %player_name%")).thenReturn("Hello Steve"); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(papiHook); + + assertEquals("Hello Steve", localizationManager.getLocalizedMessage(player, testRoute)); + } + + @Test + @DisplayName("Given PAPI hook registered but player offline, when getLocalizedMessage(player, route), then skips PAPI") + void getLocalizedMessage_player_withPapiHookButOffline_skipsPapi() { + registerEnglishDoc(); + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getString(testRoute)).thenReturn("Hello %player_name%"); + + TestCorePlayer player = createPlayerWithoutBukkit(); + + CorePapiHook papiHook = mock(CorePapiHook.class); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(papiHook); + + assertEquals("Hello %player_name%", localizationManager.getLocalizedMessage(player, testRoute)); + } + + // --- getLocalizedMessages(CorePlayer, Route) PAPI branch --- + + @Test + @DisplayName("Given PAPI hook and player online, when getLocalizedMessages(player, route), then PAPI translates each line") + void getLocalizedMessages_player_withPapiHook_translatesEachLine() { + registerEnglishDoc(); + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getStringList(testRoute)).thenReturn(List.of("Line %player_name%", "Health %player_health%")); + + TestCorePlayer player = createPlayerWithLocale(Locale.ENGLISH); + Player bukkitPlayer = player.getAsBukkitPlayer().get(); + + CorePapiHook papiHook = mock(CorePapiHook.class); + when(papiHook.translateMessage(bukkitPlayer, "Line %player_name%")).thenReturn("Line Steve"); + when(papiHook.translateMessage(bukkitPlayer, "Health %player_health%")).thenReturn("Health 20"); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(papiHook); + + List result = localizationManager.getLocalizedMessages(player, testRoute); + assertEquals(2, result.size()); + assertEquals("Line Steve", result.get(0)); + assertEquals("Health 20", result.get(1)); + } + + @Test + @DisplayName("Given PAPI hook but player offline, when getLocalizedMessages(player, route), then skips PAPI translation") + void getLocalizedMessages_player_withPapiHookButOffline_skipsPapi() { + registerEnglishDoc(); + when(englishDoc.contains(testRoute)).thenReturn(true); + when(englishDoc.getStringList(testRoute)).thenReturn(List.of("Line %player_name%")); + + TestCorePlayer player = createPlayerWithoutBukkit(); + + CorePapiHook papiHook = mock(CorePapiHook.class); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(papiHook); + + List result = localizationManager.getLocalizedMessages(player, testRoute); + assertEquals(1, result.size()); + assertEquals("Line %player_name%", result.get(0)); + } + // --- getLocalizedMessage with placeholders --- @Test