Merge pull request #180 from simoleo89/fix/items-ownership-and-charges

fix(items): harden ownership and redeem lifecycle
This commit is contained in:
DuckieTM
2026-06-15 07:21:09 +02:00
committed by GitHub
8 changed files with 162 additions and 13 deletions
@@ -0,0 +1,30 @@
package com.eu.habbo.habbohotel.bots;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class BotPickupOwnershipContractTest {
private static String source() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java"));
}
@Test
void roomOwnerPickupReturnsBotToOriginalOwner() throws Exception {
String source = source();
assertTrue(source.contains("HabboInfo receiverInfo = resolvePickupReceiver(bot, habbo);"),
"bot pickup should resolve the receiver without blindly using the picker");
assertTrue(source.contains("private HabboInfo resolvePickupReceiver(Bot bot, Habbo picker)"),
"bot pickup receiver logic should be centralized");
assertTrue(source.contains("return Emulator.getGameEnvironment().getHabboManager().getHabboInfo(bot.getOwnerId());"),
"when a room owner picks up someone else's bot, it should return to the original bot owner");
assertTrue(source.contains("Room botRoom = bot.getRoom();"),
"pickup should remove the bot from the bot's current room, not the receiver's current room");
assertTrue(source.contains("botRoom.removeBot(bot);"),
"bot removal should work even when the original owner is offline");
}
}
@@ -0,0 +1,31 @@
package com.eu.habbo.habbohotel.items.interactions;
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 RentableSpaceChargeContractTest {
@Test
void rentingSpaceChargesCreditsBeforeMarkingSpaceRented() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRentableSpace.java"));
int rentMethod = source.indexOf("public void rent(Habbo habbo)");
assertTrue(rentMethod >= 0, "InteractionRentableSpace must keep explicit rent handling");
String rentHandling = source.substring(rentMethod, Math.min(source.length(), rentMethod + 1400));
assertTrue(rentHandling.contains("int cost = this.rentCost();"),
"Rent cost must be computed once before charging");
assertTrue(rentHandling.contains("boolean hasInfiniteCredits = habbo.hasPermission(Permission.ACC_INFINITE_CREDITS);"),
"Renting must honor infinite-credit staff permission before charging");
assertTrue(rentHandling.contains("!hasInfiniteCredits && habbo.getHabboInfo().getCredits() < cost"),
"Renting must reject non-staff users without enough credits for the computed cost");
assertTrue(rentHandling.contains("habbo.giveCredits(-cost);"),
"Renting must deduct the computed credit cost");
assertTrue(rentHandling.indexOf("habbo.giveCredits(-cost);") < rentHandling.indexOf("this.setRenterId"),
"Credits must be charged before the rentable space is marked as rented");
}
}
@@ -0,0 +1,29 @@
package com.eu.habbo.messages.incoming.rooms.items;
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 MonsterPlantSeedOwnershipContractTest {
@Test
void monsterPlantSeedsCanOnlyBeRedeemedByTheirOwner() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java"));
int seedBranch = source.indexOf("item instanceof InteractionMonsterPlantSeed");
assertTrue(seedBranch >= 0, "ToggleFloorItemEvent must keep monsterplant seed handling explicit");
String seedHandling = source.substring(seedBranch, Math.min(source.length(), seedBranch + 1400));
String ownershipGuard = "if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId())";
assertTrue(seedHandling.contains(ownershipGuard),
"Monsterplant seed redemption must reject callers who do not own the seed");
assertTrue(seedHandling.contains("createMonsterplant"),
"Monsterplant seed handling must create the pet inside the guarded branch");
assertTrue(seedHandling.indexOf(ownershipGuard) < seedHandling.indexOf("createMonsterplant"),
"Ownership rejection must happen before creating the pet");
}
}
@@ -0,0 +1,29 @@
package com.eu.habbo.messages.incoming.rooms.items;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class RedeemClothingContractTest {
private static String source() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java"));
}
@Test
void clothingIsGrantedBeforeVoucherFurnitureIsConsumed() throws Exception {
String source = source();
int grantCall = source.indexOf("grantClothing(");
int roomRemoval = source.indexOf("removeHabboItem(item)");
int deleteItem = source.indexOf("new QueryDeleteHabboItem(item.getId())");
assertTrue(source.contains("private boolean grantClothing(int clothingId)"),
"clothing DB insert should report whether the grant succeeded");
assertTrue(grantCall > -1, "redeem path should call grantClothing before consuming the item");
assertTrue(grantCall < roomRemoval, "room item must not be removed before the clothing grant succeeds");
assertTrue(grantCall < deleteItem, "voucher furniture must not be deleted before the clothing grant succeeds");
}
}