diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java index e7952892..b0646439 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java @@ -398,11 +398,12 @@ public class MarketPlace { synchronized (client.getHabbo().getInventory()) { for (MarketPlaceOffer offer : offers) { if (offer.getState().equals(MarketPlaceState.SOLD)) { - client.getHabbo().getInventory().removeMarketplaceOffer(offer); - credits += offer.getPrice(); - removeUser(offer); - offer.needsUpdate(true); - Emulator.getThreading().run(offer); + if (removeUser(offer)) { + client.getHabbo().getInventory().removeMarketplaceOffer(offer); + credits += offer.getPrice(); + offer.needsUpdate(true); + Emulator.getThreading().run(offer); + } } } } @@ -416,13 +417,14 @@ public class MarketPlace { } } - private static void removeUser(MarketPlaceOffer offer) { + private static boolean removeUser(MarketPlaceOffer offer) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE marketplace_items SET user_id = ? WHERE id = ?")) { statement.setInt(1, -1); statement.setInt(2, offer.getOfferId()); - statement.execute(); + return statement.executeUpdate() > 0; } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); + return false; } } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceCreditClaimContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceCreditClaimContractTest.java new file mode 100644 index 00000000..9c0faa49 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceCreditClaimContractTest.java @@ -0,0 +1,38 @@ +package com.eu.habbo.habbohotel.catalog.marketplace; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MarketPlaceCreditClaimContractTest { + private static String marketPlaceSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java")); + } + + @Test + void soldOfferIsDetachedBeforeCreditsAreGranted() throws Exception { + String source = marketPlaceSource(); + int getCreditsStart = source.indexOf("public static void getCredits"); + int removeUserCall = source.indexOf("removeUser(offer)", getCreditsStart); + int creditAccumulator = source.indexOf("credits += offer.getPrice()", getCreditsStart); + int inventoryRemoval = source.indexOf("removeMarketplaceOffer(offer)", getCreditsStart); + + assertTrue(getCreditsStart > -1, "MarketPlace.getCredits must exist"); + assertTrue(removeUserCall > -1, "Sold marketplace offers must be detached in the database"); + assertTrue(removeUserCall < creditAccumulator, + "Credits must not be granted until the sold offer is detached from the seller in the database"); + assertTrue(removeUserCall < inventoryRemoval, + "The in-memory sold offer should remain claimable if the database detach fails"); + } + + @Test + void detachFailureIsObservableByCaller() throws Exception { + String source = marketPlaceSource(); + + assertTrue(source.contains("private static boolean removeUser"), + "removeUser must report whether the marketplace ownership update succeeded"); + } +}