feat(admin): add persistent sleep-count policy
This commit is contained in:
@@ -7,12 +7,71 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
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 excludedConcealmentIgnoresSleepCountAndRestoresPreviousState() {
|
||||
UUID targetId = UUID.randomUUID();
|
||||
Player target = player(targetId, "Alex");
|
||||
when(target.isSleepingIgnored()).thenReturn(false);
|
||||
Scoreboard scoreboard = mock(Scoreboard.class);
|
||||
Team team = mock(Team.class);
|
||||
when(scoreboard.getTeam(BukkitIdentityPresentation.teamName(targetId))).thenReturn(team);
|
||||
BukkitIdentityPresentation presentation = new BukkitIdentityPresentation(
|
||||
() -> List.of(target), scoreboard, mock(TabListController.class));
|
||||
|
||||
presentation.conceal(target);
|
||||
presentation.reveal(target);
|
||||
|
||||
verify(target).setSleepingIgnored(true);
|
||||
verify(target).setSleepingIgnored(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void includedConcealmentDoesNotChangeOrdinarySleepParticipation() {
|
||||
UUID targetId = UUID.randomUUID();
|
||||
Player target = player(targetId, "Alex");
|
||||
Scoreboard scoreboard = mock(Scoreboard.class);
|
||||
Team team = mock(Team.class);
|
||||
when(scoreboard.getTeam(BukkitIdentityPresentation.teamName(targetId))).thenReturn(team);
|
||||
BukkitIdentityPresentation presentation = new BukkitIdentityPresentation(
|
||||
() -> List.of(target),
|
||||
scoreboard,
|
||||
mock(TabListController.class),
|
||||
() -> SleepCountPolicy.INCLUDE);
|
||||
|
||||
presentation.conceal(target);
|
||||
|
||||
verify(target, never()).setSleepingIgnored(org.mockito.ArgumentMatchers.anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void policyChangesImmediatelyRestoreAndReapplyConcealedSleepParticipation() {
|
||||
UUID targetId = UUID.randomUUID();
|
||||
Player target = player(targetId, "Alex");
|
||||
when(target.isSleepingIgnored()).thenReturn(false);
|
||||
Scoreboard scoreboard = mock(Scoreboard.class);
|
||||
Team team = mock(Team.class);
|
||||
when(scoreboard.getTeam(BukkitIdentityPresentation.teamName(targetId))).thenReturn(team);
|
||||
AtomicReference<SleepCountPolicy> policy = new AtomicReference<>(SleepCountPolicy.EXCLUDE);
|
||||
BukkitIdentityPresentation presentation = new BukkitIdentityPresentation(
|
||||
() -> List.of(target), scoreboard, mock(TabListController.class), policy::get);
|
||||
presentation.conceal(target);
|
||||
|
||||
policy.set(SleepCountPolicy.INCLUDE);
|
||||
presentation.refreshSleepCountPolicy();
|
||||
policy.set(SleepCountPolicy.EXCLUDE);
|
||||
presentation.refreshSleepCountPolicy();
|
||||
|
||||
verify(target, org.mockito.Mockito.times(2)).setSleepingIgnored(true);
|
||||
verify(target).setSleepingIgnored(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void concealRemovesTabEntryAndHidesNameTagWithoutHidingEntity() {
|
||||
UUID targetId = UUID.randomUUID();
|
||||
|
||||
@@ -26,7 +26,7 @@ class StealthAdminCommandTest {
|
||||
fixture.command.onTabComplete(
|
||||
sender(true), mock(Command.class), "stealthadmin", new String[] {"gr"}));
|
||||
assertEquals(
|
||||
List.of("status", "grant", "reset", "list"),
|
||||
List.of("status", "grant", "reset", "list", "sleepcount"),
|
||||
fixture.command.onTabComplete(
|
||||
sender(true), mock(Command.class), "stealthadmin", new String[] {""}));
|
||||
}
|
||||
@@ -51,6 +51,10 @@ class StealthAdminCommandTest {
|
||||
List.of("unlocked"),
|
||||
fixture.command.onTabComplete(
|
||||
administrator, command, "stealthadmin", new String[] {"list", "un"}));
|
||||
assertEquals(
|
||||
List.of("status", "include", "exclude"),
|
||||
fixture.command.onTabComplete(
|
||||
administrator, command, "stealthadmin", new String[] {"sleepcount", ""}));
|
||||
assertEquals(
|
||||
List.of(),
|
||||
fixture.command.onTabComplete(
|
||||
@@ -62,6 +66,28 @@ class StealthAdminCommandTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsAndChangesPersistentSleepCountPolicyWithImmediateRefreshAndAudit() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
try (Fixture fixture = fixture(playerId)) {
|
||||
CommandSender sender = sender(true);
|
||||
Command command = mock(Command.class);
|
||||
|
||||
fixture.command.onCommand(
|
||||
sender, command, "stealthadmin", new String[] {"sleepcount", "status"});
|
||||
verify(sender).sendMessage(contains("excluded"));
|
||||
|
||||
fixture.command.onCommand(
|
||||
sender, command, "stealthadmin", new String[] {"sleepcount", "include"});
|
||||
|
||||
verify(sender, org.mockito.Mockito.timeout(1000)).sendMessage(contains("now included"));
|
||||
assertEquals(SleepCountPolicy.INCLUDE, fixture.manager.snapshot().sleepCountPolicy());
|
||||
verify(fixture.presentation).refreshSleepCountPolicy();
|
||||
org.junit.jupiter.api.Assertions.assertTrue(fixture.audit.stream()
|
||||
.anyMatch(message -> message.contains("sleepcount") && message.contains("include")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusInspectsKnownOfflinePlayerAndReportsAllState() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
@@ -136,18 +162,20 @@ class StealthAdminCommandTest {
|
||||
manager.update(state -> state.withPlayer(player)).join();
|
||||
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
|
||||
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
|
||||
IdentityPresentation presentation = mock(IdentityPresentation.class);
|
||||
StealthAdministrationService administration = new StealthAdministrationService(
|
||||
manager, progression, mock(IdentityPresentation.class), ignored -> { }, List::of);
|
||||
manager, progression, presentation, 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);
|
||||
return new Fixture(manager, command, presentation, audit);
|
||||
}
|
||||
|
||||
private record Fixture(
|
||||
StealthStateManager manager,
|
||||
StealthAdminCommand command,
|
||||
IdentityPresentation presentation,
|
||||
ArrayList<String> audit) implements AutoCloseable {
|
||||
@Override public void close() { manager.close(); }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,18 @@ import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StealthAdministrationServiceTest {
|
||||
@Test
|
||||
void persistsSleepCountPolicyChangesIdempotently() {
|
||||
try (StealthStateManager manager = manager()) {
|
||||
StealthAdministrationService administration = service(manager, List::of, ignored -> { });
|
||||
|
||||
assertEquals(SleepCountPolicy.EXCLUDE, administration.sleepCountPolicy());
|
||||
assertTrue(administration.setSleepCountPolicy(SleepCountPolicy.INCLUDE).join().changed());
|
||||
assertEquals(SleepCountPolicy.INCLUDE, manager.snapshot().sleepCountPolicy());
|
||||
assertFalse(administration.setSleepCountPolicy(SleepCountPolicy.INCLUDE).join().changed());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantsOfflineUnlockAndNotifiesExactlyOnce() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
|
||||
@@ -27,6 +27,9 @@ class StealthSessionServiceTest {
|
||||
sessions.login(concealedId, "Hidden");
|
||||
|
||||
assertEquals(Set.of(concealedId), sessions.concealedPlayerIds());
|
||||
manager.update(state -> state.withPlayer(
|
||||
state.player(concealedId).withSession(false, false))).join();
|
||||
assertEquals(Set.of(), sessions.concealedPlayerIds());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,28 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
class YamlStealthStateRepositoryTest {
|
||||
@TempDir Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void missingOrInvalidSleepCountPolicyDefaultsToExclude() throws Exception {
|
||||
Path missingFile = temporaryDirectory.resolve("missing.yml");
|
||||
YamlStealthStateRepository missing = new YamlStealthStateRepository(missingFile);
|
||||
assertEquals(SleepCountPolicy.EXCLUDE, missing.load().sleepCountPolicy());
|
||||
|
||||
Path invalidFile = temporaryDirectory.resolve("invalid.yml");
|
||||
Files.writeString(invalidFile, "sleep-count-policy: surprise\n");
|
||||
YamlStealthStateRepository invalid = new YamlStealthStateRepository(invalidFile);
|
||||
assertEquals(SleepCountPolicy.EXCLUDE, invalid.load().sleepCountPolicy());
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripsUuidStateAndPreservesUnknownFields() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
Files.writeString(stateFile, "schema-version: 1\nfuture-root: keep\nplayers:\n " + playerId + ":\n accumulated-millis: 25\n unlocked: false\n prepared-login: true\n concealed: false\n qualifying-since: '2026-08-14T10:00:00Z'\n last-known-name: Alex\n future-player: keep-too\n");
|
||||
Files.writeString(stateFile, "schema-version: 1\nsleep-count-policy: include\nfuture-root: keep\nplayers:\n " + playerId + ":\n accumulated-millis: 25\n unlocked: false\n prepared-login: true\n concealed: false\n qualifying-since: '2026-08-14T10:00:00Z'\n last-known-name: Alex\n future-player: keep-too\n");
|
||||
YamlStealthStateRepository repository = new YamlStealthStateRepository(stateFile);
|
||||
|
||||
PersistentStealthState loaded = repository.load();
|
||||
PlayerStealthState player = loaded.player(playerId);
|
||||
assertEquals(SleepCountPolicy.INCLUDE, loaded.sleepCountPolicy());
|
||||
assertEquals(25L, player.accumulatedMillis());
|
||||
assertTrue(player.preparedLogin());
|
||||
assertEquals(Instant.parse("2026-08-14T10:00:00Z"), player.qualifyingSince());
|
||||
@@ -31,6 +44,7 @@ class YamlStealthStateRepositoryTest {
|
||||
repository.save(loaded.withPlayer(player.withAccumulatedMillis(50L)));
|
||||
String saved = Files.readString(stateFile);
|
||||
assertTrue(saved.contains("future-root: keep"));
|
||||
assertTrue(saved.contains("sleep-count-policy: include"));
|
||||
assertTrue(saved.contains("future-player: keep-too"));
|
||||
assertEquals(50L, repository.load().player(playerId).accumulatedMillis());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user