Repository: wiiiiam278/huskhomes
Issue: Local online user resolution on teleport from username targets by closest match prevents a global exact match

Attempting username resolution against a user that has the partial username of another (i.e requesting a teleport to `APP`, while another user of the name `APPLE` is present on the network) will cause unintended outcomes.  Referencing the example, this can result in the use `APPLE` being teleported to INSTEAD of the user `APP`.

Changed files:
common/src/main/java/net/william278/huskhomes/teleport/TeleportRequest.java, common/src/main/java/net/william278/huskhomes/teleport/Username.java, common/src/main/java/net/william278/huskhomes/manager/RequestsManager.java, common/src/main/java/net/william278/huskhomes/command/RtpCommand.java, common/src/main/java/net/william278/huskhomes/teleport/TimedTeleport.java, common/src/main/java/net/william278/huskhomes/HuskHomes.java, common/src/main/java/net/william278/huskhomes/teleport/Target.java, common/src/main/java/net/william278/huskhomes/teleport/Teleportable.java, common/src/main/java/net/william278/huskhomes/teleport/TeleportBuilder.java, bukkit/src/test/java/net/william278/huskhomes/BukkitPluginTests.java, common/src/main/java/net/william278/huskhomes/teleport/Teleport.java

diff --git a/bukkit/src/test/java/net/william278/huskhomes/BukkitPluginTests.java b/bukkit/src/test/java/net/william278/huskhomes/BukkitPluginTests.java
index b849e815..d255ecd3 100644
--- a/bukkit/src/test/java/net/william278/huskhomes/BukkitPluginTests.java
+++ b/bukkit/src/test/java/net/william278/huskhomes/BukkitPluginTests.java
@@ -202,7 +202,6 @@ public class BukkitPluginTests {
             Assertions.assertFalse(plugin.getValidator().isValidName(name));
         }
 
-        // test descriptions
         @DisplayName("Test Validator Accepts Valid Descriptions")
         @ParameterizedTest(name = "Valid Description: \\"{0}\\"")
         @ValueSource(strings = {
@@ -366,7 +365,6 @@ public class BukkitPluginTests {
 
     }
 
-    // home tests, like warps but with a user parameter (owner) as well as name and position and a test for changing home privacy
     @Nested
     @DisplayName("Home Tests")
     @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
diff --git a/common/src/main/java/net/william278/huskhomes/HuskHomes.java b/common/src/main/java/net/william278/huskhomes/HuskHomes.java
index 3ad6f382..b8c6fb40 100644
--- a/common/src/main/java/net/william278/huskhomes/HuskHomes.java
+++ b/common/src/main/java/net/william278/huskhomes/HuskHomes.java
@@ -78,6 +78,31 @@ public interface HuskHomes extends TaskRunner, EventDispatcher, SafetyResolver {
     @NotNull
     List<OnlineUser> getOnlineUsers();
 
+    /**
+     * Finds a local {@link OnlineUser} by their name. Auto-completes partially typed names for the closest match
+     *
+     * @param playerName the name of the player to find
+     * @return an {@link Optional} containing the {@link OnlineUser} if found, or an empty {@link Optional} if not found
+     */
+    default Optional<OnlineUser> getOnlineUser(@NotNull String playerName) {
+        return getOnlineUserExact(playerName)
+                .or(() -> getOnlineUsers().stream()
+                        .filter(user -> user.getUsername().toLowerCase().startsWith(playerName.toLowerCase()))
+                        .findFirst());
+    }
+
+    /**
+     * Finds a local {@link OnlineUser} by their name.
+     *
+     * @param playerName the name of the player to find
+     * @return an {@link Optional} containing the {@link OnlineUser} if found, or an empty {@link Optional} if not found
+     */
+    default Optional<OnlineUser> getOnlineUserExact(@NotNull String playerName) {
+        return getOnlineUsers().stream()
+                .filter(user -> user.getUsername().equalsIgnoreCase(playerName))
+                .findFirst();
+    }
+
     @NotNull
     Set<SavedUser> getSavedUsers();
 
@@ -111,22 +136,6 @@ public interface HuskHomes extends TaskRunner, EventDispatcher, SafetyResolver {
         log(Level.INFO, "Successfully initialized " + name);
     }
 
-    /**
-     * Finds a local {@link OnlineUser} by their name. Auto-completes partially typed names for the closest match
-     *
-     * @param playerName the name of the player to find
-     * @return an {@link Optional} containing the {@link OnlineUser} if found, or an empty {@link Optional} if not found
-     */
-    @NotNull
-    default Optional<OnlineUser> findOnlinePlayer(@NotNull String playerName) {
-        return getOnlineUsers().stream()
-                .filter(user -> user.getUsername().equalsIgnoreCase(playerName))
-                .findFirst()
-                .or(() -> getOnlineUsers().stream()
-                        .filter(user -> user.getUsername().toLowerCase().startsWith(playerName.toLowerCase()))
-                        .findFirst());
-    }
-
     /**
      * The plugin {@link Settings} loaded from file
      *
diff --git a/common/src/main/java/net/william278/huskhomes/command/RtpCommand.java b/common/src/main/java/net/william278/huskhomes/command/RtpCommand.java
index cc3b17b1..0fd59cc6 100644
--- a/common/src/main/java/net/william278/huskhomes/command/RtpCommand.java
+++ b/common/src/main/java/net/william278/huskhomes/command/RtpCommand.java
@@ -50,7 +50,7 @@ public class RtpCommand extends Command implements UserListTabProvider {
 
     @Override
     public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
-        final Optional<OnlineUser> optionalTeleporter = args.length >= 1 ? plugin.findOnlinePlayer(args[0])
+        final Optional<OnlineUser> optionalTeleporter = args.length >= 1 ? plugin.getOnlineUser(args[0])
                 : executor instanceof OnlineUser ? Optional.of((OnlineUser) executor) : Optional.empty();
         if (optionalTeleporter.isEmpty()) {
             if (args.length == 0) {
diff --git a/common/src/main/java/net/william278/huskhomes/manager/RequestsManager.java b/common/src/main/java/net/william278/huskhomes/manager/RequestsManager.java
index 427efb84..5bb3d77c 100644
--- a/common/src/main/java/net/william278/huskhomes/manager/RequestsManager.java
+++ b/common/src/main/java/net/william278/huskhomes/manager/RequestsManager.java
@@ -145,7 +145,7 @@ public class RequestsManager {
                                     @NotNull TeleportRequest.Type type) throws IllegalArgumentException {
         final long expiry = Instant.now().getEpochSecond() + plugin.getSettings().getTeleportRequestExpiryTime();
         final TeleportRequest request = new TeleportRequest(requester, type, expiry);
-        final Optional<OnlineUser> localTarget = plugin.findOnlinePlayer(targetUser);
+        final Optional<OnlineUser> localTarget = plugin.getOnlineUser(targetUser);
         if (localTarget.isPresent()) {
             if (localTarget.get().equals(requester)) {
                 throw new IllegalArgumentException("Cannot send a teleport request to yourself");
@@ -282,7 +282,7 @@ public class RequestsManager {
                     .ifPresent(recipient::sendMessage);
 
             // Find the requester and inform them of the response
-            final Optional<OnlineUser> localRequester = plugin.findOnlinePlayer(request.getRequesterName());
+            final Optional<OnlineUser> localRequester = plugin.getOnlineUser(request.getRequesterName());
             if (localRequester.isPresent()) {
                 handleLocalRequestResponse(localRequester.get(), request);
             } else if (plugin.getSettings().doCrossServer()) {
diff --git a/common/src/main/java/net/william278/huskhomes/teleport/Target.java b/common/src/main/java/net/william278/huskhomes/teleport/Target.java
index a5eda39b..9cd7a031 100644
--- a/common/src/main/java/net/william278/huskhomes/teleport/Target.java
+++ b/common/src/main/java/net/william278/huskhomes/teleport/Target.java
@@ -21,8 +21,17 @@ package net.william278.huskhomes.teleport;
 
 import org.jetbrains.annotations.NotNull;
 
+/**
+ * Represents a target; a player name or a location that will be teleported <i>to</i>.
+ */
 public interface Target {
 
+    /**
+     * Create a {@link Target} from a player name
+     *
+     * @param target the player name
+     * @return the target
+     */
     @NotNull
     static Target username(@NotNull String target) {
         return new Username(target);
diff --git a/common/src/main/java/net/william278/huskhomes/teleport/Teleport.java b/common/src/main/java/net/william278/huskhomes/teleport/Teleport.java
index 527cd104..351fb54d 100644
--- a/common/src/main/java/net/william278/huskhomes/teleport/Teleport.java
+++ b/common/src/main/java/net/william278/huskhomes/teleport/Teleport.java
@@ -35,6 +35,11 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 
+/**
+ * Represents the process of a {@link Teleportable} being teleported to a {@link Target}.
+ *
+ * @see Teleport#builder(HuskHomes)
+ */
 public class Teleport {
 
     protected final HuskHomes plugin;
diff --git a/common/src/main/java/net/william278/huskhomes/teleport/TeleportBuilder.java b/common/src/main/java/net/william278/huskhomes/teleport/TeleportBuilder.java
index fe1a9b6f..c6291fe8 100644
--- a/common/src/main/java/net/william278/huskhomes/teleport/TeleportBuilder.java
+++ b/common/src/main/java/net/william278/huskhomes/teleport/TeleportBuilder.java
@@ -26,6 +26,9 @@ import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
 
+/**
+ * A builder for {@link Teleport} and {@link TimedTeleport} objects.
+ */
 public class TeleportBuilder {
     private final HuskHomes plugin;
     private OnlineUser executor;
diff --git a/common/src/main/java/net/william278/huskhomes/teleport/TeleportRequest.java b/common/src/main/java/net/william278/huskhomes/teleport/TeleportRequest.java
index b379cad5..547d53da 100644
--- a/common/src/main/java/net/william278/huskhomes/teleport/TeleportRequest.java
+++ b/common/src/main/java/net/william278/huskhomes/teleport/TeleportRequest.java
@@ -90,6 +90,7 @@ public class TeleportRequest {
     /**
      * The user making the request
      */
+    @NotNull
     public String getRequesterName() {
         return requesterName;
     }
diff --git a/common/src/main/java/net/william278/huskhomes/teleport/Teleportable.java b/common/src/main/java/net/william278/huskhomes/teleport/Teleportable.java
index 759b38bb..e71f87f0 100644
--- a/common/src/main/java/net/william278/huskhomes/teleport/Teleportable.java
+++ b/common/src/main/java/net/william278/huskhomes/teleport/Teleportable.java
@@ -20,9 +20,22 @@
 package net.william278.huskhomes.teleport;
 
 import org.jetbrains.annotations.NotNull;
+import net.william278.huskhomes.user.OnlineUser;
 
+/**
+ * Represents a teleporter; the person performing the teleport.
+ * <p>
+ * Can be represented as an {@link OnlineUser} locally or as a {@link Username username} reference
+ * that needs to be resolved first.
+ */
 public interface Teleportable {
 
+    /**
+     * Create a {@link Teleportable} from a player name
+     *
+     * @param teleporter the player name
+     * @return the teleportable
+     */
     @NotNull
     static Teleportable username(@NotNull String teleporter) {
         return new Username(teleporter);
diff --git a/common/src/main/java/net/william278/huskhomes/teleport/TimedTeleport.java b/common/src/main/java/net/william278/huskhomes/teleport/TimedTeleport.java
index cedf12b9..287501cd 100644
--- a/common/src/main/java/net/william278/huskhomes/teleport/TimedTeleport.java
+++ b/common/src/main/java/net/william278/huskhomes/teleport/TimedTeleport.java
@@ -30,6 +30,11 @@ import org.jetbrains.annotations.NotNull;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
+/**
+ * Represents a {@link Teleport} that has an associated warmup time; the teleport will not be performed until the
+ * warmup time has elapsed, during which the user must not move or take damage.
+ * @see Teleport#builder(HuskHomes)
+ */
 public class TimedTeleport extends Teleport {
 
     public static final String BYPASS_PERMISSION = "huskhomes.bypass_teleport_warmup";
diff --git a/common/src/main/java/net/william278/huskhomes/teleport/Username.java b/common/src/main/java/net/william278/huskhomes/teleport/Username.java
index 646e61e5..af9ce00b 100644
--- a/common/src/main/java/net/william278/huskhomes/teleport/Username.java
+++ b/common/src/main/java/net/william278/huskhomes/teleport/Username.java
@@ -25,11 +25,30 @@ import org.jetbrains.annotations.NotNull;
 
 import java.util.Optional;
 
+/**
+ * Represents the username of a player who may or may not be online, either locally as an {@link OnlineUser},
+ * or on a different server.
+ *
+ * @param name The name of the player
+ */
 public record Username(@NotNull String name) implements Teleportable, Target {
 
+    /**
+     * Search for a local {@link OnlineUser} by their name.
+     *
+     * @param plugin The instance of {@link HuskHomes}
+     * @return An {@link Optional} containing the {@link OnlineUser} if found
+     * @throws TeleportationException If the user is not found
+     * @implNote If a user by the name provided is on the {@link HuskHomes#getPlayerList() player list}, then this
+     * method will search for the user by exact name.
+     * <p>
+     * Otherwise, the lookup will first attempt to find the user by exact name, and if that fails, it will search for
+     * the closest name match.
+     */
     @NotNull
-    public Optional<OnlineUser> findLocally(@NotNull HuskHomes plugin) throws TeleportationException {
-        return plugin.findOnlinePlayer(name);
+    public Optional<OnlineUser> findLocally(@NotNull HuskHomes plugin) {
+        return plugin.getPlayerList(true).stream().anyMatch(listedName -> listedName.equalsIgnoreCase(name))
+                ? plugin.getOnlineUserExact(name) : plugin.getOnlineUser(name);
     }
 
 }