diff --git a/src/main/java/net/tfminecraft/infestations/command/CommandManager.java b/src/main/java/net/tfminecraft/infestations/command/CommandManager.java index 040765b..fbf8f40 100644 --- a/src/main/java/net/tfminecraft/infestations/command/CommandManager.java +++ b/src/main/java/net/tfminecraft/infestations/command/CommandManager.java @@ -19,6 +19,7 @@ import net.tfminecraft.infestations.infestation.Severity; import net.tfminecraft.infestations.loader.GroupLoader; import net.tfminecraft.infestations.utils.Provinces; +import net.tfminecraft.simplefactions.SimpleFactions; public final class CommandManager implements CommandExecutor, TabCompleter { @@ -195,10 +196,32 @@ public List onTabComplete(CommandSender sender, Command command, String return List.of(); } if (args.length == 2 && (args[0].equalsIgnoreCase("set") || args[0].equalsIgnoreCase("clear"))) { - return prefix(List.of("here"), args[1]); + List options = new ArrayList<>(); + var manager = Infestations.plugin.getInfestationManager(); + if (args[0].equalsIgnoreCase("clear")) { + manager.all().forEach(infestation -> options.add(String.valueOf(infestation.getProvinceId()))); + } else { + var sf = SimpleFactions.getInstance(); + if (sf != null && sf.getProvinceManager() != null) { + sf.getProvinceManager().getProvinces().stream() + .filter(province -> Provinces.validLand(province.getId()) && manager.get(province.getId()) == null) + .forEach(province -> options.add(String.valueOf(province.getId()))); + } + } + Integer province = completionProvince(sender, "here"); + if (province != null && Provinces.validLand(province) + && (args[0].equalsIgnoreCase("clear") + ? manager.get(province) != null : manager.get(province) == null)) { + options.add("here"); + } + return prefix(options, args[1]); } if (args.length == 3 && args[0].equalsIgnoreCase("set")) { - return prefix(new ArrayList<>(GroupLoader.groupIds()), args[2]); + Integer province = completionProvince(sender, args[1]); + if (province == null || !Provinces.validLand(province) + || Infestations.plugin.getInfestationManager().get(province) != null) return List.of(); + return prefix(GroupLoader.groupIds().stream() + .filter(group -> GroupLoader.allowsTerrain(group, province)).toList(), args[2]); } if (args.length == 4 && args[0].equalsIgnoreCase("set")) { return prefix(Arrays.stream(Severity.values()).map(Severity::id).collect(Collectors.toList()), args[3]); @@ -206,6 +229,17 @@ public List onTabComplete(CommandSender sender, Command command, String return List.of(); } + private static Integer completionProvince(CommandSender sender, String token) { + if ("here".equalsIgnoreCase(token)) { + return sender instanceof Player player ? Provinces.at(player) : null; + } + try { + return Integer.valueOf(token); + } catch (NumberFormatException ex) { + return null; + } + } + private static List prefix(List options, String typed) { String t = typed.toLowerCase(Locale.ROOT); List out = new ArrayList<>(); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b708e2e..1244d82 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -10,7 +10,7 @@ commands: description: Infestation admin commands usage: /infestation aliases: [infestations] - permission: infestations.admin + permission: infestations.admin;infestations.admin.reload lure: description: Leave a lure you have joined usage: /lure leave diff --git a/src/test/java/net/tfminecraft/infestations/command/CommandManagerTest.java b/src/test/java/net/tfminecraft/infestations/command/CommandManagerTest.java index 7563412..9773804 100644 --- a/src/test/java/net/tfminecraft/infestations/command/CommandManagerTest.java +++ b/src/test/java/net/tfminecraft/infestations/command/CommandManagerTest.java @@ -129,9 +129,11 @@ private void run(CommandSender sender, String... args) { admin(); when(console.hasPermission("infestations.admin.reload")).thenReturn(true); assertEquals(List.of("reload", "set", "clear", "list"), complete(console, command, "")); assertEquals(List.of("set"), complete(console, command, "S")); - assertEquals(List.of("here"), complete(console, command, "set", "H")); - assertEquals(List.of("here"), complete(console, command, "clear", "")); + assertEquals(List.of(), complete(console, command, "set", "H")); + assertEquals(List.of(), complete(console, command, "clear", "")); groups.when(GroupLoader::groupIds).thenReturn(Set.of("rats")); + provinces.when(() -> Provinces.validLand(4)).thenReturn(true); + groups.when(() -> GroupLoader.allowsTerrain("rats", 4)).thenReturn(true); assertEquals(List.of("rats"), complete(console, command, "set", "4", "r")); assertEquals(Severity.values().length, complete(console, command, "set", "4", "rats", "").size()); assertEquals(List.of(), complete(console, command)); @@ -141,6 +143,131 @@ private void run(CommandSender sender, String... args) { assertEquals(List.of("leave"), complete(console, lure, "L")); assertEquals(List.of(), complete(console, lure, "leave", "")); } + @Test void provinceAndGroupCandidatesRespectLiveStateAndSender() { + admin(); + when(player.hasPermission("infestations.admin")).thenReturn(true); + var sf = mock(net.tfminecraft.simplefactions.SimpleFactions.class, RETURNS_DEEP_STUBS); + var land = mock(net.tfminecraft.simplefactions.map.provinces.Province.class); + var occupied = mock(net.tfminecraft.simplefactions.map.provinces.Province.class); + var water = mock(net.tfminecraft.simplefactions.map.provinces.Province.class); + when(land.getId()).thenReturn(42); when(occupied.getId()).thenReturn(43); when(water.getId()).thenReturn(44); + when(sf.getProvinceManager().getProvinces()).thenReturn(List.of(land, occupied, water)); + provinces.when(() -> Provinces.validLand(42)).thenReturn(true); + provinces.when(() -> Provinces.validLand(43)).thenReturn(true); + provinces.when(() -> Provinces.at(player)).thenReturn(42); + when(manager.get(43)).thenReturn(new Infestation(43, "rats", Severity.MILD)); + when(manager.all()).thenReturn(List.of(new Infestation(43, "rats", Severity.MILD))); + try (var factions = mockStatic(net.tfminecraft.simplefactions.SimpleFactions.class)) { + factions.when(net.tfminecraft.simplefactions.SimpleFactions::getInstance).thenReturn(sf); + assertEquals(List.of("42"), complete(console, command, "set", "4")); + assertEquals(List.of("43"), complete(console, command, "clear", "4")); + assertEquals(List.of("42", "here"), complete(player, command, "set", "")); + assertEquals(List.of(), complete(player, command, "clear", "H")); + provinces.when(() -> Provinces.at(player)).thenReturn(43); + assertEquals(List.of("here"), complete(player, command, "clear", "H")); + provinces.when(() -> Provinces.at(player)).thenReturn(42); + } + groups.when(GroupLoader::groupIds).thenReturn(Set.of("rats", "swamp")); + groups.when(() -> GroupLoader.allowsTerrain("rats", 42)).thenReturn(true); + assertEquals(List.of("rats"), complete(player, command, "set", "HeRe", "R")); + assertEquals(List.of("rats"), complete(console, command, "set", "42", "")); + for (String invalid : List.of("here", "nonsense", "44", "43")) { + assertTrue(complete(console, command, "set", invalid, "").isEmpty()); + } + verify(console, never()).sendMessage(anyString()); + verify(player, never()).sendMessage(anyString()); + } + + @Test void setHereCompletionRequiresUninfestedLandAndStaysQuiet() { + admin(); + when(player.hasPermission("infestations.admin")).thenReturn(true); + provinces.when(() -> Provinces.at(player)).thenReturn(42); + provinces.when(() -> Provinces.validLand(42)).thenReturn(true); + provinces.when(() -> Provinces.validLand(43)).thenReturn(true); + when(manager.get(43)).thenReturn(new Infestation(43, "rats", Severity.MILD)); + try (var factions = mockStatic(net.tfminecraft.simplefactions.SimpleFactions.class)) { + assertEquals(List.of("here"), complete(player, command, "set", "")); + assertEquals(List.of("here"), complete(player, command, "SeT", "H")); + assertEquals(List.of("here"), complete(player, command, "set", "HeRe")); + assertEquals(List.of(), complete(player, command, "set", "hereX")); + assertEquals(List.of(), complete(console, command, "set", "")); + assertEquals(List.of(), complete(console, command, "set", "H")); + for (int ineligible : List.of(43, 44, 999, Provinces.UNKNOWN)) { + provinces.when(() -> Provinces.at(player)).thenReturn(ineligible); + assertEquals(List.of(), complete(player, command, "set", ""), "province " + ineligible); + assertEquals(List.of(), complete(player, command, "SET", "H"), "province " + ineligible); + } + } + verify(console, never()).sendMessage(anyString()); + verify(player, never()).sendMessage(anyString()); + messages.verifyNoInteractions(); + verify(manager, never()).set(anyInt(), anyString(), any()); + verify(manager, never()).clear(anyInt(), anyBoolean()); + } + + @Test void clearHereCompletionRequiresAnActiveInfestationOnLandAndStaysQuiet() { + admin(); + when(player.hasPermission("infestations.admin")).thenReturn(true); + provinces.when(() -> Provinces.at(player)).thenReturn(42); + provinces.when(() -> Provinces.validLand(42)).thenReturn(true); + provinces.when(() -> Provinces.validLand(43)).thenReturn(true); + Infestation infestation = new Infestation(43, "rats", Severity.MILD); + when(manager.get(43)).thenReturn(infestation); + when(manager.get(44)).thenReturn(new Infestation(44, "rats", Severity.MILD)); + when(manager.all()).thenReturn(List.of(infestation)); + assertEquals(List.of("43"), complete(player, command, "clear", "")); + assertEquals(List.of(), complete(player, command, "clear", "H")); + provinces.when(() -> Provinces.at(player)).thenReturn(43); + assertEquals(List.of("43", "here"), complete(player, command, "clear", "")); + assertEquals(List.of("here"), complete(player, command, "ClEaR", "H")); + assertEquals(List.of("here"), complete(player, command, "clear", "HeRe")); + assertEquals(List.of(), complete(player, command, "clear", "hereX")); + assertEquals(List.of("43"), complete(player, command, "clear", "4")); + assertEquals(List.of("43"), complete(console, command, "clear", "")); + assertEquals(List.of(), complete(console, command, "clear", "H")); + for (int ineligible : List.of(44, 999, Provinces.UNKNOWN)) { + provinces.when(() -> Provinces.at(player)).thenReturn(ineligible); + assertEquals(List.of("43"), complete(player, command, "clear", ""), "province " + ineligible); + assertEquals(List.of(), complete(player, command, "CLEAR", "H"), "province " + ineligible); + } + verify(console, never()).sendMessage(anyString()); + verify(player, never()).sendMessage(anyString()); + messages.verifyNoInteractions(); + verify(manager, never()).set(anyInt(), anyString(), any()); + verify(manager, never()).clear(anyInt(), anyBoolean()); + } + + @Test void registeredCommandAllowsReloadOnlySenderAndStillDeniesAdminActions() throws Exception { + when(console.hasPermission("infestations.admin.reload")).thenReturn(true); + when(Infestations.plugin.isEnabled()).thenReturn(true); + var description = new org.bukkit.plugin.PluginDescriptionFile( + getClass().getResourceAsStream("/plugin.yml")); + assertEquals("infestations.admin;infestations.admin.reload", + description.getCommands().get("infestation").get("permission")); + var constructor = PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class); + constructor.setAccessible(true); + PluginCommand registered = constructor.newInstance("infestation", Infestations.plugin); + registered.setPermission((String) description.getCommands().get("infestation").get("permission")); + registered.setExecutor(commands); + registered.setTabCompleter(commands); + assertFalse(registered.testPermissionSilent(player)); + when(player.hasPermission("infestations.admin")).thenReturn(true); + assertTrue(registered.testPermissionSilent(player)); + when(player.hasPermission("infestations.admin")).thenReturn(false); + when(player.hasPermission("infestations.admin.reload")).thenReturn(true); + assertTrue(registered.testPermissionSilent(player)); + assertTrue(registered.testPermissionSilent(console)); + assertEquals(List.of("reload"), registered.tabComplete(console, "infestations", new String[]{""})); + assertEquals(List.of("reload"), registered.tabComplete(console, "infestations", new String[]{"RE"})); + assertEquals(List.of(), registered.tabComplete(console, "infestations", new String[]{"set", "H"})); + assertEquals(List.of(), registered.tabComplete(console, "infestations", new String[]{"clear", "H"})); + assertTrue(registered.execute(console, "infestation", new String[]{"reload"})); + verify(Infestations.plugin).reloadAll(); + assertTrue(registered.execute(console, "infestation", new String[]{"set", "42", "rats", "mild"})); + verify(console).sendMessage("no-permission"); + verify(manager, never()).set(anyInt(), anyString(), any()); + } + private List complete(CommandSender sender, Command cmd, String... args) { return commands.onTabComplete(sender, cmd, cmd.getName(), args); }