feat(quests): add expiration and held item claims
This commit is contained in:
@@ -37,6 +37,29 @@ final class BukkitIssuerNotifierTest {
|
||||
assertTrue(repository.state.notifications().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedNotificationAcknowledgementRemainsPendingForRetry() throws Exception {
|
||||
Repository repository = new Repository();
|
||||
QuestService service = completedService(repository);
|
||||
IssuerNotification notification = service.state().notifications().values().iterator().next();
|
||||
Server server = mock(Server.class);
|
||||
Player player = mock(Player.class);
|
||||
when(server.getPlayer(notification.recipientId())).thenReturn(player);
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getUniqueId()).thenReturn(notification.recipientId());
|
||||
BukkitIssuerNotifier notifier = new BukkitIssuerNotifier(
|
||||
service, server, Logger.getAnonymousLogger()
|
||||
);
|
||||
repository.fail = true;
|
||||
|
||||
notifier.notifyIfOnline(notification.recipientId());
|
||||
assertEquals(1, service.pendingNotifications(notification.recipientId()).size());
|
||||
|
||||
repository.fail = false;
|
||||
notifier.notifyIfOnline(notification.recipientId());
|
||||
assertTrue(service.pendingNotifications(notification.recipientId()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void offlineNotificationRemainsDurableUntilNextLogin() throws Exception {
|
||||
Repository repository = new Repository();
|
||||
@@ -73,7 +96,14 @@ final class BukkitIssuerNotifierTest {
|
||||
|
||||
private static final class Repository implements QuestRepository {
|
||||
private QuestState state = QuestState.empty();
|
||||
private boolean fail;
|
||||
@Override public QuestState load() { return state; }
|
||||
@Override public void save(QuestState candidate) { state = candidate; }
|
||||
@Override
|
||||
public void save(QuestState candidate) throws java.io.IOException {
|
||||
if (fail) {
|
||||
throw new java.io.IOException("disk full");
|
||||
}
|
||||
state = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class BukkitQuestClaimInventoryTest {
|
||||
@Test
|
||||
void inventoryOverflowIsDroppedAtFeetWithOwnerProtection() {
|
||||
UUID owner = UUID.randomUUID();
|
||||
Player player = mock(Player.class);
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
World world = mock(World.class);
|
||||
Location feet = mock(Location.class);
|
||||
Item dropped = mock(Item.class);
|
||||
ItemStack requested = mock(ItemStack.class);
|
||||
ItemStack requestedCopy = mock(ItemStack.class);
|
||||
ItemStack leftover = mock(ItemStack.class);
|
||||
ItemStack leftoverCopy = mock(ItemStack.class);
|
||||
when(requested.clone()).thenReturn(requestedCopy);
|
||||
when(leftover.clone()).thenReturn(leftoverCopy);
|
||||
when(leftover.getAmount()).thenReturn(2);
|
||||
when(player.getUniqueId()).thenReturn(owner);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(player.getWorld()).thenReturn(world);
|
||||
when(player.getLocation()).thenReturn(feet);
|
||||
when(inventory.getStorageContents()).thenReturn(new ItemStack[36]);
|
||||
HashMap<Integer, ItemStack> leftovers = new HashMap<>();
|
||||
leftovers.put(0, leftover);
|
||||
when(inventory.addItem(any(ItemStack.class))).thenReturn(leftovers);
|
||||
when(world.dropItem(any(Location.class), any(ItemStack.class))).thenReturn(dropped);
|
||||
|
||||
QuestClaimInventory.Delivery delivery = BukkitQuestClaimInventory.deliver(
|
||||
player, List.of(requested)
|
||||
);
|
||||
|
||||
assertEquals(2, delivery.overflowAmount());
|
||||
verify(world).dropItem(feet, leftoverCopy);
|
||||
verify(dropped).setOwner(owner);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ownerProtectionFailureRestoresInventoryAndRemovesPartialDrop() {
|
||||
Player player = mock(Player.class);
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
World world = mock(World.class);
|
||||
Location feet = mock(Location.class);
|
||||
Item dropped = mock(Item.class);
|
||||
ItemStack requested = mock(ItemStack.class);
|
||||
ItemStack requestedCopy = mock(ItemStack.class);
|
||||
ItemStack leftover = mock(ItemStack.class);
|
||||
ItemStack leftoverCopy = mock(ItemStack.class);
|
||||
ItemStack existing = mock(ItemStack.class);
|
||||
ItemStack existingCopy = mock(ItemStack.class);
|
||||
when(requested.clone()).thenReturn(requestedCopy);
|
||||
when(leftover.clone()).thenReturn(leftoverCopy);
|
||||
when(existing.clone()).thenReturn(existingCopy);
|
||||
when(existingCopy.clone()).thenReturn(existingCopy);
|
||||
ItemStack[] original = new ItemStack[] {existing};
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(player.getWorld()).thenReturn(world);
|
||||
when(player.getLocation()).thenReturn(feet);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(inventory.getStorageContents()).thenReturn(original);
|
||||
HashMap<Integer, ItemStack> leftovers = new HashMap<>();
|
||||
leftovers.put(0, leftover);
|
||||
when(inventory.addItem(requestedCopy)).thenReturn(leftovers);
|
||||
when(world.dropItem(any(Location.class), any(ItemStack.class))).thenReturn(dropped);
|
||||
doThrow(new IllegalStateException("owner rejected")).when(dropped).setOwner(any());
|
||||
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> BukkitQuestClaimInventory.deliver(player, List.of(requested))
|
||||
);
|
||||
|
||||
verify(inventory).setStorageContents(any(ItemStack[].class));
|
||||
verify(dropped).remove();
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,26 @@ final class QuestBoardDialogUiTest {
|
||||
assertEquals(NOW, completer.completedAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimActionUsesEquivalentCollectionFlowAtABoard() {
|
||||
RecordingCreator creator = new RecordingCreator(false);
|
||||
java.util.concurrent.atomic.AtomicReference<Player> claimantPlayer =
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
QuestClaimGateway claimant = player -> {
|
||||
claimantPlayer.set(player);
|
||||
return new ClaimCollectionResult(1, 0);
|
||||
};
|
||||
QuestBoardDialogUi ui = new QuestBoardDialogUi(
|
||||
creator, now -> List.of(), null, null, claimant,
|
||||
Clock.fixed(NOW, ZoneOffset.UTC)
|
||||
);
|
||||
Player player = mock(Player.class);
|
||||
|
||||
ui.submitClaim(player);
|
||||
|
||||
assertEquals(player, claimantPlayer.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancellationActionUsesEquivalentCancellationFlow() {
|
||||
RecordingCreator creator = new RecordingCreator(false);
|
||||
@@ -69,6 +89,47 @@ final class QuestBoardDialogUiTest {
|
||||
assertEquals(NOW, canceller.cancelledAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void boardListingDistinguishesDeliveredBlocksFromReturnedRewards() {
|
||||
RecordingCreator creator = new RecordingCreator(false);
|
||||
Player player = mock(Player.class);
|
||||
UUID owner = UUID.randomUUID();
|
||||
org.mockito.Mockito.when(player.getUniqueId()).thenReturn(owner);
|
||||
QuestClaimGateway claimant = new QuestClaimGateway() {
|
||||
@Override
|
||||
public ClaimCollectionResult collect(Player ignored) {
|
||||
return new ClaimCollectionResult(0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuestClaim> pendingClaims(Player ignored) {
|
||||
return List.of(
|
||||
new QuestClaim(
|
||||
UUID.randomUUID(), UUID.randomUUID(), owner,
|
||||
List.of(new EscrowItem("STONE", 2, null)), NOW,
|
||||
QuestClaimSource.COMPLETION
|
||||
),
|
||||
new QuestClaim(
|
||||
UUID.randomUUID(), UUID.randomUUID(), owner,
|
||||
List.of(new EscrowItem("DIAMOND", 1, null)), NOW,
|
||||
QuestClaimSource.EXPIRATION
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
QuestBoardDialogUi ui = new QuestBoardDialogUi(
|
||||
creator, now -> List.of(), null, null, claimant,
|
||||
Clock.fixed(NOW, ZoneOffset.UTC)
|
||||
);
|
||||
|
||||
String listing = ui.claimListingText(player);
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertTrue(listing.contains("DELIVERED BLOCKS — 2 STONE"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(
|
||||
listing.contains("RETURNED REWARD (EXPIRATION) — 1 DIAMOND")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void boardOnlyOffersCancellationActionsForPlayersOwnedActiveQuests() {
|
||||
RecordingCreator creator = new RecordingCreator(false);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class QuestClaimControllerTest {
|
||||
@Test
|
||||
void collectsEveryClaimAndClearlyDistinguishesItemsAndOverflow() throws Exception {
|
||||
Repository repository = new Repository();
|
||||
QuestService service = serviceWithAllClaimSources(repository);
|
||||
UUID issuer = service.state().claims().keySet().iterator().next();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(issuer);
|
||||
RecordingInventory inventory = new RecordingInventory(2);
|
||||
|
||||
ClaimCollectionResult result = new QuestClaimController(service, inventory).collect(player);
|
||||
|
||||
assertEquals(new ClaimCollectionResult(3, 6), result);
|
||||
assertTrue(service.claimsFor(issuer).isEmpty());
|
||||
verify(player).sendMessage(contains("delivered blocks from completed quest"));
|
||||
verify(player).sendMessage(contains("returned reward from cancelled quest"));
|
||||
verify(player).sendMessage(contains("returned reward from expired quest"));
|
||||
verify(player).sendMessage(contains("dropped at your feet, protected for you"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deliveryFailureLeavesClaimPendingForRetry() throws Exception {
|
||||
Repository repository = new Repository();
|
||||
QuestService service = expiredService(repository);
|
||||
UUID issuer = service.state().claims().keySet().iterator().next();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(issuer);
|
||||
QuestClaimInventory inventory = items -> ignored -> {
|
||||
throw new IllegalStateException("world rejected drop");
|
||||
};
|
||||
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> new QuestClaimController(service, inventory).collect(player)
|
||||
);
|
||||
|
||||
assertEquals(1, service.claimsFor(issuer).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void acknowledgementFailureRollsBackDeliveryAndLeavesClaimPending() throws Exception {
|
||||
Repository repository = new Repository();
|
||||
QuestService service = expiredService(repository);
|
||||
UUID issuer = service.state().claims().keySet().iterator().next();
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(issuer);
|
||||
RecordingInventory inventory = new RecordingInventory(0);
|
||||
repository.fail = true;
|
||||
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() -> new QuestClaimController(service, inventory).collect(player)
|
||||
);
|
||||
|
||||
assertEquals(1, inventory.rollbacks);
|
||||
assertEquals(1, service.claimsFor(issuer).size());
|
||||
}
|
||||
|
||||
private static QuestService serviceWithAllClaimSources(Repository repository) throws Exception {
|
||||
QuestService service = new QuestService(repository);
|
||||
UUID issuer = UUID.randomUUID();
|
||||
Quest completed = create(service, issuer, "STONE");
|
||||
Quest cancelled = create(service, issuer, "DIRT");
|
||||
Quest expired = create(service, issuer, "SAND");
|
||||
service.complete(
|
||||
completed.id(), new EscrowItem("STONE", 1, null), Instant.EPOCH.plusSeconds(1)
|
||||
);
|
||||
service.cancel(cancelled.id(), issuer, Instant.EPOCH.plusSeconds(1));
|
||||
service.expire(expired.expiresAt());
|
||||
return service;
|
||||
}
|
||||
|
||||
private static QuestService expiredService(Repository repository) throws Exception {
|
||||
QuestService service = new QuestService(repository);
|
||||
Quest quest = create(service, UUID.randomUUID(), "STONE");
|
||||
service.expire(quest.expiresAt());
|
||||
return service;
|
||||
}
|
||||
|
||||
private static Quest create(QuestService service, UUID issuer, String material)
|
||||
throws IOException {
|
||||
return service.create(
|
||||
issuer, "Issuer", material, 1,
|
||||
List.of(new EscrowItem("DIAMOND", 1, null)), Instant.EPOCH
|
||||
);
|
||||
}
|
||||
|
||||
private static final class RecordingInventory implements QuestClaimInventory {
|
||||
private final int overflow;
|
||||
private int rollbacks;
|
||||
|
||||
private RecordingInventory(int overflow) {
|
||||
this.overflow = overflow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreparedClaim prepare(List<EscrowItem> items) {
|
||||
return player -> new Delivery() {
|
||||
@Override public int overflowAmount() { return overflow; }
|
||||
@Override public void rollback() { rollbacks++; }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Repository implements QuestRepository {
|
||||
private QuestState state = QuestState.empty();
|
||||
private boolean fail;
|
||||
@Override public QuestState load() { return state; }
|
||||
@Override public void save(QuestState candidate) throws IOException {
|
||||
if (fail) {
|
||||
throw new IOException("disk full");
|
||||
}
|
||||
state = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,28 @@ final class QuestCommandTest {
|
||||
assertEquals(NOW, completer.completedAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimCommandUsesTheSameCollectionGateway() {
|
||||
RecordingCreator creator = new RecordingCreator();
|
||||
Player player = mock(Player.class);
|
||||
java.util.concurrent.atomic.AtomicReference<Player> claimantPlayer =
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
QuestClaimGateway claimant = claimingPlayer -> {
|
||||
claimantPlayer.set(claimingPlayer);
|
||||
return new ClaimCollectionResult(1, 0);
|
||||
};
|
||||
QuestCommand executor = new QuestCommand(
|
||||
creator, now -> List.of(), null, null, claimant,
|
||||
Clock.fixed(NOW, ZoneOffset.UTC)
|
||||
);
|
||||
|
||||
assertTrue(executor.onCommand(
|
||||
player, mock(Command.class), "quests", new String[] {"claim"}
|
||||
));
|
||||
|
||||
assertEquals(player, claimantPlayer.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelCommandRoutesPlayerAndCurrentTimeToEquivalentGateway() {
|
||||
RecordingCreator creator = new RecordingCreator();
|
||||
@@ -160,6 +182,8 @@ final class QuestCommandTest {
|
||||
executor.onTabComplete(player, command, "quests", new String[] {"li"}));
|
||||
assertEquals(List.of("cancel"),
|
||||
executor.onTabComplete(player, command, "quests", new String[] {"ca"}));
|
||||
assertEquals(List.of("claim"),
|
||||
executor.onTabComplete(player, command, "quests", new String[] {"cl"}));
|
||||
assertEquals(List.of("STONE", "STONE_BRICKS"), executor.onTabComplete(
|
||||
player, command, "quests", new String[] {"create", "sto"}
|
||||
));
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class QuestExpiryAndClaimServiceTest {
|
||||
@Test
|
||||
void expiryReturnsExactRewardAsTypedClaimAndAcknowledgementRemovesIt() throws Exception {
|
||||
MemoryQuestRepository repository = new MemoryQuestRepository();
|
||||
QuestService service = new QuestService(repository);
|
||||
UUID issuer = UUID.randomUUID();
|
||||
EscrowItem reward = new EscrowItem("DIAMOND", 3, "exact-data");
|
||||
Quest quest = service.create(
|
||||
issuer, "Issuer", "STONE", 64, List.of(reward), Instant.EPOCH
|
||||
);
|
||||
|
||||
service.expire(quest.expiresAt());
|
||||
List<QuestClaim> claims = service.claimsFor(issuer);
|
||||
|
||||
assertEquals(QuestStatus.EXPIRED, service.state().quests().get(quest.id()).status());
|
||||
assertEquals(List.of(reward), claims.getFirst().items());
|
||||
assertEquals(QuestClaimSource.EXPIRATION, claims.getFirst().source());
|
||||
assertEquals(QuestClaimType.RETURNED_REWARD, claims.getFirst().type());
|
||||
service.acknowledgeClaim(issuer, claims.getFirst().id());
|
||||
assertTrue(service.claimsFor(issuer).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sevenDayBoundaryIsExclusiveBeforeAndDueExactlyAtExpiration() throws Exception {
|
||||
QuestService service = new QuestService(new MemoryQuestRepository());
|
||||
Quest quest = create(service, UUID.randomUUID());
|
||||
|
||||
assertTrue(service.expire(quest.expiresAt().minusNanos(1)).isEmpty());
|
||||
assertEquals(QuestStatus.ACTIVE, service.state().quests().get(quest.id()).status());
|
||||
|
||||
assertEquals(1, service.expire(quest.expiresAt()).size());
|
||||
assertEquals(QuestStatus.EXPIRED, service.state().quests().get(quest.id()).status());
|
||||
assertTrue(service.expire(quest.expiresAt().plusSeconds(1)).isEmpty());
|
||||
assertEquals(1, service.claimsFor(quest.issuerId()).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionCancellationAndExpirationClaimsKeepTheirDistinctSources() throws Exception {
|
||||
QuestService service = new QuestService(new MemoryQuestRepository());
|
||||
UUID issuer = UUID.randomUUID();
|
||||
Quest completed = create(service, issuer);
|
||||
Quest cancelled = service.create(
|
||||
issuer, "Issuer", "DIRT", 1,
|
||||
List.of(new EscrowItem("EMERALD", 2, null)), Instant.EPOCH
|
||||
);
|
||||
Quest expired = service.create(
|
||||
issuer, "Issuer", "SAND", 1,
|
||||
List.of(new EscrowItem("GOLD_INGOT", 4, null)), Instant.EPOCH
|
||||
);
|
||||
|
||||
service.complete(
|
||||
completed.id(), new EscrowItem("STONE", 1, null), Instant.EPOCH.plusSeconds(1)
|
||||
);
|
||||
service.cancel(cancelled.id(), issuer, Instant.EPOCH.plusSeconds(1));
|
||||
service.expire(expired.expiresAt());
|
||||
|
||||
assertEquals(
|
||||
List.of(
|
||||
QuestClaimSource.COMPLETION,
|
||||
QuestClaimSource.CANCELLATION,
|
||||
QuestClaimSource.EXPIRATION
|
||||
),
|
||||
service.claimsFor(issuer).stream().map(QuestClaim::source).toList()
|
||||
);
|
||||
assertEquals(
|
||||
List.of(
|
||||
QuestClaimType.DELIVERED_BLOCKS,
|
||||
QuestClaimType.RETURNED_REWARD,
|
||||
QuestClaimType.RETURNED_REWARD
|
||||
),
|
||||
service.claimsFor(issuer).stream().map(QuestClaim::type).toList()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedExpirationPersistsNothingAndCanBeRetriedWithoutDuplication() throws Exception {
|
||||
MemoryQuestRepository repository = new MemoryQuestRepository();
|
||||
QuestService service = new QuestService(repository);
|
||||
Quest quest = create(service, UUID.randomUUID());
|
||||
repository.fail = true;
|
||||
|
||||
assertThrows(IOException.class, () -> service.expire(quest.expiresAt()));
|
||||
assertEquals(QuestStatus.ACTIVE, service.state().quests().get(quest.id()).status());
|
||||
assertTrue(service.claimsFor(quest.issuerId()).isEmpty());
|
||||
assertTrue(service.pendingNotifications(quest.issuerId()).isEmpty());
|
||||
|
||||
repository.fail = false;
|
||||
service.expire(quest.expiresAt());
|
||||
service.expire(quest.expiresAt());
|
||||
assertEquals(1, service.claimsFor(quest.issuerId()).size());
|
||||
assertEquals(1, service.pendingNotifications(quest.issuerId()).size());
|
||||
}
|
||||
|
||||
private static Quest create(QuestService service, UUID issuer) throws IOException {
|
||||
return service.create(
|
||||
issuer, "Issuer", "STONE", 1,
|
||||
List.of(new EscrowItem("DIAMOND", 3, null)), Instant.EPOCH
|
||||
);
|
||||
}
|
||||
|
||||
private static final class MemoryQuestRepository implements QuestRepository {
|
||||
private QuestState state = QuestState.empty();
|
||||
private boolean fail;
|
||||
|
||||
@Override public QuestState load() { return state; }
|
||||
|
||||
@Override
|
||||
public void save(QuestState candidate) throws IOException {
|
||||
if (fail) {
|
||||
throw new IOException("disk full");
|
||||
}
|
||||
state = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Logger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class QuestExpiryTaskTest {
|
||||
@Test
|
||||
void scheduledPassExpiresDueQuestAndRequestsImmediateOnlineNotification() throws Exception {
|
||||
Instant boundary = Instant.parse("2026-09-12T03:00:00Z");
|
||||
Repository repository = new Repository();
|
||||
QuestService service = new QuestService(repository);
|
||||
UUID issuer = UUID.randomUUID();
|
||||
service.create(
|
||||
issuer, "Issuer", "STONE", 1,
|
||||
List.of(new EscrowItem("DIAMOND", 1, null)),
|
||||
boundary.minusSeconds(604800)
|
||||
);
|
||||
AtomicReference<UUID> notified = new AtomicReference<>();
|
||||
QuestExpiryTask task = new QuestExpiryTask(
|
||||
service, notified::set, Clock.fixed(boundary, ZoneOffset.UTC),
|
||||
Logger.getAnonymousLogger()
|
||||
);
|
||||
|
||||
task.run();
|
||||
|
||||
assertEquals(issuer, notified.get());
|
||||
assertEquals(1, service.pendingNotifications(issuer).size());
|
||||
assertEquals(QuestStatus.EXPIRED, service.state().quests().values().iterator().next().status());
|
||||
}
|
||||
|
||||
private static final class Repository implements QuestRepository {
|
||||
private QuestState state = QuestState.empty();
|
||||
@Override public QuestState load() { return state; }
|
||||
@Override public void save(QuestState candidate) { state = candidate; }
|
||||
}
|
||||
}
|
||||
@@ -124,9 +124,43 @@ final class YamlQuestRepositoryTest {
|
||||
assertEquals(expected, repository.load());
|
||||
String yaml = Files.readString(path);
|
||||
assertTrue(yaml.contains("claims:"));
|
||||
assertTrue(yaml.contains("source: COMPLETION"));
|
||||
assertTrue(yaml.contains("notifications:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sourceLessHistoricalClaimsInferTypeFromQuestLifecycle() throws Exception {
|
||||
Path path = temporaryDirectory.resolve("quests.yml");
|
||||
Files.writeString(path, """
|
||||
quests:
|
||||
- id: 00000000-0000-0000-0000-000000000010
|
||||
issuer-id: 00000000-0000-0000-0000-000000000001
|
||||
issuer-name: Issuer
|
||||
requested-material: STONE
|
||||
requested-amount: 1
|
||||
created-at: '2026-09-05T03:00:00Z'
|
||||
expires-at: '2026-09-12T03:00:00Z'
|
||||
status: CANCELLED
|
||||
reward:
|
||||
- material: DIAMOND
|
||||
amount: 1
|
||||
claims:
|
||||
- id: 00000000-0000-0000-0000-000000000020
|
||||
quest-id: 00000000-0000-0000-0000-000000000010
|
||||
owner-id: 00000000-0000-0000-0000-000000000001
|
||||
created-at: '2026-09-05T04:00:00Z'
|
||||
items:
|
||||
- material: DIAMOND
|
||||
amount: 1
|
||||
""");
|
||||
|
||||
QuestClaim claim = new YamlQuestRepository(path).load().claims().values()
|
||||
.iterator().next().getFirst();
|
||||
|
||||
assertEquals(QuestClaimSource.CANCELLATION, claim.source());
|
||||
assertEquals(QuestClaimType.RETURNED_REWARD, claim.type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedStateIsRejectedRatherThanPartiallyLoaded() throws Exception {
|
||||
Path path = temporaryDirectory.resolve("quests.yml");
|
||||
|
||||
Reference in New Issue
Block a user