Merge branch 'dev' into chore/deps-resilience-validation

This commit is contained in:
DuckieTM
2026-06-15 07:24:02 +02:00
committed by GitHub
82 changed files with 2418 additions and 264 deletions
@@ -0,0 +1,21 @@
package com.eu.habbo;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConsoleLogbackLayoutTest {
@Test
void consolePatternKeepsStartupMessagesReadable() throws Exception {
String logback = Files.readString(Path.of("src/main/resources/logback.xml"));
assertTrue(logback.contains("morningstarLevel"), "console should use the adaptive level formatter");
assertTrue(logback.contains("morningstarLogger"), "console should use the adaptive logger formatter");
assertTrue(logback.contains("| %msg%n"), "console should leave a clear message column");
assertFalse(logback.contains("%-36logger{36}"), "wide package loggers waste console space");
}
}
@@ -0,0 +1,88 @@
package com.eu.habbo;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class EmulatorStartupConsoleTest {
@Test
void startupHeroUsesUniversalAsciiLayout() {
String hero = Emulator.startupHero();
assertTrue(hero.contains("__ __ ___ ____"));
assertTrue(hero.contains("MORNINGSTAR EXTENDED"));
assertTrue(hero.contains("Version"));
assertTrue(hero.contains("Build"));
assertFalse(hero.contains("\u001B["), "startup hero must not require ANSI support");
}
@Test
void startupHeroCanRenderStyledLayoutWhenAnsiIsAvailable() {
String hero = Emulator.startupHero(true);
assertTrue(hero.contains("\u001B["), "styled hero should include ANSI colors");
assertTrue(hero.contains("[OK] MORNINGSTAR EXTENDED"));
assertTrue(hero.contains("[JVM]"));
assertTrue(hero.endsWith("\u001B[0m\n"), "styled hero should reset terminal attributes");
}
@Test
void consoleStyleAutoDetectsWindowsTerminal() {
assertTrue(Emulator.shouldStyleConsole(
Map.of("WT_SESSION", "abc123"),
true,
"Windows 11",
"auto"));
}
@Test
void consoleStyleFallsBackWhenOutputIsNotInteractive() {
assertFalse(Emulator.shouldStyleConsole(
Map.of("WT_SESSION", "abc123"),
false,
"Windows 11",
"auto"));
}
@Test
void consoleStyleCanBeForcedOff() {
assertFalse(Emulator.shouldStyleConsole(
Map.of("WT_SESSION", "abc123"),
true,
"Windows 11",
"plain"));
}
@Test
void windowsAnsiModeInstallsJansiBeforePrintingStartupHero() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
assertTrue(source.contains("AnsiConsole.systemInstall()"),
"forced ANSI mode must install the Jansi bridge for Windows CMD/System.out");
assertTrue(source.contains("configureAnsiConsole(styledConsole)"),
"console bridge must be configured before startupHero is printed");
assertTrue(source.indexOf("configureAnsiConsole(styledConsole)") < source.indexOf("startupHero(styledConsole)"),
"Jansi must be installed before writing ANSI startup output");
}
@Test
void registersGuiEnabledBeforeReadingIt() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
assertTrue(source.contains("register(\"gui.enabled\", \"0\")"),
"gui.enabled must be registered disabled by default so it does not log missing config errors or start the UI unexpectedly");
assertTrue(source.contains("register(\"gui.autostart.enabled\", \"0\")"),
"GUI autostart must use a new disabled-by-default key so old gui.enabled=1 settings do not launch the current UI");
assertTrue(source.indexOf("register(\"gui.autostart.enabled\", \"0\")") < source.indexOf("shouldLaunchGui()"),
"GUI autostart must be registered before the launch decision");
assertFalse(source.contains("getBoolean(\"gui.enabled\", true)"),
"GUI must not use a true fallback");
assertFalse(source.contains("getBoolean(\"gui.enabled\", false)"),
"legacy gui.enabled must not control startup anymore");
}
}
@@ -0,0 +1,45 @@
package com.eu.habbo.core;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
class CommandDescriptionTextsContractTest {
private static final Path FULL_DATABASE = Path.of("../Default Database/FullDatabase.sql");
private static final Path LIVE_SCHEMA_UPDATE = Path.of("../Database Updates/003_live_required_schema.sql");
private static final List<String> REQUIRED_DESCRIPTION_KEYS = List.of(
"commands.description.acc_modtool_room_info",
"commands.description.cmd_add_youtube_playlist",
"commands.description.cmd_disablemassmentions",
"commands.description.cmd_disablementions",
"commands.description.cmd_give_prefix",
"commands.description.cmd_hidewired",
"commands.description.cmd_list_prefixes",
"commands.description.cmd_remove_prefix",
"commands.description.cmd_setroom_template",
"commands.description.cmd_update_youtube_playlists"
);
@Test
void fullDatabaseDefinesCommandDescriptionsUsedByCommandsList() throws IOException {
assertContainsAllDescriptionKeys(Files.readString(FULL_DATABASE), "FullDatabase.sql");
}
@Test
void liveSchemaUpdateBackfillsCommandDescriptionsForExistingDatabases() throws IOException {
assertContainsAllDescriptionKeys(Files.readString(LIVE_SCHEMA_UPDATE), "003_live_required_schema.sql");
}
private static void assertContainsAllDescriptionKeys(String source, String fileName) {
for (String key : REQUIRED_DESCRIPTION_KEYS) {
assertTrue(source.contains("'" + key + "'"),
fileName + " must define " + key + " to avoid TextsManager missing-key logs");
}
}
}
@@ -0,0 +1,35 @@
package com.eu.habbo.core;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class CommandTextLookupContractTest {
private static final Path TEXTS_MANAGER = Path.of("src/main/java/com/eu/habbo/core/TextsManager.java");
private static final Path COMMANDS_COMMAND = Path.of("src/main/java/com/eu/habbo/habbohotel/commands/CommandsCommand.java");
private static final Path AVAILABLE_COMMANDS_COMPOSER = Path.of(
"src/main/java/com/eu/habbo/messages/outgoing/commands/AvailableCommandsComposer.java");
@Test
void textsManagerExposesQuietFallbackLookupForOptionalTexts() throws IOException {
String source = Files.readString(TEXTS_MANAGER);
assertTrue(source.contains("public String getValueQuietly(String key, String defaultValue)"));
assertTrue(source.contains("return this.texts.getProperty(key, defaultValue);"));
}
@Test
void commandListsUseQuietDescriptionLookups() throws IOException {
String commandsCommand = Files.readString(COMMANDS_COMMAND);
String availableCommandsComposer = Files.readString(AVAILABLE_COMMANDS_COMPOSER);
assertTrue(commandsCommand.contains("getValueQuietly(textKey, \"\")"),
":commands should not log an error when an optional command description is missing");
assertTrue(availableCommandsComposer.contains("getValueQuietly(\"commands.description.\" + cmd.permission, cmd.permission)"),
"available commands composer should not log an error when an optional command description is missing");
}
}
@@ -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,50 @@
package com.eu.habbo.habbohotel.catalog;
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 VoucherClaimContractTest {
private static String voucherSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/Voucher.java"));
}
private static String catalogManagerSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java"));
}
@Test
void voucherClaimIsSynchronizedAndPersistsBeforeRewardEligibility() throws Exception {
String source = voucherSource();
assertTrue(source.contains("public synchronized ClaimResult claimForUser(int userId)"),
"voucher claim should check limits and persist history under a per-voucher lock");
assertTrue(source.contains("private boolean insertHistoryEntry"),
"history insert should report database failure to the caller");
int insertCall = source.indexOf("insertHistoryEntry(userId, timestamp)");
int memoryAppend = source.indexOf("this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp))");
assertTrue(insertCall > -1, "claimForUser must persist the history row");
assertTrue(memoryAppend > insertCall,
"in-memory history must only be updated after the database insert succeeds");
}
@Test
void catalogRewardsOnlyAfterVoucherClaimSucceeds() throws Exception {
String source = catalogManagerSource();
int claim = source.indexOf("Voucher.ClaimResult claimResult = voucher.claimForUser");
int claimedGuard = source.indexOf("case CLAIMED", claim);
int pointsGrant = source.indexOf("client.getHabbo().givePoints", claim);
int creditsGrant = source.indexOf("client.getHabbo().giveCredits", claim);
assertTrue(claim > -1, "CatalogManager must claim the voucher before applying rewards");
assertTrue(claimedGuard > claim, "voucher rewards should only continue for a CLAIMED result");
assertTrue(pointsGrant > claimedGuard, "points must be granted only after CLAIMED");
assertTrue(creditsGrant > claimedGuard, "credits must be granted only after CLAIMED");
}
}
@@ -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");
}
}
@@ -0,0 +1,63 @@
package com.eu.habbo.habbohotel.guilds;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import com.eu.habbo.messages.ClientMessage;
import io.netty.buffer.Unpooled;
import org.junit.jupiter.api.Test;
class GuildBadgeBuilderTest {
@Test
void buildsBadgeFromFlatPartTriplets() {
ClientMessage packet = messageWithInts(
1, 2, 4,
35, 8, 0
);
assertEquals("b001024s035080", GuildBadgeBuilder.readBadge(packet, 6));
}
@Test
void rejectsCountThatDoesNotRepresentCompleteTriplets() {
ClientMessage packet = messageWithInts(1, 2, 4);
assertNull(GuildBadgeBuilder.readBadge(packet, 4));
}
@Test
void rejectsPayloadShorterThanDeclaredCount() {
ClientMessage packet = messageWithInts(1, 2);
assertNull(GuildBadgeBuilder.readBadge(packet, 3));
}
@Test
void rejectsTooManyBadgeParts() {
ClientMessage packet = messageWithInts(
1, 1, 4,
2, 1, 4,
3, 1, 4,
4, 1, 4,
5, 1, 4,
6, 1, 4
);
assertNull(GuildBadgeBuilder.readBadge(packet, 18));
}
@Test
void rejectsPartValuesOutsideBadgeCodeRanges() {
assertNull(GuildBadgeBuilder.readBadge(messageWithInts(1000, 1, 4), 3));
assertNull(GuildBadgeBuilder.readBadge(messageWithInts(1, 100, 4), 3));
assertNull(GuildBadgeBuilder.readBadge(messageWithInts(1, 1, 9), 3));
}
private static ClientMessage messageWithInts(int... values) {
var buffer = Unpooled.buffer(values.length * Integer.BYTES);
for (int value : values) {
buffer.writeInt(value);
}
return new ClientMessage(0, buffer);
}
}
@@ -0,0 +1,24 @@
package com.eu.habbo.habbohotel.guilds;
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 GuildManagerMembershipContractTest {
private static String guildManagerSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java"));
}
@Test
void acceptRequestOnlyPromotesPendingMembershipRows() throws Exception {
String source = guildManagerSource();
assertTrue(source.contains("UPDATE guilds_members SET level_id = ?, member_since = ? WHERE user_id = ? AND guild_id = ? AND level_id = ?"),
"accepting a guild request must only promote rows still in REQUESTED state");
assertTrue(source.contains("statement.setInt(5, GuildRank.REQUESTED.type);"),
"the accept-request update must bind the expected REQUESTED rank guard");
}
}
@@ -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,30 @@
package com.eu.habbo.habbohotel.rooms;
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 RoomTradeManagerContractTest {
private static String roomTradeManagerSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/rooms/RoomTradeManager.java"));
}
@Test
void startTradeRejectsParticipantsAlreadyInActiveTradeInsideLock() throws Exception {
String source = roomTradeManagerSource();
int synchronizedBlock = source.indexOf("synchronized (this.activeTrades)");
int activeGuard = source.indexOf("hasActiveTrade(userOne) || this.hasActiveTrade(userTwo)");
int addTrade = source.indexOf("this.activeTrades.add(trade)");
assertTrue(synchronizedBlock > -1, "RoomTradeManager.startTrade must lock activeTrades before mutation");
assertTrue(activeGuard > synchronizedBlock,
"startTrade must check both participants for an existing active trade while holding the activeTrades lock");
assertTrue(activeGuard < addTrade,
"duplicate participant guard must run before a new RoomTrade is added");
assertTrue(source.contains("private boolean hasActiveTrade(Habbo user)"),
"active trade lookup should be reusable under the same activeTrades lock");
}
}
@@ -0,0 +1,38 @@
package com.eu.habbo.habbohotel.rooms;
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 RoomTradeSafetyContractTest {
private static String roomTradeSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java"));
}
@Test
void sqlFailureStopsBeforeInventoryTransfer() throws Exception {
String source = roomTradeSource();
int catchIndex = source.indexOf("catch (SQLException e)");
int inventoryTransferIndex = source.indexOf("THashSet<HabboItem> itemsUserOne");
assertTrue(catchIndex > -1, "RoomTrade must handle SQL failures explicitly");
assertTrue(inventoryTransferIndex > catchIndex, "Inventory transfer should happen after SQL ownership updates");
assertTrue(source.substring(catchIndex, inventoryTransferIndex).contains("return false"),
"SQL failures must abort the trade before in-memory inventory/credit transfer");
}
@Test
void itemOwnersChangeOnlyAfterDatabaseBatchSucceeds() throws Exception {
String source = roomTradeSource();
int firstOwnerMutation = source.indexOf("item.setUserId(");
int batchExecution = source.indexOf("statement.executeBatch();");
assertTrue(firstOwnerMutation > -1, "RoomTrade should update in-memory item owners after commit");
assertTrue(batchExecution > -1, "RoomTrade should persist item owner changes with a batch update");
assertTrue(firstOwnerMutation > batchExecution,
"In-memory item owners must not change until the database batch has succeeded");
}
}
@@ -0,0 +1,14 @@
package com.eu.habbo.habbohotel.users.infostand;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InfostandBackgroundManagerTest {
@Test
void summaryKeepsStartupLogCompact() {
assertEquals(
"Infostand Background Manager -> Loaded! (260 assets)",
InfostandBackgroundManager.summary(188, 22, 9, 16, 25));
}
}
@@ -0,0 +1,51 @@
package com.eu.habbo.messages;
import com.eu.habbo.messages.incoming.Incoming;
import com.eu.habbo.messages.outgoing.Outgoing;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PacketNamesContractTest {
@Test
void incomingPacketNameIdsAreUnique() throws Exception {
assertPublicFinalPacketIdsAreUnique(Incoming.class);
}
@Test
void outgoingPacketNameIdsAreUnique() throws Exception {
assertPublicFinalPacketIdsAreUnique(Outgoing.class);
}
private static void assertPublicFinalPacketIdsAreUnique(Class<?> packetClass) throws Exception {
Map<Integer, String> seen = new HashMap<>();
Map<Integer, String> duplicates = new HashMap<>();
for (Field field : packetClass.getFields()) {
int modifiers = field.getModifiers();
if (!Modifier.isPublic(modifiers)
|| !Modifier.isStatic(modifiers)
|| !Modifier.isFinal(modifiers)
|| field.getType() != int.class) {
continue;
}
int packetId = field.getInt(null);
if (packetId <= 0) {
continue;
}
String previous = seen.putIfAbsent(packetId, field.getName());
if (previous != null) {
duplicates.put(packetId, previous + " / " + field.getName());
}
}
assertTrue(duplicates.isEmpty(), packetClass.getSimpleName() + " has duplicate packet IDs: " + duplicates);
}
}
@@ -0,0 +1,43 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class CatalogAdminOfferMutationContractTest {
private static final Path CREATE_SOURCE = Path.of(
"src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java");
private static final Path SAVE_SOURCE = Path.of(
"src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java");
@Test
void createAndSaveValidatePayloadAndTargetPageBeforeWriting() throws IOException {
String create = Files.readString(CREATE_SOURCE);
String save = Files.readString(SAVE_SOURCE);
assertTrue(create.contains("CatalogAdminOfferPayload.validate("));
assertTrue(save.contains("CatalogAdminOfferPayload.validate("));
assertTrue(create.contains("getCatalogPage(payload.pageId, payload.pageType) == null"));
assertTrue(save.contains("getCatalogPage(payload.pageId, payload.pageType) == null"));
int createValidation = create.indexOf("CatalogAdminOfferPayload.validate(");
int createInsert = create.indexOf("INSERT INTO catalog_items");
int saveValidation = save.indexOf("CatalogAdminOfferPayload.validate(");
int saveUpdate = save.indexOf("UPDATE catalog_items");
assertTrue(createValidation < createInsert, "create offer should validate before insert SQL is prepared");
assertTrue(saveValidation < saveUpdate, "save offer should validate before update SQL is prepared");
}
@Test
void saveOfferReportsMissingRowsInsteadOfAlwaysSucceeding() throws IOException {
String save = Files.readString(SAVE_SOURCE);
assertTrue(save.contains("statement.executeUpdate() == 0"));
assertTrue(save.contains("Offer not found: "));
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import org.junit.jupiter.api.Test;
class CatalogAdminOfferPayloadTest {
@Test
void acceptsAndNormalizesValidOfferPayload() {
CatalogAdminOfferPayload payload = CatalogAdminOfferPayload.validate(
42, "1, 2,3", "Rare Chair", 100, 5, 0, 1, 0,
"extra", true, 0, 0, 10, CatalogPageType.NORMAL);
assertNotNull(payload);
assertEquals("1,2,3", payload.itemIds);
assertEquals("Rare Chair", payload.catalogName);
}
@Test
void rejectsInvalidItemIdsAndNegativeEconomyValues() {
assertNull(CatalogAdminOfferPayload.validate(42, "1,abc", "Name", 0, 0, 0, 1, 0,
"", false, 0, 0, 0, CatalogPageType.NORMAL));
assertNull(CatalogAdminOfferPayload.validate(42, "1", "Name", -1, 0, 0, 1, 0,
"", false, 0, 0, 0, CatalogPageType.NORMAL));
assertNull(CatalogAdminOfferPayload.validate(42, "1", "Name", 0, 0, 0, 0, 0,
"", false, 0, 0, 0, CatalogPageType.NORMAL));
}
@Test
void builderOffersStillRequireSafeCommonFields() {
assertNotNull(CatalogAdminOfferPayload.validate(42, "", "BC Offer", -1, -1, -1, -1, -1,
"", false, -1, -1, 0, CatalogPageType.BUILDER));
assertNull(CatalogAdminOfferPayload.validate(0, "1", "BC Offer", 0, 0, 0, 1, 0,
"", false, 0, 0, 0, CatalogPageType.BUILDER));
assertNull(CatalogAdminOfferPayload.validate(42, "1", "", 0, 0, 0, 1, 0,
"", false, 0, 0, 0, CatalogPageType.BUILDER));
}
}
@@ -0,0 +1,57 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CatalogAdminPageMutationContractTest {
private static final Path CREATE_SOURCE = Path.of(
"src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java");
private static final Path SAVE_SOURCE = Path.of(
"src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java");
private static final Path MOVE_SOURCE = Path.of(
"src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java");
private static final Path DELETE_SOURCE = Path.of(
"src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java");
@Test
void pageParentChecksStayWithinTheSameCatalogPageType() throws IOException {
String create = Files.readString(CREATE_SOURCE);
String save = Files.readString(SAVE_SOURCE);
String move = Files.readString(MOVE_SOURCE);
assertTrue(create.contains("getCatalogPage(parentId, pageType)"));
assertTrue(save.contains("getCatalogPage(parentId, pageType)"));
assertTrue(save.contains("getCatalogPage(current, pageType)"));
assertTrue(move.contains("getCatalogPage(newParentId, pageType)"));
assertTrue(move.contains("getCatalogPage(current, pageType)"));
}
@Test
void movePageValidatesTargetBeforeTogglingVisibilityOrEnabledState() throws IOException {
String move = Files.readString(MOVE_SOURCE);
int pageLookup = move.indexOf("getCatalogPage(pageId, pageType)");
int enabledToggle = move.indexOf("SET enabled = IF");
int visibleToggle = move.indexOf("SET visible = IF");
assertTrue(pageLookup >= 0, "move page should load the page before mutating it");
assertTrue(pageLookup < enabledToggle, "enabled toggle must not run before page existence is checked");
assertTrue(pageLookup < visibleToggle, "visible toggle must not run before page existence is checked");
}
@Test
void pageMutationsReportMissingRowsInsteadOfAlwaysSucceeding() throws IOException {
String save = Files.readString(SAVE_SOURCE);
String move = Files.readString(MOVE_SOURCE);
String delete = Files.readString(DELETE_SOURCE);
assertTrue(save.contains("statement.executeUpdate() == 0"));
assertTrue(move.contains("statement.executeUpdate() == 0"));
assertTrue(delete.contains("statement.executeUpdate() == 0"));
}
}
@@ -78,4 +78,33 @@ class FurniDataManagerTest {
assertEquals(assetBase.resolve("gamedata").resolve("FurnitureData.json"), source.path());
assertFalse(source.directory());
}
@Test
void prefersRendererConfigOverLegacyFurnidataPath(@TempDir Path dir) throws Exception {
Path legacy = dir.resolve("legacy").resolve("FurnitureData.json");
Files.createDirectories(legacy.getParent());
Files.writeString(legacy, "{}");
Path assetBase = dir.resolve("nitro-assets");
Path rendererSource = assetBase.resolve("gamedata").resolve("FurnitureData.json");
Files.createDirectories(rendererSource.getParent());
Files.writeString(rendererSource, "{}");
Path rendererConfig = dir.resolve("renderer-config.json");
Files.writeString(rendererConfig, """
{
"gamedata.url": "http://localhost:5173/nitro-assets/gamedata",
"furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%"
}
""");
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolveConfigured(
legacy.toString(),
rendererConfig.toString(),
assetBase.toString());
assertTrue(source.ok());
assertEquals(rendererSource, source.path());
assertEquals("renderer-config furnidata.url", source.message());
}
}
@@ -0,0 +1,52 @@
package com.eu.habbo.messages.incoming.furnieditor;
import com.google.gson.JsonParser;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class FurniEditorUpdatePayloadTest {
@Test
void acceptsSafeEditorFields() {
FurniEditorUpdatePayload payload = FurniEditorUpdatePayload.validate(JsonParser.parseString("""
{
"publicName": "Rare Chair",
"type": "s",
"width": 2,
"length": 1,
"stackHeight": 1.5,
"allowTrade": true,
"interactionModesCount": 3
}
""").getAsJsonObject());
assertTrue(payload.valid());
assertEquals(7, payload.values.size());
}
@Test
void rejectsOutOfRangeAndOversizedFields() {
assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"width\":-1}").getAsJsonObject()).valid());
assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"stackHeight\":1000}").getAsJsonObject()).valid());
assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"allowTrade\":2}").getAsJsonObject()).valid());
assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"publicName\":\"" + "x".repeat(57) + "\"}").getAsJsonObject()).valid());
}
@Test
void ignoresUnknownFieldsButRequiresAtLeastOneValidField() {
FurniEditorUpdatePayload payload = FurniEditorUpdatePayload.validate(
JsonParser.parseString("{\"itemName\":\"blocked\",\"unknown\":true}").getAsJsonObject());
assertFalse(payload.valid());
assertEquals("No valid fields to update", payload.error);
}
@Test
void buildsCatalogItemIdsTokenPattern() {
assertEquals("%,12,%", FurniEditorHelper.catalogItemIdsTokenPattern(12));
assertTrue((",112,12,13,").contains(",12,"));
assertFalse((",112,13,").contains(",12,"));
}
}
@@ -0,0 +1,33 @@
package com.eu.habbo.messages.incoming.polls;
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 PollRoomScopeContractTest {
@Test
void pollHandlersRequireMatchingCurrentRoomPoll() throws Exception {
assertRequiresMatchingRoomPoll("AnswerPollEvent.java");
assertRequiresMatchingRoomPoll("CancelPollEvent.java");
assertRequiresMatchingRoomPoll("GetPollDataEvent.java");
}
private void assertRequiresMatchingRoomPoll(String fileName) throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/polls/" + fileName));
int packetPollId = source.indexOf("int pollId = this.packet.readInt();");
int pollLookup = source.indexOf("getPoll(pollId)");
assertTrue(packetPollId >= 0, fileName + " must read the poll id from the packet");
assertTrue(pollLookup >= 0, fileName + " must look up the requested poll explicitly");
String guardedSection = source.substring(packetPollId, pollLookup);
assertTrue(guardedSection.contains("getCurrentRoom()"),
fileName + " must bind poll actions to the caller's current room");
assertTrue(guardedSection.contains("room == null || room.getPollId() != pollId"),
fileName + " must reject poll ids that are not active in the current room");
}
}
@@ -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");
}
}
@@ -0,0 +1,24 @@
package com.eu.habbo.messages.incoming.rooms.users;
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 RoomModerationScopeContractTest {
@Test
void roomUserBanAndMuteAreScopedToCurrentRoom() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/users");
for (String handler : new String[]{"RoomUserBanEvent.java", "RoomUserMuteEvent.java", "UnbanRoomUserEvent.java"}) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("getCurrentRoom()"),
handler + " must authorize room moderation against the user's current room");
assertTrue(source.contains("room.getId() != roomId"),
handler + " must reject client-supplied room ids that do not match the current room");
}
}
}
@@ -0,0 +1,32 @@
package com.eu.habbo.messages.incoming.rooms.users;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class RoomUserRemoveRightsContractTest {
private static final Path SOURCE = Path.of(
"src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserRemoveRightsEvent.java");
@Test
void removeRightsBatchIsBoundedAndRequiresCompletePayload() throws IOException {
String source = Files.readString(SOURCE);
assertTrue(source.contains("private static final int MAX_RIGHTS_REMOVALS = 100;"));
assertTrue(source.contains("PacketGuard.isCountInRange(amount, 1, MAX_RIGHTS_REMOVALS)"));
assertTrue(source.contains("PacketGuard.hasFixedWidthEntries(this.packet, amount, BYTES_PER_USER_ID)"));
int guardIndex = source.indexOf("PacketGuard.isCountInRange(amount, 1, MAX_RIGHTS_REMOVALS)");
int payloadIndex = source.indexOf("PacketGuard.hasFixedWidthEntries(this.packet, amount, BYTES_PER_USER_ID)");
int readIndex = source.indexOf("int userId = this.packet.readInt();");
int removeIndex = source.indexOf("room.removeRights(userId);");
assertTrue(guardIndex < readIndex, "batch size should be validated before reading user ids");
assertTrue(payloadIndex < readIndex, "payload length should be validated before reading user ids");
assertTrue(readIndex < removeIndex, "rights should only be removed after reading a validated user id");
}
}
@@ -0,0 +1,49 @@
package com.eu.habbo.util.logback;
import ch.qos.logback.classic.Level;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConsoleStyleTest {
@Test
void formatsLevelWithIconAndColorWhenStyled() {
String formatted = ConsoleStyle.level(Level.WARN, true);
assertTrue(formatted.contains("\u001B["));
assertTrue(formatted.contains("[!] WARN "));
assertTrue(formatted.endsWith("\u001B[0m"));
}
@Test
void formatsLevelAsPlainTextWhenNotStyled() {
assertEquals("WARN ", ConsoleStyle.level(Level.WARN, false));
}
@Test
void formatsLoggerWithColorWhenStyled() {
String formatted = ConsoleStyle.logger("com.eu.habbo.networking.Server", true);
assertTrue(formatted.contains("\u001B["));
assertTrue(formatted.contains("Server"));
assertTrue(formatted.endsWith("\u001B[0m"));
}
@Test
void keepsLoggerPlainAndCompactWhenNotStyled() {
assertEquals("Server ", ConsoleStyle.logger("com.eu.habbo.networking.Server", false));
}
@Test
void honorsPlainOverrideEvenInWindowsTerminal() {
assertFalse(ConsoleStyle.isEnabled(
Map.of("WT_SESSION", "abc123"),
true,
"Windows 11",
"plain"));
}
}