Merge branch 'dev' into fix/wired-count-time-payloads

This commit is contained in:
DuckieTM
2026-06-18 12:40:04 +02:00
committed by GitHub
206 changed files with 3889 additions and 569 deletions
@@ -0,0 +1,64 @@
package com.eu.habbo.habbohotel.commands;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CommandTargetGuardContractTest {
@Test
void highRiskUserCommandsUseCentralTargetGuard() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/habbohotel/commands");
for (String command : List.of(
"AlertCommand.java",
"BanCommand.java",
"DisconnectCommand.java",
"GivePrefixCommand.java",
"GiveRankCommand.java",
"IPBanCommand.java",
"MachineBanCommand.java",
"MuteCommand.java",
"RemovePrefixCommand.java",
"SuperbanCommand.java",
"UnmuteCommand.java"
)) {
String source = Files.readString(base.resolve(command));
assertTrue(source.contains("CommandTargetGuard.canTarget"),
command + " must use the central command target guard for staff/core rank handling");
}
}
@Test
void rankGrantingUsesCentralAssignmentGuard() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java"));
assertTrue(source.contains("CommandTargetGuard.canAssignRank"),
"GiveRankCommand must guard the assigned rank with the same core-rank semantics");
}
@Test
void targetGuardKeepsCorePeerOverrideCentralized() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java"));
String rule = "targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId";
assertTrue(countOccurrences(source, rule) >= 2,
"non-core command users must only target lower ranks while the highest/core rank may target peer ranks");
}
private static int countOccurrences(String source, String needle) {
int count = 0;
int index = 0;
while ((index = source.indexOf(needle, index)) >= 0) {
count++;
index += needle.length();
}
return count;
}
}
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.gameclients;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertTrue;
class GameClientManagerContractTest {
@@ -19,4 +20,11 @@ class GameClientManagerContractTest {
assertDoesNotThrow(() -> manager.disposeClient(null));
assertDoesNotThrow(() -> manager.forceDisposeClient(null));
}
@Test
void gameClientDisposeIsExplicitlyIdempotent() throws Exception {
assertTrue(java.util.concurrent.atomic.AtomicBoolean.class.isAssignableFrom(
GameClient.class.getDeclaredField("disposed").getType()
));
}
}
@@ -0,0 +1,26 @@
package com.eu.habbo.habbohotel.items;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ItemDataGuardTest {
@Test
void normalizesExtraDataToDatabaseBound() {
assertEquals("", ItemDataGuard.normalizeExtraData(null));
assertEquals(ItemDataGuard.MAX_EXTRA_DATA_LENGTH,
ItemDataGuard.normalizeExtraData("x".repeat(ItemDataGuard.MAX_EXTRA_DATA_LENGTH + 1)).length());
}
@Test
void parsesOnlyPositiveVendingIds() {
assertArrayEquals(new int[]{1, 2, 3}, ItemDataGuard.parsePositiveIntList("1; 2.bad,3,-4,0"));
}
@Test
void ignoresMalformedMultiHeights() {
assertArrayEquals(new double[]{0.5, 1.25}, ItemDataGuard.parseHeights("0.5;nope;Infinity;1.25"));
}
}
@@ -0,0 +1,51 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
import com.eu.habbo.messages.ClientMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredInputGuardTest {
@Test
void trimsOversizedStringParams() {
String input = "x".repeat(WiredInputGuard.MAX_STRING_PARAM_LENGTH + 1);
ClientMessage message = new ClientMessage(1, stringBuffer(input));
assertEquals(WiredInputGuard.MAX_STRING_PARAM_LENGTH,
WiredInputGuard.readStringParam(message).length());
}
@Test
void filtersNonPositiveFurniIds() {
ByteBuf buffer = Unpooled.buffer();
buffer.writeInt(4);
buffer.writeInt(1);
buffer.writeInt(0);
buffer.writeInt(-1);
buffer.writeInt(2);
assertArrayEquals(new int[]{1, 2}, WiredInputGuard.readFurniIds(new ClientMessage(1, buffer)));
}
@Test
void clampsDelayAndSelectionCode() {
assertEquals(0, WiredInputGuard.normalizeDelay(-10));
assertEquals(WiredInputGuard.DEFAULT_MAX_DELAY, WiredInputGuard.normalizeDelay(WiredInputGuard.DEFAULT_MAX_DELAY + 1));
assertEquals(-1, WiredInputGuard.normalizeStuffSelectionCode(99));
assertEquals(2, WiredInputGuard.normalizeStuffSelectionCode(2));
}
private static ByteBuf stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
ByteBuf buffer = Unpooled.buffer();
buffer.writeShort(bytes.length);
buffer.writeBytes(bytes);
return buffer;
}
}
@@ -0,0 +1,26 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredLegacyDataGuardTest {
@Test
void malformedDelayFallsBackToZero() {
assertEquals(0, WiredLegacyDataGuard.parseDelay(null));
assertEquals(0, WiredLegacyDataGuard.parseDelay("nope"));
assertEquals(0, WiredLegacyDataGuard.parseDelay("-5"));
}
@Test
void oversizedDelayIsClampedThroughWiredInputGuard() {
assertEquals(WiredLegacyDataGuard.DEFAULT_MAX_DELAY, WiredLegacyDataGuard.parseDelay("999999"));
}
@Test
void nullRoomOrBlankItemsReturnEmptyList() {
assertEquals(0, WiredLegacyDataGuard.parseRoomItems("1;2;bad", null).size());
assertEquals(0, WiredLegacyDataGuard.parseRoomItems("", null).size());
}
}
@@ -0,0 +1,28 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredNumericInputGuardTest {
@Test
void rejectsInvalidOrNonPositiveAmounts() {
assertEquals(0, WiredNumericInputGuard.parsePositiveAmount(null, 100));
assertEquals(0, WiredNumericInputGuard.parsePositiveAmount("nope", 100));
assertEquals(0, WiredNumericInputGuard.parsePositiveAmount("0", 100));
assertEquals(0, WiredNumericInputGuard.parsePositiveAmount("-5", 100));
}
@Test
void clampsAmountsToConfiguredMaximum() {
assertEquals(50, WiredNumericInputGuard.parsePositiveAmount("50", 100));
assertEquals(100, WiredNumericInputGuard.parsePositiveAmount("500", 100));
}
@Test
void appliesAbsoluteMaximumEvenWhenConfiguredTooHigh() {
assertEquals(WiredNumericInputGuard.MAX_ABSOLUTE_AMOUNT,
WiredNumericInputGuard.parsePositiveAmount("999999999", Integer.MAX_VALUE));
}
}
@@ -0,0 +1,35 @@
package com.eu.habbo.habbohotel.items.interactions.wired;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredTimerInputGuardTest {
@Test
void clientTimerUnitsAreMultipliedWithoutOverflow() {
assertEquals(500, WiredTimerInputGuard.fromClientUnits(1, 500, 500));
assertEquals(WiredTimerInputGuard.MAX_TIMER_MS,
WiredTimerInputGuard.fromClientUnits(Integer.MAX_VALUE, 5000, 5000));
}
@Test
void invalidClientTimerUnitsUseMinimumDelay() {
assertEquals(500, WiredTimerInputGuard.fromClientUnits(0, 500, 500));
assertEquals(5000, WiredTimerInputGuard.fromClientUnits(-10, 5000, 5000));
}
@Test
void storedTimerValuesFallbackOrClamp() {
assertEquals(10000, WiredTimerInputGuard.normalizeStoredMillis(null, 500, 10000));
assertEquals(10000, WiredTimerInputGuard.normalizeStoredMillis(-1, 500, 10000));
assertEquals(500, WiredTimerInputGuard.normalizeStoredMillis(500, 500, 10000));
assertEquals(WiredTimerInputGuard.MAX_TIMER_MS,
WiredTimerInputGuard.normalizeStoredMillis(Integer.MAX_VALUE, 500, 10000));
}
@Test
void shortRepeaterKeepsItsLegacyMaximum() {
assertEquals(500, WiredTimerInputGuard.fromClientUnits(Integer.MAX_VALUE, 50, 50, 500));
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredConditionFurniPayloadGuardTest {
@Test
void counterTimeNormalizesTimeComparisonQuantifierAndSources() {
WiredConditionCounterTimeMatches condition = new WiredConditionCounterTimeMatches(1, 1, null, "", 0, 0);
assertEquals(1, condition.normalizeComparison(1));
assertEquals(1, condition.normalizeComparison(-1));
assertEquals(1, condition.normalizeComparison(99));
assertEquals(0, condition.normalizeMinutes(-10));
assertEquals(99, condition.normalizeMinutes(250));
assertEquals(0, condition.normalizeHalfSecondSteps(-1));
assertEquals(119, condition.normalizeHalfSecondSteps(500));
assertEquals(1, condition.normalizeQuantifier(1));
assertEquals(0, condition.normalizeQuantifier(7));
assertEquals(WiredSourceUtil.SOURCE_SELECTED, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTED));
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(42_424));
}
@Test
void altitudeNormalizesPayloadInputs() {
WiredConditionHasAltitude condition = new WiredConditionHasAltitude(1, 1, null, "", 0, 0);
assertEquals(1, condition.normalizeComparison(1));
assertEquals(1, condition.normalizeComparison(-5));
assertEquals(1, condition.normalizeComparison(9));
assertEquals(1, condition.normalizeQuantifier(1));
assertEquals(0, condition.normalizeQuantifier(8));
assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SIGNAL));
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(-900));
assertEquals(0.0D, condition.parseAltitudeOrDefault("nope"));
assertEquals(12.35D, condition.parseAltitudeOrDefault("12.345"));
assertEquals("12.35", condition.formatAltitude(12.345D));
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.games.GameTeamColors;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredConditionInputGuardTest {
@Test
void teamColorsAreResolvedByProtocolTypeWithoutArrayIndexing() {
assertEquals(GameTeamColors.RED, WiredConditionInputGuard.normalizeTeamColorType(1, GameTeamColors.RED));
assertEquals(GameTeamColors.TEN, WiredConditionInputGuard.normalizeTeamColorType(14, GameTeamColors.RED));
assertEquals(GameTeamColors.RED, WiredConditionInputGuard.normalizeTeamColorType(-1, GameTeamColors.RED));
assertEquals(GameTeamColors.RED, WiredConditionInputGuard.normalizeTeamColorType(Integer.MAX_VALUE, GameTeamColors.RED));
}
@Test
void userSourcesFallBackToTriggerWhenUnknown() {
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredConditionInputGuard.normalizeUserSource(-100));
assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredConditionInputGuard.normalizeUserSource(WiredSourceUtil.SOURCE_SELECTOR));
assertEquals(WiredSourceUtil.SOURCE_SIGNAL, WiredConditionInputGuard.normalizeUserSource(WiredSourceUtil.SOURCE_SIGNAL));
}
@Test
void userCountRangesArePositiveOrderedAndCapped() {
assertArrayEquals(new int[]{0, 50}, WiredConditionInputGuard.normalizeUserCountRange(-50, 50));
assertArrayEquals(new int[]{10, 20}, WiredConditionInputGuard.normalizeUserCountRange(20, 10));
assertArrayEquals(new int[]{1000, 1000}, WiredConditionInputGuard.normalizeUserCountRange(Integer.MAX_VALUE, Integer.MAX_VALUE));
}
@Test
void elapsedTimerCyclesArePositiveAndBounded() {
assertEquals(0, WiredConditionInputGuard.normalizeTimerCycles(-1));
assertEquals(42, WiredConditionInputGuard.normalizeTimerCycles(42));
assertEquals(WiredConditionInputGuard.MAX_TIMER_CYCLES,
WiredConditionInputGuard.normalizeTimerCycles(Integer.MAX_VALUE));
}
}
@@ -0,0 +1,91 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredConditionTeamGameBaseTest {
private final ExposedTeamGameBase guard = new ExposedTeamGameBase();
@Test
void scoresAreNonNegativeAndCapped() {
assertEquals(0, this.guard.score(-1));
assertEquals(42, this.guard.score(42));
assertEquals(WiredConditionTeamGameBase.MAX_SCORE, this.guard.score(Integer.MAX_VALUE));
}
@Test
void userSourcesFallBackToTriggerWhenUnknown() {
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, this.guard.userSource(-1));
assertEquals(WiredSourceUtil.SOURCE_SELECTOR, this.guard.userSource(WiredSourceUtil.SOURCE_SELECTOR));
}
@Test
void teamTypesAndPlacementsStayInSupportedRange() {
assertEquals(1, this.guard.placement(-1));
assertEquals(4, this.guard.placement(4));
assertEquals(1, this.guard.explicitTeamType(-1));
assertEquals(4, this.guard.explicitTeamType(4));
}
private static class ExposedTeamGameBase extends WiredConditionTeamGameBase {
private ExposedTeamGameBase() {
super(0, 0, null, "", 0, 0);
}
@Override
public com.eu.habbo.habbohotel.wired.WiredConditionType getType() {
return com.eu.habbo.habbohotel.wired.WiredConditionType.TEAM_HAS_SCORE;
}
@Override
public boolean evaluate(com.eu.habbo.habbohotel.wired.core.WiredContext ctx) {
return false;
}
@Override
public boolean execute(com.eu.habbo.habbohotel.rooms.RoomUnit roomUnit, com.eu.habbo.habbohotel.rooms.Room room, Object[] stuff) {
return false;
}
@Override
public boolean saveData(com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings settings) {
return false;
}
@Override
public void onPickUp() {
}
@Override
public String getWiredData() {
return "";
}
@Override
public void loadWiredData(java.sql.ResultSet set, com.eu.habbo.habbohotel.rooms.Room room) {
}
@Override
public void serializeWiredData(com.eu.habbo.messages.ServerMessage message, com.eu.habbo.habbohotel.rooms.Room room) {
}
int score(int value) {
return this.normalizeScore(value);
}
int userSource(int value) {
return this.normalizeUserSource(value);
}
int placement(int value) {
return this.normalizePlacement(value);
}
int explicitTeamType(int value) {
return this.normalizeExplicitTeamType(value);
}
}
}
@@ -0,0 +1,38 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredConditionUserPayloadGuardTest {
@Test
void actorDirectionBoundsMaskSourceAndQuantifier() {
WiredConditionActorDir condition = new WiredConditionActorDir(1, 1, null, "", 0, 0);
assertEquals(255, condition.normalizeDirectionMask(-1));
assertEquals(0, condition.normalizeDirectionMask(256));
assertEquals(WiredSourceUtil.SOURCE_CLICKED_USER, condition.normalizeUserSource(WiredSourceUtil.SOURCE_CLICKED_USER));
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(123_456));
assertEquals(1, condition.normalizeQuantifier(1));
assertEquals(0, condition.normalizeQuantifier(2));
}
@Test
void triggererMatchBoundsEntitySourcesQuantifierAndUsername() {
WiredConditionTriggererMatch condition = new WiredConditionTriggererMatch(1, 1, null, "", 0, 0);
assertEquals(WiredConditionTriggererMatch.ENTITY_HABBO, condition.normalizeEntityType(999));
assertEquals(WiredConditionTriggererMatch.ENTITY_PET, condition.normalizeEntityType(WiredConditionTriggererMatch.ENTITY_PET));
assertEquals(WiredConditionTriggererMatch.AVATAR_MODE_CERTAIN, condition.normalizeAvatarMode(1));
assertEquals(WiredConditionTriggererMatch.AVATAR_MODE_ANY, condition.normalizeAvatarMode(2));
assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizePrimaryUserSource(WiredSourceUtil.SOURCE_SIGNAL));
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizePrimaryUserSource(900));
assertEquals(WiredConditionTriggererMatch.SOURCE_SPECIFIED_USERNAME, condition.normalizeCompareUserSource(WiredConditionTriggererMatch.SOURCE_SPECIFIED_USERNAME));
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeCompareUserSource(-1));
assertEquals(1, condition.normalizeQuantifier(1));
assertEquals(0, condition.normalizeQuantifier(5));
assertEquals("tester", condition.normalizeUsername(" tester "));
assertEquals(WiredConditionTriggererMatch.MAX_USERNAME_LENGTH, condition.normalizeUsername("x".repeat(200)).length());
}
}
@@ -0,0 +1,26 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WiredDateRangeInputGuardTest {
@Test
void timestampsAreNonNegative() {
assertEquals(0, WiredDateRangeInputGuard.normalizeTimestamp(-1));
assertEquals(42, WiredDateRangeInputGuard.normalizeTimestamp(42));
}
@Test
void validRangesArePreserved() {
assertArrayEquals(new int[]{100, 200}, WiredDateRangeInputGuard.normalizeRange(100, 200));
}
@Test
void negativeAndInvertedRangesBecomeInactive() {
assertArrayEquals(new int[]{0, 0}, WiredDateRangeInputGuard.normalizeRange(-10, -1));
assertArrayEquals(new int[]{0, 0}, WiredDateRangeInputGuard.normalizeRange(200, 100));
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
class WiredFurniConditionInputGuardTest {
@Test
void furniSourcesFallBackToTriggerWhenUnknown() {
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredFurniConditionInputGuard.normalizeFurniSource(-1));
assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredFurniConditionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTOR));
assertEquals(WiredSourceUtil.SOURCE_SELECTED, WiredFurniConditionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTED));
}
@Test
void selectedItemsPromoteTriggerSourceToSelected() {
assertEquals(WiredSourceUtil.SOURCE_SELECTED,
WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(WiredSourceUtil.SOURCE_TRIGGER, true));
assertEquals(WiredSourceUtil.SOURCE_SELECTOR,
WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(WiredSourceUtil.SOURCE_SELECTOR, true));
}
@Test
void itemIdsIgnoreInvalidValuesAndRespectCap() {
assertIterableEquals(Arrays.asList(4, 9),
WiredFurniConditionInputGuard.sanitizeItemIds(Arrays.asList(-1, 4, null, 9, 10), 2));
}
@Test
void legacyItemIdsIgnoreMalformedParts() {
assertIterableEquals(Arrays.asList(10, 20, 30),
WiredFurniConditionInputGuard.parseLegacyItemIds("10;bad;-1;20\t30", 5));
assertIterableEquals(Arrays.asList(10, 20),
WiredFurniConditionInputGuard.parseLegacyItemIds("10;20;30", 2));
}
}
@@ -0,0 +1,33 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class WiredMatchPositionInputGuardTest {
@Test
void furniSourcesFallBackToTriggerWhenUnknown() {
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredMatchPositionInputGuard.normalizeFurniSource(-1, false));
assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTOR, false));
assertEquals(WiredSourceUtil.SOURCE_SIGNAL, WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SIGNAL, false));
}
@Test
void selectedSettingsPromoteTriggerSourceToSelected() {
assertEquals(WiredSourceUtil.SOURCE_SELECTED,
WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_TRIGGER, true));
}
@Test
void stateIsNullSafeSingleLineAndBounded() {
assertEquals("", WiredMatchPositionInputGuard.normalizeState(null));
assertEquals("a b c", WiredMatchPositionInputGuard.normalizeState("a\tb\nc"));
String longState = "x".repeat(WiredMatchPositionInputGuard.MAX_STATE_LENGTH + 10);
String normalized = WiredMatchPositionInputGuard.normalizeState(longState);
assertEquals(WiredMatchPositionInputGuard.MAX_STATE_LENGTH, normalized.length());
assertFalse(normalized.contains("\n"));
}
}
@@ -0,0 +1,22 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class WiredUserActionInputGuardTest {
@Test
void rejectsInvalidOrFutureTimestamps() {
assertFalse(WiredUserActionInputGuard.isRecentTimestamp(0, 1000, 5000));
assertFalse(WiredUserActionInputGuard.isRecentTimestamp(1500, 1000, 5000));
assertFalse(WiredUserActionInputGuard.isRecentTimestamp(900, 1000, 0));
}
@Test
void acceptsTimestampsInsideWindowOnly() {
assertTrue(WiredUserActionInputGuard.isRecentTimestamp(900, 1000, 5000));
assertFalse(WiredUserActionInputGuard.isRecentTimestamp(100, 1000, 500));
}
}
@@ -0,0 +1,33 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class WiredUserConditionInputGuardTest {
@Test
void badgeCodesAreBoundedAndSingleLine() {
assertEquals("", WiredUserConditionInputGuard.normalizeBadgeCode(null));
assertEquals("ACH Test", WiredUserConditionInputGuard.normalizeBadgeCode(" ACH\tTest\n"));
String normalized = WiredUserConditionInputGuard.normalizeBadgeCode("x".repeat(WiredUserConditionInputGuard.MAX_BADGE_CODE_LENGTH + 10));
assertEquals(WiredUserConditionInputGuard.MAX_BADGE_CODE_LENGTH, normalized.length());
assertFalse(normalized.contains("\n"));
}
@Test
void numericIdsAreNonNegativeAndCapped() {
assertEquals(0, WiredUserConditionInputGuard.normalizeEffectId(-1));
assertEquals(WiredUserConditionInputGuard.MAX_EFFECT_ID, WiredUserConditionInputGuard.normalizeEffectId(Integer.MAX_VALUE));
assertEquals(0, WiredUserConditionInputGuard.normalizeHandItemId(-1));
assertEquals(WiredUserConditionInputGuard.MAX_HAND_ITEM_ID, WiredUserConditionInputGuard.normalizeHandItemId(Integer.MAX_VALUE));
}
@Test
void unknownUserSourcesFallBackToTrigger() {
assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredUserConditionInputGuard.normalizeUserSource(-1));
assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredUserConditionInputGuard.normalizeUserSource(WiredSourceUtil.SOURCE_SELECTOR));
}
}
@@ -0,0 +1,21 @@
package com.eu.habbo.habbohotel.permissions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PermissionSettingContractTest {
@Test
void unknownPermissionValuesFailClosed() {
assertEquals(PermissionSetting.DISALLOWED, PermissionSetting.fromString(null));
assertEquals(PermissionSetting.DISALLOWED, PermissionSetting.fromString(""));
assertEquals(PermissionSetting.DISALLOWED, PermissionSetting.fromString("999"));
}
@Test
void knownPermissionValuesMapToExplicitSettings() {
assertEquals(PermissionSetting.ALLOWED, PermissionSetting.fromString("1"));
assertEquals(PermissionSetting.ROOM_OWNER, PermissionSetting.fromString("2"));
}
}
@@ -0,0 +1,35 @@
package com.eu.habbo.habbohotel.permissions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RankPermissionContractTest {
@Test
void missingPermissionsFailClosed() {
Rank rank = new Rank(1);
assertFalse(rank.hasPermission(null, false));
assertFalse(rank.hasPermission("", false));
assertFalse(rank.hasPermission("acc_supporttool", false));
}
@Test
void roomOwnerPermissionOnlyPassesWithRoomRights() {
Rank rank = new Rank(1);
rank.setPermission("acc_placefurni", PermissionSetting.ROOM_OWNER);
assertFalse(rank.hasPermission("acc_placefurni", false));
assertTrue(rank.hasPermission("acc_placefurni", true));
}
@Test
void allowedPermissionPassesWithoutRoomRights() {
Rank rank = new Rank(1);
rank.setPermission("acc_supporttool", PermissionSetting.ALLOWED);
assertTrue(rank.hasPermission("acc_supporttool", false));
}
}
@@ -0,0 +1,55 @@
package com.eu.habbo.messages.incoming.catalog;
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 CatalogSearchOfferIdContractTest {
private static String source(String path) throws Exception {
return Files.readString(Path.of(path));
}
@Test
void catalogItemsExposeStableSearchOfferIdWhenDatabaseOfferIdIsMissing() throws Exception {
String source = source("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java");
int method = source.indexOf("public int getSearchOfferId()");
int rawGuard = source.indexOf("this.offerId > 0", method);
int fallback = source.indexOf("return haveOffer(this) ? this.id : -1", rawGuard);
assertTrue(method > -1, "CatalogItem should expose a search-safe offer id");
assertTrue(rawGuard > method, "CatalogItem should preserve valid positive database offer ids");
assertTrue(fallback > rawGuard,
"CatalogItem should fall back to catalog item id when offer_id is missing but the item can be offered");
}
@Test
void catalogManagerIndexesSearchOfferIdsInsteadOfRawOfferIds() throws Exception {
String source = source("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java");
int searchOffer = source.indexOf("int searchOfferId = item.getSearchOfferId()");
int addOffer = source.indexOf("page.addOfferId(searchOfferId)", searchOffer);
int offerDefs = source.indexOf("this.offerDefs.put(searchOfferId, item.getId())", addOffer);
assertTrue(searchOffer > -1, "CatalogManager should calculate the runtime search offer id");
assertTrue(addOffer > searchOffer, "CatalogManager should expose runtime search offer ids in catalog pages");
assertTrue(offerDefs > addOffer, "CatalogManager should map runtime search offer ids back to catalog items");
assertTrue(!source.contains("this.offerDefs.put(item.getOfferId(), item.getId())"),
"CatalogManager must not index raw -1 offer ids for catalog search");
}
@Test
void catalogSearchLookupResolvesCatalogItemIdsAndComparesSearchOfferIds() throws Exception {
String source = source("src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchedItemEvent.java");
assertTrue(source.contains("int catalogItemId ="),
"Catalog search lookup should name offerDefs values as catalog item ids");
assertTrue(source.contains("getCatalogItem(catalogItemId)"),
"Catalog search should resolve the mapped catalog item directly");
assertTrue(source.contains("item.getSearchOfferId() == offerId"),
"Catalog search should compare runtime search offer ids, not raw database offer ids");
}
}
@@ -0,0 +1,50 @@
package com.eu.habbo.messages.incoming.catalog.marketplace;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
class MarketplaceInputContractTest {
@Test
void marketplaceIdHandlersRejectNonPositiveIds() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace");
for (String handler : List.of(
"BuyItemEvent.java",
"RequestItemInfoEvent.java",
"SellItemEvent.java",
"TakeBackItemEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("MarketplaceInputGuard.isPositiveId"),
handler + " must reject zero or negative ids before marketplace or inventory lookup");
}
}
@Test
void offerSearchNormalizesCacheKeyInputs() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestOffersEvent.java"));
assertTrue(source.contains("MarketplaceInputGuard.normalizeMinPrice"),
"marketplace offer search must normalize minimum price");
assertTrue(source.contains("MarketplaceInputGuard.normalizeMaxPrice"),
"marketplace offer search must normalize maximum price");
assertTrue(source.contains("MarketplaceInputGuard.normalizeSearch"),
"marketplace offer search must trim and bound search text");
assertTrue(source.contains("MarketplaceInputGuard.normalizeSort"),
"marketplace offer search must normalize sort before using it as a cache key");
}
@Test
void takeBackDoesNotFirePluginEventForMissingOffer() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java"));
assertTrue(source.contains("if (offer == null)"),
"takeBackItem must ignore missing offers before constructing plugin events");
}
}
@@ -0,0 +1,42 @@
package com.eu.habbo.messages.incoming.catalog.marketplace;
import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace;
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 MarketplaceInputGuardTest {
@Test
void idsMustBePositive() {
assertFalse(MarketplaceInputGuard.isPositiveId(0));
assertFalse(MarketplaceInputGuard.isPositiveId(-1));
assertTrue(MarketplaceInputGuard.isPositiveId(1));
}
@Test
void searchIsTrimmedAndBounded() {
assertEquals("", MarketplaceInputGuard.normalizeSearch(null));
assertEquals("rare", MarketplaceInputGuard.normalizeSearch(" rare "));
assertEquals(MarketplaceInputGuard.MAX_SEARCH_LENGTH, MarketplaceInputGuard.normalizeSearch("a".repeat(80)).length());
}
@Test
void sortFallsBackToDefaultOutsideKnownRange() {
assertEquals(MarketplaceInputGuard.DEFAULT_SORT, MarketplaceInputGuard.normalizeSort(0));
assertEquals(3, MarketplaceInputGuard.normalizeSort(3));
assertEquals(MarketplaceInputGuard.DEFAULT_SORT, MarketplaceInputGuard.normalizeSort(7));
}
@Test
void priceRangesPreserveCacheSentinelAndStayBounded() {
assertEquals(-1, MarketplaceInputGuard.normalizeMinPrice(-1));
assertEquals(0, MarketplaceInputGuard.normalizeMinPrice(-100));
assertEquals(MarketPlace.MAXIMUM_LISTING_PRICE, MarketplaceInputGuard.normalizeMinPrice(Integer.MAX_VALUE));
assertEquals(-1, MarketplaceInputGuard.normalizeMaxPrice(-1, -1));
assertEquals(500, MarketplaceInputGuard.normalizeMaxPrice(100, 500));
assertEquals(MarketPlace.MAXIMUM_LISTING_PRICE, MarketplaceInputGuard.normalizeMaxPrice(Integer.MAX_VALUE, 0));
}
}
@@ -0,0 +1,107 @@
package com.eu.habbo.messages.incoming.friends;
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 FriendBatchGuardContractTest {
private static String source(String name) throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/friends/" + name + ".java"));
}
@Test
void declineFriendRequestsBoundsClientSuppliedBatchCount() throws Exception {
String source = source("DeclineFriendRequestEvent");
int count = source.indexOf("int count = this.packet.readInt()");
int guard = source.indexOf("count <= 0 || count > MAX_BATCH_SIZE", count);
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
int delete = source.indexOf("deleteFriendRequests", loop);
assertTrue(source.contains("MAX_BATCH_SIZE = 100"),
"Friend request decline batches should have a conservative cap");
assertTrue(count > -1, "DeclineFriendRequestEvent must read the client supplied count");
assertTrue(guard > count, "DeclineFriendRequestEvent must validate the count after reading it");
assertTrue(guard < loop, "DeclineFriendRequestEvent must validate the count before looping");
assertTrue(loop < delete, "DeclineFriendRequestEvent should only mutate after the bounded loop starts");
}
@Test
void removeFriendsBoundsClientSuppliedBatchCountBeforeMutations() throws Exception {
String source = source("RemoveFriendEvent");
int count = source.indexOf("int count = this.packet.readInt()");
int guard = source.indexOf("count <= 0 || count > MAX_BATCH_SIZE", count);
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
int idGuard = source.indexOf("habboId <= 0", loop);
int unfriend = source.indexOf("Messenger.unfriend", loop);
assertTrue(source.contains("MAX_BATCH_SIZE = 100"),
"Friend removal batches should have a conservative cap");
assertTrue(count > -1, "RemoveFriendEvent must read the client supplied count");
assertTrue(guard > count, "RemoveFriendEvent must validate the count after reading it");
assertTrue(guard < loop, "RemoveFriendEvent must validate the count before looping");
assertTrue(idGuard > loop && idGuard < unfriend,
"RemoveFriendEvent must skip invalid ids before mutating friendships");
}
@Test
void acceptFriendRequestsBoundsClientSuppliedBatchCountBeforeLoadingTargets() throws Exception {
String source = source("AcceptFriendRequestEvent");
int count = source.indexOf("int count = this.packet.readInt()");
int guard = source.indexOf("count <= 0 || count > 100", count);
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
int idGuard = source.indexOf("userId <= 0", loop);
int loadTarget = source.indexOf("getHabbo(userId)", loop);
assertTrue(count > -1, "AcceptFriendRequestEvent must read the client supplied count");
assertTrue(guard > count && guard < loop,
"AcceptFriendRequestEvent must validate the count before looping");
assertTrue(idGuard > loop && idGuard < loadTarget,
"AcceptFriendRequestEvent must skip invalid ids before loading targets");
}
@Test
void friendRequestAndMessagesUseSharedInputGuards() throws Exception {
String guard = source("FriendInputGuard");
String request = source("FriendRequestEvent");
String privateMessage = source("FriendPrivateMessageEvent");
String invite = source("InviteFriendsEvent");
assertTrue(guard.contains("MAX_USERNAME_LENGTH = 15"),
"Friend request usernames should keep the Habbo username length bound");
assertTrue(guard.contains("MAX_MESSAGE_LENGTH = 255"),
"Messenger payloads should keep the client message length bound");
assertTrue(request.contains("FriendInputGuard.normalizeUsername"),
"Friend requests should normalize usernames before lookup");
assertTrue(request.contains("FriendInputGuard.isValidUsername"),
"Friend requests should reject empty or oversized usernames before DB lookup");
assertTrue(request.contains("Messenger.friendRequested(targetId, this.client.getHabbo().getHabboInfo().getId())"),
"Friend requests should reject duplicate outgoing requests");
assertTrue(privateMessage.contains("FriendInputGuard.normalizeMessage"),
"Private messages should be normalized and capped before plugin dispatch");
assertTrue(invite.contains("FriendInputGuard.normalizeMessage"),
"Room invites should be normalized and capped before fan-out");
}
@Test
void relationshipChangesFirePluginEventAndValidatePluginMutation() throws Exception {
String source = source("ChangeRelationEvent");
int event = source.indexOf("new UserRelationShipEvent");
int fire = source.indexOf("Emulator.getPluginManager().fireEvent(event)", event);
int pluginGuard = source.indexOf("FriendInputGuard.isValidRelation(event.relationShip)", fire);
int setRelation = source.indexOf("buddy.setRelation(event.relationShip)", pluginGuard);
assertTrue(source.contains("FriendInputGuard.isValidRelation(relationId)"),
"Relationship changes should reject invalid client relation ids");
assertTrue(event > -1 && fire > event,
"Relationship changes should dispatch the plugin event before applying changes");
assertTrue(pluginGuard > fire && pluginGuard < setRelation,
"Relationship changes should reject invalid plugin-mutated relation ids");
}
}
@@ -0,0 +1,59 @@
package com.eu.habbo.messages.incoming.guilds;
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 GuildManagementInputGuardContractTest {
private static String source(String file) throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/" + file));
}
@Test
void guildCreateAndRenameShareNameAndDescriptionBounds() throws Exception {
String limits = source("GuildInputLimits.java");
String buy = source("RequestGuildBuyEvent.java");
String rename = source("GuildChangeNameDescEvent.java");
assertTrue(limits.contains("MAX_GUILD_NAME_LENGTH = 29"),
"Guild names should keep the existing 29 character protocol bound");
assertTrue(limits.contains("MAX_GUILD_DESCRIPTION_LENGTH = 254"),
"Guild descriptions should keep the existing database/protocol bound");
assertTrue(limits.contains("!name.isBlank()"),
"Guild names must not be empty or whitespace-only");
assertTrue(buy.contains("GuildInputLimits.isValidGuildName(name)"),
"Guild purchase should use the shared name guard");
assertTrue(buy.contains("GuildInputLimits.isValidGuildDescription(description)"),
"Guild purchase should use the shared description guard");
assertTrue(rename.contains("GuildInputLimits.isValidGuildName(newName)"),
"Guild rename should reject invalid client names before plugin events");
assertTrue(rename.contains("GuildInputLimits.isValidGuildName(nameEvent.name)"),
"Guild rename should reject invalid plugin-mutated names before persistence");
}
@Test
void guildColorInputsAreCheckedAgainstLoadedPalette() throws Exception {
String buy = source("RequestGuildBuyEvent.java");
String colors = source("GuildChangeColorsEvent.java");
assertTrue(buy.contains("symbolColor(colorOne)") && buy.contains("backgroundColor(colorTwo)"),
"Guild purchase should reject color ids that are not in the loaded guild palette");
assertTrue(colors.contains("symbolColor(colorOne)") && colors.contains("backgroundColor(colorTwo)"),
"Guild color changes should reject invalid client color ids");
assertTrue(colors.contains("symbolColor(colorsEvent.colorOne)") && colors.contains("backgroundColor(colorsEvent.colorTwo)"),
"Guild color changes should reject invalid plugin-mutated color ids");
}
@Test
void guildStateInputsStayInsideKnownEnumRange() throws Exception {
String settings = source("GuildChangeSettingsEvent.java");
assertTrue(settings.contains("state < 0 || state >= GuildState.values().length"),
"Guild settings should reject invalid client state ids before event dispatch");
assertTrue(settings.contains("settingsEvent.state < 0 || settingsEvent.state >= GuildState.values().length"),
"Guild settings should reject invalid plugin-mutated state ids before applying them");
}
}
@@ -0,0 +1,55 @@
package com.eu.habbo.messages.incoming.guilds;
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 GuildMembersInputGuardContractTest {
private static String eventSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildMembersEvent.java"));
}
private static String managerSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java"));
}
@Test
void guildMemberListInputsAreBoundedBeforeManagerQueries() throws Exception {
String source = eventSource();
int pageRead = source.indexOf("int pageId = this.packet.readInt()");
int queryRead = source.indexOf("String query = this.packet.readString()", pageRead);
int levelRead = source.indexOf("int levelId = this.packet.readInt()", queryRead);
int guard = source.indexOf("pageId < 0 || pageId > MAX_PAGE_ID", levelRead);
int managerCall = source.indexOf("getGuildMembers(g, pageId, levelId, query)", guard);
assertTrue(source.contains("MAX_PAGE_ID = 1000"),
"Guild member pagination should have a server-side upper bound");
assertTrue(source.contains("MAX_QUERY_LENGTH = 32"),
"Guild member search query should have a server-side length bound");
assertTrue(source.contains("MAX_LEVEL_ID = 2"),
"Guild member rank filter should be bounded to known levels");
assertTrue(pageRead > -1 && queryRead > pageRead && levelRead > queryRead,
"Guild member handler must read page/query/level from the packet");
assertTrue(guard > levelRead && guard < managerCall,
"Guild member handler must validate inputs before querying the manager");
}
@Test
void guildMemberCountEscapesLikeQuerySameAsListQuery() throws Exception {
String source = managerSource();
int listMethod = source.indexOf("public ArrayList<GuildMember> getGuildMembers(Guild guild, int page, int levelId, String query)");
int listEscape = source.indexOf("SqlLikeEscaper.escape(query)", listMethod);
int countMethod = source.indexOf("public int getGuildMembersCount(Guild guild, int page, int levelId, String query)");
int countEscape = source.indexOf("SqlLikeEscaper.escape(query)", countMethod);
assertTrue(listEscape > listMethod,
"Guild member list query should escape SQL LIKE wildcards");
assertTrue(countEscape > countMethod,
"Guild member count query should escape SQL LIKE wildcards too");
}
}
@@ -15,6 +15,7 @@ class GuildForumInputGuardContractTest {
for (String handler : List.of(
"GuildForumPostThreadEvent.java",
"GuildForumDataEvent.java",
"GuildForumModerateMessageEvent.java",
"GuildForumModerateThreadEvent.java",
"GuildForumThreadUpdateEvent.java",
@@ -39,9 +40,12 @@ class GuildForumInputGuardContractTest {
String settings = Files.readString(base.resolve("GuildForumUpdateSettingsEvent.java"));
String moderateThread = Files.readString(base.resolve("GuildForumModerateThreadEvent.java"));
String moderateMessage = Files.readString(base.resolve("GuildForumModerateMessageEvent.java"));
String threads = Files.readString(base.resolve("GuildForumThreadsEvent.java"));
assertTrue(messages.contains("GuildForumInputGuard.isValidPage(index, limit)"),
"thread message reads must bound index/limit before fetching comments");
assertTrue(threads.contains("GuildForumInputGuard.isValidThreadIndex(index)"),
"thread list reads must bound the client-provided index before composing results");
assertTrue(markRead.contains("GuildForumInputGuard.isValidMarkReadBatch(count)"),
"mark-as-read must bound the client-provided batch count before DB writes");
assertTrue(settings.contains("GuildForumInputGuard.isSettingsState"),
@@ -59,4 +63,16 @@ class GuildForumInputGuardContractTest {
assertTrue(source.contains("GuildForumInputGuard.normalize(this.packet.readString())"),
"forum post subject and body should be normalized before word filtering and length checks");
}
@Test
void markAsReadRequiresForumReadAccessBeforeWritingViews() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java"));
int guildLookup = source.indexOf("Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId)");
int readGuard = source.indexOf("guild.canHabboReadForum(userId, member, staff)");
int insert = source.indexOf("INSERT INTO `guild_forum_views`");
assertTrue(guildLookup > -1 && readGuard > guildLookup && readGuard < insert,
"mark-as-read should confirm the user can read the forum before inserting view rows");
}
}
@@ -21,6 +21,8 @@ class GuildForumInputGuardTest {
assertFalse(GuildForumInputGuard.isValidPage(0, 0));
assertTrue(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT));
assertFalse(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT + 1));
assertTrue(GuildForumInputGuard.isValidThreadIndex(GuildForumInputGuard.MAX_THREAD_INDEX));
assertFalse(GuildForumInputGuard.isValidThreadIndex(GuildForumInputGuard.MAX_THREAD_INDEX + 1));
}
@Test
@@ -0,0 +1,28 @@
package com.eu.habbo.messages.incoming.handshake;
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 SecureLoginGuardContractTest {
private static String source() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java"));
}
@Test
void websocketSsoTicketIsLengthBoundedBeforeDatabaseLookup() throws Exception {
String source = source();
int maxConstant = source.indexOf("MAX_SSO_TICKET_LENGTH = 128");
int guard = source.indexOf("sso.isEmpty() || sso.length() > MAX_SSO_TICKET_LENGTH");
int lookup = source.indexOf("SELECT id FROM users WHERE auth_ticket = ?");
assertTrue(maxConstant > -1, "Secure login should define the same SSO length cap used by HTTP auth");
assertTrue(guard > -1, "Secure login must reject missing or oversized SSO tickets");
assertTrue(guard < lookup, "SSO length must be validated before database lookup");
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.handshake;
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 SecureLoginInputGuardTest {
@Test
void normalizesNullAndSpacesBeforeAuthentication() {
assertEquals("", SecureLoginInputGuard.normalizeSsoTicket(null));
assertEquals("abc123", SecureLoginInputGuard.normalizeSsoTicket(" abc 123 "));
}
@Test
void rejectsMissingOrOversizedTickets() {
assertFalse(SecureLoginInputGuard.isValidSsoTicket(""));
assertFalse(SecureLoginInputGuard.isValidSsoTicket("x".repeat(SecureLoginInputGuard.MAX_SSO_TICKET_LENGTH + 1)));
}
@Test
void acceptsTicketWithinBound() {
assertTrue(SecureLoginInputGuard.isValidSsoTicket("x".repeat(SecureLoginInputGuard.MAX_SSO_TICKET_LENGTH)));
}
}
@@ -109,4 +109,28 @@ class ModToolPermissionContractTest {
handler + " must reject empty or oversized staff-supplied text");
}
}
@Test
void staffSuppliedModToolTargetsArePositiveBeforeLookupOrMutation() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
for (String handler : List.of(
"ModToolAlertEvent.java",
"ModToolWarnEvent.java",
"ModToolKickEvent.java",
"ModToolChangeRoomSettingsEvent.java",
"ModToolRequestRoomInfoEvent.java",
"ModToolRequestRoomVisitsEvent.java",
"ModToolIssueDefaultSanctionEvent.java",
"ModToolSanctionAlertEvent.java",
"ModToolSanctionBanEvent.java",
"ModToolSanctionMuteEvent.java",
"ModToolSanctionTradeLockEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("ModToolTicketGuard.isPositiveId"),
handler + " must reject zero or negative client-provided ids before manager/database lookups");
}
}
}
@@ -0,0 +1,21 @@
package com.eu.habbo.messages.incoming.navigator;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class NavigatorInputGuardTest {
@Test
void searchValuesAreTrimmedAndBounded() {
assertEquals("", NavigatorInputGuard.normalizeSearch(null));
assertEquals("rare", NavigatorInputGuard.normalizeSearch(" rare "));
assertEquals(NavigatorInputGuard.MAX_SEARCH_LENGTH, NavigatorInputGuard.normalizeSearch("a".repeat(100)).length());
}
@Test
void savedSearchValuesUseLargerBound() {
assertEquals("", NavigatorInputGuard.normalizeSavedSearchValue(null));
assertEquals("owner:duckie", NavigatorInputGuard.normalizeSavedSearchValue(" owner:duckie "));
assertEquals(NavigatorInputGuard.MAX_SAVED_SEARCH_LENGTH, NavigatorInputGuard.normalizeSavedSearchValue("a".repeat(400)).length());
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.incoming.navigator;
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 NavigatorSearchInputContractTest {
@Test
void classicSearchNormalizesInputAndPassesUnprefixedQueriesToManagers() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsEvent.java"));
assertTrue(source.contains("NavigatorInputGuard.normalizeSearch(this.packet.readString())"),
"classic room search must normalize raw client text before cache or manager lookups");
assertTrue(source.contains("getRoomsForHabbo(query)"),
"owner search must pass only the unprefixed owner query");
assertTrue(source.contains("getRoomsWithTag(query)"),
"tag search must pass only the unprefixed tag query");
assertTrue(source.contains("getGroupRoomsWithName(query)"),
"group search must pass only the unprefixed group query");
assertTrue(source.contains("buildCacheKey(prefix, query)"),
"classic room search must cache using normalized prefix/query pairs");
}
@Test
void savedAndTagSearchesNormalizeText() throws Exception {
String saved = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java"));
String tag = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsByTagEvent.java"));
assertTrue(saved.contains("NavigatorInputGuard.normalizeSavedSearchValue"),
"saved searches must trim and bound search code/filter values");
assertTrue(tag.contains("NavigatorInputGuard.normalizeSearch"),
"tag search must trim and bound tag values");
}
}
@@ -0,0 +1,53 @@
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 PollAnswerInputGuardContractTest {
private static String source() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/polls/AnswerPollEvent.java"));
}
@Test
void pollAnswerCountAndPartLengthAreBoundedBeforeBuildingCombinedAnswer() throws Exception {
String source = source();
int count = source.indexOf("int count = this.packet.readInt()");
int answers = source.indexOf("String answers = this.packet.readString()", count);
int guard = source.indexOf("count <= 0 || count > MAX_ANSWER_COUNT", answers);
int builder = source.indexOf("StringBuilder answer = new StringBuilder()", guard);
int loop = source.indexOf("for (int i = 0; i < count; i++)", builder);
assertTrue(source.contains("MAX_ANSWER_COUNT = 20"),
"Poll answers should have a bounded answer count");
assertTrue(source.contains("MAX_ANSWER_PART_LENGTH = 255"),
"Poll answer fragments should have a bounded length");
assertTrue(source.contains("MAX_COMBINED_ANSWER_LENGTH = 2048"),
"Poll combined answer should have a bounded final length");
assertTrue(count > -1 && answers > count, "Poll handler must read count and answer string");
assertTrue(guard > answers, "Poll handler must validate count and answer string after reading them");
assertTrue(guard < builder && builder < loop,
"Poll handler must validate inputs before building the repeated answer string");
}
@Test
void combinedAnswerLengthIsCheckedBeforeWordQuizOrDatabaseWrite() throws Exception {
String source = source();
int append = source.indexOf("answer.append(\":\").append(answers)");
int combinedGuard = source.indexOf("answer.length() > MAX_COMBINED_ANSWER_LENGTH", append);
int wordQuiz = source.indexOf("handleWordQuiz", combinedGuard);
int dbWrite = source.indexOf("INSERT INTO polls_answers", combinedGuard);
assertTrue(combinedGuard > append,
"Poll handler must check combined answer length while building it");
assertTrue(combinedGuard < wordQuiz,
"Poll handler must bound word quiz answers before dispatching them");
assertTrue(combinedGuard < dbWrite,
"Poll handler must bound poll answers before persisting them");
}
}
@@ -0,0 +1,60 @@
package com.eu.habbo.messages.incoming.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 RoomSettingsInputGuardContractTest {
private static String source() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java"));
}
@Test
void roomStateAndTagCountAreValidatedWithoutModuloOrTruncation() throws Exception {
String source = source();
int stateRead = source.indexOf("int stateId = this.packet.readInt()");
int stateGuard = source.indexOf("stateId < 0 || stateId >= RoomState.values().length", stateRead);
int stateAssign = source.indexOf("RoomState state = RoomState.values()[stateId]", stateGuard);
int tagCount = source.indexOf("int count = this.packet.readInt()", stateAssign);
int tagGuard = source.indexOf("count < 0 || count > MAX_TAGS", tagCount);
int tagLoop = source.indexOf("for (int i = 0; i < count; i++)", tagGuard);
assertTrue(source.contains("MAX_TAGS = 2"), "Room settings tag count should have an explicit cap");
assertTrue(!source.contains("% RoomState.values().length"),
"Room state must not use modulo because negative values can crash or remap input");
assertTrue(!source.contains("Math.min(this.packet.readInt(), 2)"),
"Room settings must reject oversized tag counts instead of truncating and desynchronizing the packet");
assertTrue(stateRead > -1 && stateGuard > stateRead && stateAssign > stateGuard,
"Room state must be range-checked before indexing RoomState.values()");
assertTrue(tagCount > -1 && tagGuard > tagCount && tagLoop > tagGuard,
"Tag count must be range-checked before reading tag strings");
}
@Test
void roomSettingsOptionsAreValidatedBeforeMutatingRoom() throws Exception {
String source = source();
int tradeMode = source.indexOf("int tradeMode = this.packet.readInt()");
int validation = source.indexOf("!isInRange(tradeMode, 0, MAX_OPTION_LEVEL)", tradeMode);
int setTags = source.indexOf("room.setTags(tags.toString())", validation);
int setChatDistance = source.indexOf("room.setChatDistance(chatDistance)", setTags);
assertTrue(source.contains("MAX_ROOM_PASSWORD_LENGTH = 64"),
"Room password should have a bounded server-side length");
assertTrue(source.contains("MAX_USERS_MAX = 200"),
"Room capacity should have a bounded server-side maximum");
assertTrue(source.contains("MIN_CHAT_DISTANCE = 1") && source.contains("MAX_CHAT_DISTANCE = 99"),
"Room chat distance should be explicitly bounded");
assertTrue(!source.contains("Math.abs(this.packet.readInt())"),
"Room settings must reject invalid chat distance instead of converting negative values");
assertTrue(validation > tradeMode, "Room options must be validated after reading them");
assertTrue(validation < setTags, "Room options must be validated before mutating room fields");
assertTrue(setChatDistance > setTags, "Validated chat distance should be applied after the guard block");
assertTrue(source.contains("private static boolean isInRange"),
"Room settings should use one clear range helper for numeric option guards");
}
}
@@ -0,0 +1,154 @@
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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RoomItemInputGuardContractTest {
private static String source(String name) throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/" + name + ".java"));
}
@Test
void itemMutationHandlersRejectInvalidIdsBeforeRoomLookups() throws Exception {
for (String handler : new String[]{"RoomPickupItemEvent", "RotateMoveItemEvent", "UpdateFurniturePositionEvent", "MoveWallItemEvent", "ToggleFloorItemEvent", "ToggleWallItemEvent", "AdvertisingSaveEvent", "MannequinSaveNameEvent", "MannequinSaveLookEvent", "FootballGateSaveLookEvent", "PostItSaveDataEvent", "PostItPlaceEvent", "PostItDeleteEvent"}) {
String source = source(handler);
int idRead = source.indexOf("this.packet.readInt()");
int guard = source.indexOf("RoomItemInputGuard.isPositiveId", idRead);
int lookup = source.indexOf("getHabboItem", guard);
assertTrue(guard > idRead, handler + " should validate item ids after reading them");
assertTrue(lookup == -1 || guard < lookup, handler + " should validate item ids before room item lookups");
}
}
@Test
void specialItemHandlersRejectInvalidIdsBeforeLookups() throws Exception {
for (String handler : new String[]{
"rentablespace/RentSpaceEvent",
"rentablespace/RentSpaceCancelEvent",
"lovelock/LoveLockStartConfirmEvent",
"youtube/YoutubeRequestPlaylistChange",
"youtube/YoutubeRequestPlaylists",
"youtube/YoutubeRequestStateChange",
"jukebox/JukeBoxAddSoundTrackEvent",
"RedeemItemEvent",
"RedeemClothingEvent"
}) {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/" + handler + ".java"));
int idRead = source.indexOf("this.packet.readInt()");
int guard = source.indexOf("RoomItemInputGuard.isPositiveId", idRead);
assertTrue(guard > idRead, handler + " should validate item ids after reading them");
}
}
@Test
void roomPlacementParsesClientPayloadSafely() throws Exception {
String source = source("RoomPlaceItemEvent");
assertTrue(source.contains("RoomItemInputGuard.parseInt(values[0])"),
"item placement should parse item id without throwing on malformed packets");
assertTrue(source.contains("values.length < 4"),
"item placement should require complete coordinate payloads");
assertTrue(source.contains("RoomItemInputGuard.parseShort(values[1])"),
"floor placement should parse x coordinate safely");
assertTrue(source.contains("RoomItemInputGuard.parseShort(values[2])"),
"floor placement should parse y coordinate safely");
assertTrue(source.contains("RoomItemInputGuard.parseInt(values[3])"),
"floor placement should parse rotation safely");
}
@Test
void advertisingCustomValuesAreBoundedBeforeMutation() throws Exception {
String source = source("AdvertisingSaveEvent");
int count = source.indexOf("int count = this.packet.readInt()");
int guard = source.indexOf("RoomItemInputGuard.isValidCustomValueCount(count)", count);
int loop = source.indexOf("for (int i = 0; i < count / 2; i++)", guard);
int mutate = source.indexOf(".values.put(key, value)", loop);
assertTrue(guard > count && guard < loop,
"custom value pair count should be bounded before reading key/value pairs");
assertTrue(source.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_KEY_LENGTH)"),
"custom value keys should be trimmed and capped");
assertTrue(source.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_VALUE_LENGTH)"),
"custom value values should be trimmed and capped");
assertTrue(mutate > loop,
"custom values should only mutate after bounded reads");
}
@Test
void stickyPoleMultiCommandPayloadIsBounded() throws Exception {
String source = source("SavePostItStickyPoleEvent");
int split = source.indexOf("String[] commands = this.packet.readString().split");
int countGuard = source.indexOf("commands.length > RoomItemInputGuard.MAX_STICKY_POLE_COMMANDS", split);
int trim = source.indexOf("RoomItemInputGuard.trimToMax(command.replace", countGuard);
int execute = source.indexOf("CommandHandler.handleCommand", trim);
assertTrue(split > -1 && countGuard > split,
"sticky-pole multi-command packets should cap command count before looping");
assertTrue(trim > countGuard && trim < execute,
"sticky-pole multi-command packets should cap each command before execution");
}
@Test
void specialLookPayloadsAreValidatedBeforeMutation() throws Exception {
String football = source("FootballGateSaveLookEvent");
String mannequinName = source("MannequinSaveNameEvent");
String moodlight = source("MoodLightSaveSettingsEvent");
assertTrue(football.contains("RoomItemInputGuard.isValidGender(gender)"),
"football gates should reject unknown gender keys instead of defaulting to male");
assertTrue(football.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_LOOK_LENGTH)"),
"football gate looks should be capped before persistence");
assertTrue(mannequinName.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), 32)"),
"mannequin names should be capped before extradata persistence");
assertTrue(moodlight.contains("if (room == null)"),
"moodlight saves should null-check current room before inspecting rights");
}
@Test
void youtubeAndJukeboxInputsAreBoundedBeforeListAccess() throws Exception {
String playlistChange = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java"));
String jukeboxRemove = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRemoveSoundTrackEvent.java"));
String jukeboxRequest = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRequestPlayListEvent.java"));
int playlistTrim = playlistChange.indexOf("RoomItemInputGuard.trimToMax");
int emptyVideos = playlistChange.indexOf("playlist.get().getVideos().isEmpty()");
int getFirst = playlistChange.indexOf("playlist.get().getVideos().get(0)");
assertTrue(playlistTrim > -1,
"youtube playlist ids should be capped before lookup");
assertTrue(emptyVideos > playlistTrim && emptyVideos < getFirst,
"youtube playlist changes should reject empty playlists before get(0)");
assertTrue(jukeboxRemove.contains("index < 0 || index >= room.getTraxManager().getSongs().size()"),
"jukebox remove should bound client-provided indexes before list access");
assertTrue(jukeboxRequest.contains("if (room == null)"),
"jukebox playlist requests should null-check current room");
}
@Test
void helperRejectsMalformedValues() {
assertFalse(RoomItemInputGuard.isPositiveId(0));
assertTrue(RoomItemInputGuard.isPositiveId(1));
assertFalse(RoomItemInputGuard.isValidCustomValueCount(0));
assertFalse(RoomItemInputGuard.isValidCustomValueCount(3));
assertTrue(RoomItemInputGuard.isValidCustomValueCount(RoomItemInputGuard.MAX_CUSTOM_VALUE_PAIRS * 2));
assertFalse(RoomItemInputGuard.isValidCustomValueCount((RoomItemInputGuard.MAX_CUSTOM_VALUE_PAIRS + 1) * 2));
assertEquals(123, RoomItemInputGuard.parseInt("123"));
assertNull(RoomItemInputGuard.parseInt("abc"));
assertEquals((short) 12, RoomItemInputGuard.parseShort("12"));
assertNull(RoomItemInputGuard.parseShort("40000"));
assertTrue(RoomItemInputGuard.isValidGender("m"));
assertTrue(RoomItemInputGuard.isValidGender("F"));
assertFalse(RoomItemInputGuard.isValidGender("x"));
}
}
@@ -0,0 +1,91 @@
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RoomUserInputGuardContractTest {
private static String source(String name) throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/users/" + name + ".java"));
}
@Test
void roomModerationHandlersRejectInvalidUserAndRoomIds() throws Exception {
for (String handler : new String[]{"RoomUserBanEvent", "UnbanRoomUserEvent", "RoomUserMuteEvent"}) {
String source = source(handler);
int userRead = source.indexOf("int userId = this.packet.readInt()");
int roomRead = source.indexOf("int roomId = this.packet.readInt()", userRead);
int guard = source.indexOf("RoomUserInputGuard.isPositiveId(userId)", roomRead);
int roomLookup = source.indexOf("getCurrentRoom()", guard);
assertTrue(userRead > -1 && roomRead > userRead, handler + " should read user and room ids");
assertTrue(guard > roomRead && guard < roomLookup,
handler + " should reject invalid ids before resolving room state");
}
}
@Test
void rightsMutationHandlersRejectInvalidUserIds() throws Exception {
String giveRights = source("RoomUserGiveRightsEvent");
String removeRights = source("RoomUserRemoveRightsEvent");
int giveRead = giveRights.indexOf("int userId = this.packet.readInt()");
int giveGuard = giveRights.indexOf("RoomUserInputGuard.isPositiveId(userId)", giveRead);
int giveTarget = giveRights.indexOf("room.getHabbo(userId)", giveGuard);
int removeRead = removeRights.indexOf("int userId = this.packet.readInt()");
int removeGuard = removeRights.indexOf("RoomUserInputGuard.isPositiveId(userId)", removeRead);
int removeCall = removeRights.indexOf("room.removeRights(userId)", removeGuard);
assertTrue(giveGuard > giveRead && giveGuard < giveTarget,
"give-rights should validate target id before online/friend lookups");
assertTrue(removeGuard > removeRead && removeGuard < removeCall,
"remove-rights should skip invalid ids before removing rights");
}
@Test
void kickPluginEventOnlyFiresAfterPermissionCheck() throws Exception {
String source = source("RoomUserKickEvent");
int userRead = source.indexOf("int userId = this.packet.readInt()");
int idGuard = source.indexOf("RoomUserInputGuard.isPositiveId(userId)", userRead);
int targetLookup = source.indexOf("room.getHabbo(userId)", idGuard);
int permissionCheck = source.indexOf("room.hasRights(this.client.getHabbo())", targetLookup);
int event = source.indexOf("new UserKickEvent", permissionCheck);
int kick = source.indexOf("room.kickHabbo(target, true)", event);
assertTrue(idGuard > userRead && idGuard < targetLookup,
"kick should validate target id before room lookup");
assertTrue(permissionCheck > targetLookup && event > permissionCheck && event < kick,
"kick plugin event should only fire once the actor is authorized");
}
@Test
void roomActionsRejectUnknownActionIdsBeforeComposersAndWired() throws Exception {
String source = source("RoomUserActionEvent");
int actionRead = source.indexOf("int action = this.packet.readInt()");
int guard = source.indexOf("RoomUserInputGuard.isValidAction(action)", actionRead);
int composer = source.indexOf("new RoomUserActionComposer", guard);
int wired = source.indexOf("WiredManager.triggerUserPerformsAction", guard);
assertTrue(guard > actionRead && guard < composer,
"room actions should reject unknown ids before composing room state");
assertTrue(guard < wired,
"room actions should reject unknown ids before wired triggers");
}
@Test
void helperBoundsExpectedRanges() {
assertFalse(RoomUserInputGuard.isPositiveId(0));
assertTrue(RoomUserInputGuard.isPositiveId(1));
assertFalse(RoomUserInputGuard.isValidAction(-1));
assertTrue(RoomUserInputGuard.isValidAction(0));
assertTrue(RoomUserInputGuard.isValidAction(7));
assertFalse(RoomUserInputGuard.isValidAction(8));
}
}
@@ -0,0 +1,50 @@
package com.eu.habbo.messages.incoming.trading;
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 TradeOfferGuardContractTest {
private static String incomingSource(String name) throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/trading/" + name + ".java"));
}
private static String roomTradeSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java"));
}
@Test
void multipleTradeOfferPacketBoundsClientSuppliedCountBeforeInventoryLookups() throws Exception {
String source = incomingSource("TradeOfferMultipleItemsEvent");
int count = source.indexOf("int count = this.packet.readInt()");
int guard = source.indexOf("count <= 0 || count > RoomTrade.MAX_OFFERED_ITEMS", count);
int loop = source.indexOf("for (int i = 0; i < count; i++)", count);
int lookup = source.indexOf("getHabboItem(itemId)", loop);
assertTrue(count > -1, "TradeOfferMultipleItemsEvent must read the client supplied count");
assertTrue(guard > count, "TradeOfferMultipleItemsEvent must validate the count after reading it");
assertTrue(guard < loop, "TradeOfferMultipleItemsEvent must validate the count before looping");
assertTrue(loop < lookup, "TradeOfferMultipleItemsEvent should only resolve inventory items inside the bounded loop");
assertTrue(source.contains("itemId <= 0"),
"TradeOfferMultipleItemsEvent must skip invalid item ids before inventory lookup");
}
@Test
void roomTradeEnforcesServerSideOfferedItemCapBeforeInventoryMutation() throws Exception {
String source = roomTradeSource();
int constant = source.indexOf("MAX_OFFERED_ITEMS = 100");
int singleGuard = source.indexOf("user.getItems().size() >= MAX_OFFERED_ITEMS");
int multipleGuard = source.indexOf("user.getItems().size() >= MAX_OFFERED_ITEMS", singleGuard + 1);
int remove = source.indexOf("removeHabboItem(item)", multipleGuard);
assertTrue(constant > -1, "RoomTrade must define a server-side offered item cap");
assertTrue(singleGuard > constant, "RoomTrade.offerItem must enforce the item cap");
assertTrue(multipleGuard > singleGuard, "RoomTrade.offerMultipleItems must enforce the item cap");
assertTrue(multipleGuard < remove, "RoomTrade must enforce the cap before mutating inventory");
}
}
@@ -0,0 +1,64 @@
package com.eu.habbo.messages.incoming.users;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class UserInputGuardContractTest {
private static String source(String name) throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/users/" + name + ".java"));
}
@Test
void userProfileLookupsRejectInvalidIdsBeforeOfflineQueries() throws Exception {
for (String handler : new String[]{"RequestUserProfileEvent", "RequestProfileFriendsEvent", "RequestWearingBadgesEvent"}) {
String source = source(handler);
int read = source.indexOf("this.packet.readInt()");
int guard = source.indexOf("UserInputGuard.isPositiveId", read);
int lookup = Math.max(source.indexOf("getOfflineHabboInfo", guard), source.indexOf("getBadgesOfflineHabbo", guard));
assertTrue(guard > read, handler + " should validate the packet id after reading it");
assertTrue(lookup == -1 || guard < lookup, handler + " should validate ids before offline lookups");
}
}
@Test
void settingsInputsAreNormalizedBeforePersistence() throws Exception {
String volumes = source("SaveUserVolumesEvent");
String flags = source("UpdateUIFlagsEvent");
assertTrue(volumes.contains("UserInputGuard.clampVolume(this.packet.readInt())"),
"volume settings should be clamped to the client-supported range");
assertTrue(flags.contains("UserInputGuard.sanitizeUiFlags(this.packet.readInt())"),
"UI flags should be sanitized before persistence");
}
@Test
void mottoIsNormalizedAndRejectedBeforeSaveSideEffects() throws Exception {
String motto = source("SaveMottoEvent");
int pluginValue = motto.indexOf("UserInputGuard.normalizeText(event.newMotto)");
int lengthGuard = motto.indexOf("motto.length() > Emulator.getConfig().getInt", pluginValue);
int save = motto.indexOf("setMotto(motto)", lengthGuard);
int achievement = motto.indexOf("AchievementManager.progressAchievement", save);
assertTrue(pluginValue > -1, "plugin-mutated motto should be normalized");
assertTrue(lengthGuard > pluginValue && lengthGuard < save,
"motto length should be validated before saving");
assertTrue(save < achievement,
"motto achievement should only progress after a valid save");
}
@Test
void helperClampsAndMasksValues() {
assertEquals(0, UserInputGuard.clampVolume(-20));
assertEquals(40, UserInputGuard.clampVolume(40));
assertEquals(100, UserInputGuard.clampVolume(101));
assertEquals(0, UserInputGuard.sanitizeUiFlags(-1));
assertEquals(0xFFFF, UserInputGuard.sanitizeUiFlags(0x1FFFF));
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.networking.gameserver.auth;
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 AuthTokenGuardContractTest {
@Test
void accessTokenRejectsOversizedTokensBeforeSplitAndDecode() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/AccessTokenService.java"));
int maxConstant = source.indexOf("MAX_TOKEN_CHARS = 2048");
int lengthGuard = source.indexOf("token.length() > MAX_TOKEN_CHARS");
int split = source.indexOf("token.split");
int decode = source.indexOf("URL_DEC.decode");
assertTrue(maxConstant > -1, "Access tokens should have a bounded serialized size");
assertTrue(lengthGuard > -1, "Access token verification must reject oversized tokens");
assertTrue(lengthGuard < split, "Access token length guard must run before split");
assertTrue(lengthGuard < decode, "Access token length guard must run before Base64 decode");
}
@Test
void rememberTokenRejectsOversizedTokensBeforeSplitAndDecode() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java"));
int maxConstant = source.indexOf("MAX_TOKEN_CHARS = 2048");
int lengthGuard = source.indexOf("jwt.length() > MAX_TOKEN_CHARS");
int split = source.indexOf("jwt.split");
int decode = source.indexOf("URL_DEC.decode");
assertTrue(maxConstant > -1, "Remember tokens should have a bounded serialized size");
assertTrue(lengthGuard > -1, "Remember token verification must reject oversized tokens");
assertTrue(lengthGuard < split, "Remember token length guard must run before split");
assertTrue(lengthGuard < decode, "Remember token length guard must run before Base64 decode");
}
}
@@ -0,0 +1,44 @@
package com.eu.habbo.networking.gameserver.auth;
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 NitroSecureApiHandlerContractTest {
private static String handlerSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java"));
}
private static String emulatorSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
}
@Test
void encryptedApiPayloadSizeIsBoundedBeforeCopyAndDecrypt() throws Exception {
String handler = handlerSource();
String emulator = emulatorSource();
int readableBytes = handler.indexOf("int readableBytes = req.content().readableBytes()");
int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes);
int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload);
int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes);
int decrypt = handler.indexOf("NitroSecureAssetHandler.decrypt", byteArray);
assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"),
"Secure API handler should have a conservative default payload cap");
assertTrue(handler.contains("nitro.secure.api.max_payload_bytes"),
"Secure API max payload should be configurable");
assertTrue(readableBytes > -1, "Secure API handler must read content size before allocation");
assertTrue(maxPayload > readableBytes, "Secure API handler must resolve max payload before allocation");
assertTrue(oversizedGuard > maxPayload, "Secure API handler must reject oversized encrypted payloads");
assertTrue(oversizedGuard < byteArray, "Oversized encrypted payloads must be rejected before byte array allocation");
assertTrue(byteArray < decrypt, "Secure API payload must be bounded before decrypting");
assertTrue(handler.contains("REQUEST_ENTITY_TOO_LARGE"),
"Secure API callers need a deterministic status for oversized encrypted payloads");
assertTrue(emulator.contains("register(\"nitro.secure.api.max_payload_bytes\", \"65536\")"),
"Secure API max payload default must be registered before startup");
}
}
@@ -0,0 +1,48 @@
package com.eu.habbo.networking.gameserver.auth;
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 NitroSecureAssetHandlerContractTest {
private static String handlerSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java"));
}
private static String emulatorSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
}
@Test
void secureAssetFilesAreSizeCheckedBeforeReadAndCache() throws Exception {
String handler = handlerSource();
String emulator = emulatorSource();
int size = handler.indexOf("long size = Files.size(target)");
int maxBytes = handler.indexOf("int maxBytes = maxAssetBytes(kind)", size);
int oversizedGuard = handler.indexOf("size > maxBytes", maxBytes);
int cacheLookup = handler.indexOf("CACHE.get(cacheKey)", oversizedGuard);
int readAllBytes = handler.indexOf("Files.readAllBytes(target)", oversizedGuard);
assertTrue(handler.contains("DEFAULT_MAX_CONFIG_BYTES = 2 * 1024 * 1024"),
"Secure config assets should have a conservative default file cap");
assertTrue(handler.contains("DEFAULT_MAX_GAMEDATA_BYTES = 16 * 1024 * 1024"),
"Secure gamedata assets should have a bounded default file cap");
assertTrue(handler.contains("nitro.secure.config.max_file_bytes"),
"Secure config max file size should be configurable");
assertTrue(handler.contains("nitro.secure.gamedata.max_file_bytes"),
"Secure gamedata max file size should be configurable");
assertTrue(size > -1, "Secure assets must inspect file size before loading bytes");
assertTrue(maxBytes > size, "Secure assets must resolve the configured cap before loading bytes");
assertTrue(oversizedGuard > maxBytes, "Secure assets must reject oversized files");
assertTrue(oversizedGuard < cacheLookup, "Oversized secure assets must not be served from cache");
assertTrue(oversizedGuard < readAllBytes, "Oversized secure assets must be rejected before readAllBytes");
assertTrue(emulator.contains("register(\"nitro.secure.config.max_file_bytes\", \"2097152\")"),
"Secure config max file size default must be registered before startup");
assertTrue(emulator.contains("register(\"nitro.secure.gamedata.max_file_bytes\", \"16777216\")"),
"Secure gamedata max file size default must be registered before startup");
}
}
@@ -50,6 +50,30 @@ class RCONServerHandlerContractTest {
assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed");
}
@Test
void rconPayloadSizeIsBoundedBeforeBufferCopy() throws Exception {
String handler = handlerSource();
String emulator = emulatorSource();
int readableBytes = handler.indexOf("int readableBytes = data.readableBytes()");
int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes);
int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload);
int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes);
assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"),
"RCON handler should have a conservative default payload cap");
assertTrue(handler.contains("rcon.max_payload_bytes"),
"RCON max payload should be configurable");
assertTrue(readableBytes > -1, "RCON handler must read ByteBuf size before allocation");
assertTrue(maxPayload > readableBytes, "RCON handler must resolve max payload before allocation");
assertTrue(oversizedGuard > maxPayload, "RCON handler must reject oversized payloads");
assertTrue(oversizedGuard < byteArray, "Oversized RCON payloads must be rejected before byte array allocation");
assertTrue(handler.contains("PAYLOAD_TOO_LARGE"),
"RCON callers need a deterministic response for oversized payloads");
assertTrue(emulator.contains("register(\"rcon.max_payload_bytes\", \"65536\")"),
"RCON max payload default must be registered before startup");
}
@Test
void inboundByteBufIsReleasedFromFinallyBlock() throws Exception {
String source = handlerSource();
@@ -59,4 +83,16 @@ class RCONServerHandlerContractTest {
assertTrue(finallyIndex >= 0, "RCON channelRead must release inbound ByteBufs from a finally block");
assertTrue(releaseIndex > finallyIndex, "RCON channelRead must release the inbound ByteBuf after finally starts");
}
@Test
void rconWhitelistUsesSocketAddressInsteadOfStringSplitting() throws Exception {
String source = handlerSource();
assertTrue(source.contains("InetSocketAddress"),
"RCON whitelist should resolve socket addresses instead of parsing remoteAddress.toString()");
assertTrue(source.contains("getHostAddress()"),
"RCON whitelist should compare the resolved host address");
assertTrue(!source.contains(".toString().split(\":\")"),
"RCON whitelist must not split host:port strings because that breaks IPv6 addresses");
}
}