feat(admin): manage player stealth state
Release / release (push) Successful in 2m20s
CI / build (push) Successful in 1m6s

This commit is contained in:
dmg
2026-08-15 00:20:29 -04:00
parent 6071bfbdce
commit 2bcba5071f
9 changed files with 546 additions and 15 deletions
@@ -0,0 +1,33 @@
package games.dmg.spigotstealth;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class KnownPlayerResolverTest {
@Test
void resolvesKnownOfflinePlayerByExactNameOrUuid() {
UUID playerId = UUID.randomUUID();
PlayerStealthState player = PlayerStealthState.empty(playerId).withLastKnownName("Alex");
PersistentStealthState state = new PersistentStealthState(Map.of(playerId, player), Map.of());
KnownPlayerResolver resolver = new KnownPlayerResolver(() -> state, List::of);
assertEquals(playerId, resolver.resolve("alex").player().playerId());
assertEquals(playerId, resolver.resolve(playerId.toString()).player().playerId());
}
@Test
void reportsAmbiguousAndUnknownNamesWithoutCreatingRecords() {
UUID first = UUID.randomUUID();
UUID second = UUID.randomUUID();
PersistentStealthState state = new PersistentStealthState(Map.of(
first, PlayerStealthState.empty(first).withLastKnownName("Alex"),
second, PlayerStealthState.empty(second).withLastKnownName("ALEX")), Map.of());
KnownPlayerResolver resolver = new KnownPlayerResolver(() -> state, List::of);
assertEquals(KnownPlayerResolver.Status.AMBIGUOUS, resolver.resolve("alex").status());
assertEquals(KnownPlayerResolver.Status.UNKNOWN, resolver.resolve("nobody").status());
assertEquals(2, state.players().size());
}
}
@@ -0,0 +1,89 @@
package games.dmg.spigotstealth;
import static org.mockito.ArgumentMatchers.contains;
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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.Test;
class StealthAdminCommandTest {
@Test
void statusInspectsKnownOfflinePlayerAndReportsAllState() {
UUID playerId = UUID.randomUUID();
try (Fixture fixture = fixture(playerId)) {
CommandSender sender = sender(true);
fixture.command.onCommand(sender, mock(Command.class), "stealthadmin", new String[] {"status", "Alex"});
verify(sender).sendMessage(contains(playerId.toString()));
verify(sender).sendMessage(contains("prepared=yes"));
verify(sender).sendMessage(contains("concealed=no"));
}
}
@Test
void grantPersistsThenReportsAndAuditsAction() {
UUID playerId = UUID.randomUUID();
try (Fixture fixture = fixture(playerId)) {
CommandSender sender = sender(true);
fixture.command.onCommand(sender, mock(Command.class), "stealthadmin", new String[] {"grant", playerId.toString()});
verify(sender, org.mockito.Mockito.timeout(1000)).sendMessage(contains("granted"));
org.junit.jupiter.api.Assertions.assertTrue(fixture.manager.snapshot().player(playerId).unlocked());
org.junit.jupiter.api.Assertions.assertTrue(fixture.audit.stream().anyMatch(message -> message.contains("grant") && message.contains(playerId.toString())));
}
}
@Test
void resetRequiresConfirmationAndPermission() {
UUID playerId = UUID.randomUUID();
try (Fixture fixture = fixture(playerId)) {
CommandSender denied = sender(false);
fixture.command.onCommand(denied, mock(Command.class), "stealthadmin", new String[] {"reset", "Alex", "confirm"});
verify(denied).sendMessage(contains("permission"));
CommandSender allowed = sender(true);
fixture.command.onCommand(allowed, mock(Command.class), "stealthadmin", new String[] {"reset", "Alex"});
verify(allowed).sendMessage(contains("confirm"));
verify(allowed, never()).sendMessage(contains("reset complete"));
}
}
private static CommandSender sender(boolean permission) {
CommandSender sender = mock(CommandSender.class);
when(sender.hasPermission("spigotstealth.admin")).thenReturn(permission);
when(sender.getName()).thenReturn("Operator");
return sender;
}
private static Fixture fixture(UUID playerId) {
StealthStateRepository repository = new StealthStateRepository() {
@Override public PersistentStealthState load() { return new PersistentStealthState(Map.of(), Map.of()); }
@Override public void save(PersistentStealthState state) { }
};
StealthStateManager manager = new StealthStateManager(repository, new PersistentStealthState(Map.of(), Map.of()));
PlayerStealthState player = new PlayerStealthState(playerId, "Alex", 5000L, false, true, false, null, Map.of());
manager.update(state -> state.withPlayer(player)).join();
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
StealthAdministrationService administration = new StealthAdministrationService(
manager, progression, mock(IdentityPresentation.class), ignored -> { }, List::of);
KnownPlayerResolver resolver = new KnownPlayerResolver(manager::snapshot, List::of);
ArrayList<String> audit = new ArrayList<>();
StealthAdminCommand command = new StealthAdminCommand(
administration, resolver, Runnable::run, audit::add, Duration.ofHours(8));
return new Fixture(manager, command, audit);
}
private record Fixture(
StealthStateManager manager,
StealthAdminCommand command,
ArrayList<String> audit) implements AutoCloseable {
@Override public void close() { manager.close(); }
}
}
@@ -0,0 +1,101 @@
package games.dmg.spigotstealth;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
class StealthAdministrationServiceTest {
@Test
void grantsOfflineUnlockAndNotifiesExactlyOnce() {
UUID playerId = UUID.randomUUID();
ArrayList<UUID> notifications = new ArrayList<>();
try (StealthStateManager manager = manager()) {
StealthAdministrationService administration = service(manager, List::of, notifications::add);
assertTrue(administration.grant(playerId, "Alex").join().changed());
assertFalse(administration.grant(playerId, "Alex").join().changed());
assertTrue(manager.snapshot().player(playerId).unlocked());
assertEquals(List.of(playerId), notifications);
}
}
@Test
void resetClearsProgressUnlockPreparationConcealmentAndActiveTracking() {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
org.mockito.Mockito.when(player.getUniqueId()).thenReturn(playerId);
try (StealthStateManager manager = manager()) {
PlayerStealthState state = new PlayerStealthState(
playerId, "Alex", 1234L, true, true, true, Instant.EPOCH, Map.of("future", "keep"));
manager.update(all -> all.withPlayer(state)).join();
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
progression.begin(playerId, "Alex", Instant.EPOCH).join();
IdentityPresentation presentation = mock(IdentityPresentation.class);
StealthAdministrationService administration = new StealthAdministrationService(
manager, progression, presentation, ignored -> { }, () -> List.of(player));
administration.reset(playerId, "Alex", player).join();
PlayerStealthState reset = manager.snapshot().player(playerId);
assertEquals(0L, reset.accumulatedMillis());
assertFalse(reset.unlocked());
assertFalse(reset.preparedLogin());
assertFalse(reset.concealed());
assertFalse(progression.isQualifying(playerId));
assertEquals("keep", reset.unknownFields().get("future"));
verify(presentation).reveal(player);
}
}
@Test
void listsOnlyCurrentlyOnlineConcealedPlayers() {
UUID concealedId = UUID.randomUUID();
UUID ordinaryId = UUID.randomUUID();
Player concealed = player(concealedId, "Hidden");
Player ordinary = player(ordinaryId, "Visible");
try (StealthStateManager manager = manager()) {
manager.update(state -> state
.withPlayer(PlayerStealthState.empty(concealedId).withLastKnownName("Hidden").withSession(false, true))
.withPlayer(PlayerStealthState.empty(ordinaryId).withLastKnownName("Visible"))).join();
StealthAdministrationService administration = service(
manager, () -> List.of(concealed, ordinary), ignored -> { });
assertEquals(List.of("Hidden"), administration.concealedOnlineNames());
}
}
private static StealthAdministrationService service(
StealthStateManager manager,
java.util.function.Supplier<? extends java.util.Collection<? extends Player>> online,
java.util.function.Consumer<UUID> notifier) {
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
return new StealthAdministrationService(
manager, progression, mock(IdentityPresentation.class), notifier, online);
}
private static Player player(UUID playerId, String name) {
Player player = mock(Player.class);
org.mockito.Mockito.when(player.getUniqueId()).thenReturn(playerId);
org.mockito.Mockito.when(player.getName()).thenReturn(name);
return player;
}
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()));
}
}