Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import net.tfminecraft.armourshop.objects.SkinCategory;
import net.tfminecraft.armourshop.objects.SkinSet;
import net.tfminecraft.armourshop.pack.apply.PackPullRunner;
import net.tfminecraft.armourshop.pack.delete.ActiveTokenCodeCache;
import net.tfminecraft.armourshop.pack.catalog.CatalogSyncService;
import net.tfminecraft.armourshop.pack.reload.DeferredIaReloadService;
import net.tfminecraft.armourshop.utils.ChatMessages;
Expand Down Expand Up @@ -537,6 +538,7 @@ private boolean handleTokenDelete(CommandSender sender, String codeArg) {
+ (result.error != null ? result.error : "Could not delete token."));
return;
}
ActiveTokenCodeCache.invalidate();
sender.sendMessage(ChatColor.GREEN + "[ArmourShop] "
+ ChatColor.YELLOW + "Deleted token "
+ ChatColor.AQUA + toRevoke);
Expand Down Expand Up @@ -590,6 +592,11 @@ public List<String> onTabComplete(
return filter(completions, args[0]);
}

if (args.length == 3 && args[0].equalsIgnoreCase("token")
&& args[1].equalsIgnoreCase("delete") && Permissions.isAdmin(sender)) {
return filter(ActiveTokenCodeCache.snapshot(), args[2]);
}

if (args.length == 2 && args[0].equalsIgnoreCase("token")) {
List<String> completions = new ArrayList<>();
if (Permissions.isAdmin(sender)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.tfminecraft.armourshop.pack.delete;

import java.util.List;
import net.tfminecraft.armourshop.api.ProvinceSystemClient;

/** Active unused token codes, refreshed asynchronously with the deletion cache TTL. */
public final class ActiveTokenCodeCache {
private static final GenerationalIdCache CACHE = new GenerationalIdCache(() -> {
var result = ProvinceSystemClient.listActiveCodes();
return result.ok
? ProvinceSystemClient.DeletableListResult.success(result.codes.stream()
.map(entry -> entry.code).distinct().toList())
: ProvinceSystemClient.DeletableListResult.fail(result.error);
});

private ActiveTokenCodeCache() {}

public static List<String> snapshot() {
return CACHE.snapshot();
}

public static void invalidate() {
CACHE.invalidate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,15 @@ private SkinCategory category(String id, SkinSet... sets) {
}

@Test void tokenDeletionNormalizesQuotedCodeAndReportsApiOutcomes() {
for (String code : Arrays.asList(null, " ", "\" \"")) { assertTrue(run(player, "token", "delete", code)); assertTrue(message().contains("Usage:")); }
client.when(() -> ProvinceSystemClient.revokeCode("code")).thenReturn(SimpleResult.success(), SimpleResult.fail(null), SimpleResult.fail("rejected"));
assertTrue(run(player, "token", "delete", " \" code \" ")); drain(); assertTrue(message().contains("Deleted token"));
run(player, "token", "delete", "code"); drain(); assertTrue(message().contains("Could not delete token"));
run(player, "token", "delete", "code"); drain(); assertTrue(message().contains("rejected"));
client.verify(() -> ProvinceSystemClient.revokeCode("code"), times(3));
try (var cache = mockStatic(ActiveTokenCodeCache.class)) {
for (String code : Arrays.asList(null, " ", "\" \"")) { assertTrue(run(player, "token", "delete", code)); assertTrue(message().contains("Usage:")); }
client.when(() -> ProvinceSystemClient.revokeCode("code")).thenReturn(SimpleResult.success(), SimpleResult.fail(null), SimpleResult.fail("rejected"));
assertTrue(run(player, "token", "delete", " \" code \" ")); drain(); assertTrue(message().contains("Deleted token"));
run(player, "token", "delete", "code"); drain(); assertTrue(message().contains("Could not delete token"));
run(player, "token", "delete", "code"); drain(); assertTrue(message().contains("rejected"));
client.verify(() -> ProvinceSystemClient.revokeCode("code"), times(3));
cache.verify(ActiveTokenCodeCache::invalidate, times(1));
}
}

@Test void tokenListsUseNamesOfflineLookupAndUuidFallbacks() {
Expand Down Expand Up @@ -283,6 +286,21 @@ private SkinCategory category(String id, SkinSet... sets) {
when(command.getName()).thenReturn("other"); assertTrue(manager.onTabComplete(console, command, "as", new String[]{""}).isEmpty());
}

@Test void tokenDeletionCompletesOnlyCachedCodesForAdminsOnTheExactPath() {
try (var cache = mockStatic(ActiveTokenCodeCache.class)) {
cache.when(ActiveTokenCodeCache::snapshot).thenReturn(List.of("Alpha123", "Beta456"));
assertEquals(List.of("Alpha123", "Beta456"), manager.onTabComplete(console, command, "as", new String[]{"token", "delete", ""}));
assertEquals(List.of("Alpha123"), manager.onTabComplete(console, command, "as", new String[]{"TOKEN", "DELETE", "aLp"}));
assertEquals(List.of(), manager.onTabComplete(console, command, "as", new String[]{"token", "delete", "unknown"}));
cache.clearInvocations();
for (String[] args : List.of(new String[]{"token", "create", ""}, new String[]{"wrong", "delete", ""}, new String[]{"token", "delete", "Alpha123", ""}))
assertEquals(List.of(), manager.onTabComplete(console, command, "as", args));
assertEquals(List.of(), manager.onTabComplete(mock(CommandSender.class), command, "as", new String[]{"token", "delete", ""}));
cache.verifyNoInteractions();
client.verifyNoInteractions();
}
}

@Test void tabCompletionUsesLocaleIndependentIdentifiers() {
var cat = category("ITEMS"); categories.when(CategoryLoader::get).thenReturn(List.of(cat)); Locale old = Locale.getDefault();
try { Locale.setDefault(Locale.forLanguageTag("tr-TR")); assertEquals(List.of("ITEMS"), manager.onTabComplete(console, command, "as", new String[]{"model", "apply", "i"})); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private record CacheField(Object owner, Field field) {}
when(scheduler.runTaskAsynchronously(eq(plugin), any(Runnable.class))).thenAnswer(i -> {workers.add(i.getArgument(1)); return mock(BukkitTask.class);});
when(scheduler.runTask(eq(plugin), any(Runnable.class))).thenAnswer(i -> {main.add(i.getArgument(1)); return mock(BukkitTask.class);});
client = mockStatic(ProvinceSystemClient.class); client.when(() -> ProvinceSystemClient.revokeSubmission(anyString())).thenReturn(SimpleResult.success());
for (Class<?> type : List.of(DeletableSubmissionCache.class, DeletableStaffSkinCache.class)) {
for (Class<?> type : List.of(DeletableSubmissionCache.class, DeletableStaffSkinCache.class, ActiveTokenCodeCache.class)) {
Object cache = field(type, "CACHE").get(null);
for (String name : List.of("IDS", "FETCHED_AT", "refreshInFlight", "generation")) {
Field field = field(GenerationalIdCache.class, name);
Expand Down Expand Up @@ -171,6 +171,27 @@ private void invalidationDuringFetch(boolean staff, boolean fail) throws Excepti
}
}

@Test void activeTokenCodesRefreshOnlyOnWorkersAndDropRevokedCodesAfterInvalidation() {
var alpha = new ActiveCode("Alpha123", null, null, null, null);
var beta = new ActiveCode("Beta456", null, null, null, null);
client.when(ProvinceSystemClient::listActiveCodes).thenReturn(ActiveCodesResult.success(List.of(alpha, beta, alpha)));
assertTrue(ActiveTokenCodeCache.snapshot().isEmpty());
assertTrue(ActiveTokenCodeCache.snapshot().isEmpty());
assertEquals(1, workers.size()); client.verifyNoInteractions();
workers.remove().run();
assertEquals(List.of("Alpha123", "Beta456"), ActiveTokenCodeCache.snapshot());
assertTrue(workers.isEmpty());
client.when(ProvinceSystemClient::listActiveCodes).thenReturn(ActiveCodesResult.success(List.of(beta)));
ActiveTokenCodeCache.invalidate(); workers.remove().run();
assertEquals(List.of("Beta456"), ActiveTokenCodeCache.snapshot());
client.when(ProvinceSystemClient::listActiveCodes).thenReturn(ActiveCodesResult.fail("offline"));
ActiveTokenCodeCache.invalidate(); workers.remove().run();
assertEquals(List.of("Beta456"), ActiveTokenCodeCache.snapshot());
client.when(ProvinceSystemClient::listActiveCodes).thenReturn(ActiveCodesResult.success(List.of()));
workers.remove().run();
assertTrue(ActiveTokenCodeCache.snapshot().isEmpty());
}

@Test void cacheSnapshotsCannotBeMutatedByTabCompleters() {
client.when(ProvinceSystemClient::listDeletableSubmissionIds).thenReturn(DeletableListResult.success(List.of("one")));
client.when(ProvinceSystemClient::listDeletableStaffSkinIds).thenReturn(DeletableListResult.success(List.of("one")));
Expand Down
Loading