feat(stealth): conceal identity for prepared sessions
CI / build (push) Successful in 5m4s
Release / release (push) Successful in 6m13s

This commit is contained in:
dmg
2026-08-15 00:16:09 -04:00
parent 90851a0d87
commit 6071bfbdce
16 changed files with 510 additions and 18 deletions
@@ -0,0 +1,61 @@
package games.dmg.spigotstealth;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team;
import org.junit.jupiter.api.Test;
class BukkitIdentityPresentationTest {
@Test
void concealRemovesTabEntryAndHidesNameTagWithoutHidingEntity() {
UUID targetId = UUID.randomUUID();
Player target = player(targetId, "Alex");
Player observer = player(UUID.randomUUID(), "Morgan");
Scoreboard scoreboard = mock(Scoreboard.class);
Team team = mock(Team.class);
when(scoreboard.getTeam(BukkitIdentityPresentation.teamName(targetId))).thenReturn(null);
when(scoreboard.registerNewTeam(BukkitIdentityPresentation.teamName(targetId))).thenReturn(team);
TabListController tabLists = mock(TabListController.class);
BukkitIdentityPresentation presentation = new BukkitIdentityPresentation(
() -> List.of(target, observer), scoreboard, tabLists);
presentation.conceal(target);
verify(team).setOption(Team.Option.NAME_TAG_VISIBILITY, Team.OptionStatus.NEVER);
verify(team).addEntry("Alex");
verify(tabLists).remove(observer, targetId);
verify(observer, never()).hidePlayer(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
}
@Test
void newObserverHasEveryConcealedPlayerRemovedFromTab() {
UUID targetId = UUID.randomUUID();
Player target = player(targetId, "Alex");
Player observer = player(UUID.randomUUID(), "Morgan");
Scoreboard scoreboard = mock(Scoreboard.class);
Team team = mock(Team.class);
when(scoreboard.getTeam(BukkitIdentityPresentation.teamName(targetId))).thenReturn(team);
TabListController tabLists = mock(TabListController.class);
BukkitIdentityPresentation presentation = new BukkitIdentityPresentation(
() -> List.of(target, observer), scoreboard, tabLists);
presentation.conceal(target);
presentation.refreshForObserver(observer);
verify(tabLists, org.mockito.Mockito.atLeastOnce()).remove(observer, targetId);
}
private static Player player(UUID playerId, String name) {
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn(name);
return player;
}
}
@@ -26,6 +26,7 @@ class PluginMetadataTest {
assertEquals("games.dmg.spigotstealth.SpigotStealthPlugin", metadata.get("main"));
assertNotNull(metadata.get("commands"));
assertNotNull(metadata.get("permissions"));
assertEquals(java.util.List.of("ProtocolLib"), metadata.get("depend"));
} catch (java.io.IOException exception) {
throw new AssertionError(exception);
}
@@ -0,0 +1,77 @@
package games.dmg.spigotstealth;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerJoinEvent;
import org.junit.jupiter.api.Test;
class StealthSessionListenerTest {
@Test
void preparedLoginSuppressesAnnouncementAndConcealsIdentityForSession() {
UUID playerId = UUID.randomUUID();
try (StealthStateManager manager = manager()) {
manager.update(state -> state.withPlayer(unlocked(playerId).withSession(true, false))).join();
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
IdentityPresentation presentation = mock(IdentityPresentation.class);
StealthSessionListener listener = new StealthSessionListener(
new StealthSessionService(manager, progression), presentation, "Stealth active");
Player player = player(playerId);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(player);
listener.onJoin(event);
verify(event).setJoinMessage(null);
verify(presentation).conceal(player);
verify(player).sendMessage("Stealth active");
}
}
@Test
void ordinaryLoginRetainsAnnouncementAndIdentity() {
UUID playerId = UUID.randomUUID();
try (StealthStateManager manager = manager()) {
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
IdentityPresentation presentation = mock(IdentityPresentation.class);
StealthSessionListener listener = new StealthSessionListener(
new StealthSessionService(manager, progression), presentation, "Stealth active");
Player player = player(playerId);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(player);
listener.onJoin(event);
verify(event, never()).setJoinMessage(null);
verify(presentation).reveal(player);
verify(player, never()).sendMessage("Stealth active");
}
}
private static Player player(UUID playerId) {
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Alex");
return player;
}
private static PlayerStealthState unlocked(UUID playerId) {
return new PlayerStealthState(playerId, "Alex", Duration.ofHours(8).toMillis(), true, false, false, null, Map.of());
}
private static StealthStateManager manager() {
StealthStateRepository repository = new StealthStateRepository() {
@Override public PersistentStealthState load() { return new PersistentStealthState(Map.of(), Map.of()); }
@Override public void save(PersistentStealthState state) { }
};
return new StealthStateManager(repository, new PersistentStealthState(Map.of(), Map.of()));
}
}
@@ -0,0 +1,71 @@
package games.dmg.spigotstealth;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class StealthSessionServiceTest {
@Test
void unlockedQualifyingDisconnectPreparesAndConsumesOneConcealedLogin() {
UUID playerId = UUID.randomUUID();
try (StealthStateManager manager = manager()) {
manager.update(state -> state.withPlayer(unlocked(playerId))).join();
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
progression.begin(playerId, "Alex", Instant.EPOCH).join();
StealthSessionService sessions = new StealthSessionService(manager, progression);
sessions.disconnect(playerId).join();
assertTrue(manager.snapshot().player(playerId).preparedLogin());
assertTrue(sessions.login(playerId, "Alex").concealed());
assertFalse(manager.snapshot().player(playerId).preparedLogin());
assertTrue(manager.snapshot().player(playerId).concealed());
sessions.disconnect(playerId).join();
assertFalse(sessions.login(playerId, "Alex").concealed());
}
}
@Test
void lockedPlayerCannotPrepareConcealedLogin() {
UUID playerId = UUID.randomUUID();
try (StealthStateManager manager = manager()) {
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
progression.begin(playerId, "Alex", Instant.EPOCH).join();
StealthSessionService sessions = new StealthSessionService(manager, progression);
sessions.disconnect(playerId).join();
assertFalse(sessions.login(playerId, "Alex").concealed());
}
}
@Test
void ordinaryDisconnectClearsEarlierPreparation() {
UUID playerId = UUID.randomUUID();
try (StealthStateManager manager = manager()) {
manager.update(state -> state.withPlayer(unlocked(playerId).withSession(true, false))).join();
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
StealthSessionService sessions = new StealthSessionService(manager, progression);
sessions.disconnect(playerId).join();
assertFalse(manager.snapshot().player(playerId).preparedLogin());
}
}
private static PlayerStealthState unlocked(UUID playerId) {
return new PlayerStealthState(playerId, "Alex", Duration.ofHours(8).toMillis(), true, false, false, null, Map.of());
}
private static StealthStateManager manager() {
StealthStateRepository repository = new StealthStateRepository() {
@Override public PersistentStealthState load() { return new PersistentStealthState(Map.of(), Map.of()); }
@Override public void save(PersistentStealthState state) { }
};
return new StealthStateManager(repository, new PersistentStealthState(Map.of(), Map.of()));
}
}