Merge branch 'dev' into fix/catalog-inventory-safety

This commit is contained in:
DuckieTM
2026-06-15 22:17:00 +02:00
committed by GitHub
64 changed files with 2398 additions and 51 deletions
@@ -0,0 +1,281 @@
package com.eu.habbo.habbohotel.earnings;
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager.ClaimRepository;
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager.ConfigSource;
import com.eu.habbo.habbohotel.earnings.EarningsCenterManager.RewardApplier;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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 EarningsCenterManagerTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.ofEpochSecond(1_800_000_000L), ZoneOffset.UTC);
@Test
void disabledFeatureReturnsDisabledEntriesAndRejectsClaims() {
TestConfig config = new TestConfig().with("earnings.enabled", "0");
TestClaims claims = new TestClaims();
TestRewards rewards = new TestRewards();
EarningsCenterManager manager = new EarningsCenterManager(config, claims, rewards, FIXED_CLOCK);
List<EarningsEntry> entries = manager.getEntries(null);
EarningsClaimResult result = manager.claim(null, "daily_gift");
assertFalse(entries.getFirst().isEnabled());
assertFalse(entries.getFirst().isClaimable());
assertEquals(EarningsClaimResult.Status.DISABLED, result.getStatus());
assertTrue(rewards.granted.isEmpty());
}
@Test
void unknownCategoryIsRejected() {
EarningsCenterManager manager = new EarningsCenterManager(enabledConfig(), new TestClaims(), new TestRewards(), FIXED_CLOCK);
EarningsClaimResult result = manager.claim(null, "not_real");
assertEquals(EarningsClaimResult.Status.UNKNOWN_CATEGORY, result.getStatus());
}
@Test
void successfulClaimGrantsConfiguredRewardOnce() {
TestConfig config = enabledConfig()
.with("earnings.daily_gift.credits", "25")
.with("earnings.daily_gift.points", "3")
.with("earnings.daily_gift.points.type", "7");
TestClaims claims = new TestClaims();
TestRewards rewards = new TestRewards();
EarningsCenterManager manager = new EarningsCenterManager(config, claims, rewards, FIXED_CLOCK);
EarningsClaimResult first = manager.claim(null, "daily_gift");
EarningsClaimResult duplicate = manager.claim(null, "daily_gift");
assertEquals(EarningsClaimResult.Status.SUCCESS, first.getStatus());
assertEquals(EarningsClaimResult.Status.ALREADY_CLAIMED, duplicate.getStatus());
assertEquals(2, rewards.granted.size());
assertEquals(EarningsReward.TYPE_CREDITS, rewards.granted.get(0).getType());
assertEquals(25, rewards.granted.get(0).getAmount());
assertEquals(EarningsReward.TYPE_POINTS, rewards.granted.get(1).getType());
assertEquals(7, rewards.granted.get(1).getPointsType());
}
@Test
void categoryWithNoConfiguredRewardIsNotClaimable() {
EarningsCenterManager manager = new EarningsCenterManager(enabledConfig(), new TestClaims(), new TestRewards(), FIXED_CLOCK);
EarningsClaimResult result = manager.claim(null, "games");
assertEquals(EarningsClaimResult.Status.NO_REWARD, result.getStatus());
assertFalse(result.getEntry().isClaimable());
}
@Test
void configurableBadgeItemAndHcRewardsAreIncludedInEntryState() {
TestConfig config = enabledConfig()
.with("earnings.bonus_bag.badge", "ACH_Test1")
.with("earnings.bonus_bag.item_id", "123")
.with("earnings.bonus_bag.item.quantity", "2")
.with("earnings.bonus_bag.hc.days", "7");
EarningsCenterManager manager = new EarningsCenterManager(config, new TestClaims(), new TestRewards(), FIXED_CLOCK);
EarningsEntry entry = manager.getEntries(null).stream()
.filter(current -> current.getCategory() == EarningsCategory.BONUS_BAG)
.findFirst()
.orElseThrow();
assertTrue(entry.isClaimable());
assertEquals(3, entry.getRewards().size());
assertEquals(EarningsReward.TYPE_BADGE, entry.getRewards().get(0).getType());
assertEquals("ACH_Test1", entry.getRewards().get(0).getData());
assertEquals(EarningsReward.TYPE_ITEM, entry.getRewards().get(1).getType());
assertEquals("123", entry.getRewards().get(1).getData());
assertEquals(2, entry.getRewards().get(1).getAmount());
assertEquals(EarningsReward.TYPE_HC_DAYS, entry.getRewards().get(2).getType());
assertEquals(7, entry.getRewards().get(2).getAmount());
}
@Test
void failedRewardGrantRollsBackClaimRecord() {
TestConfig config = enabledConfig().with("earnings.daily_gift.credits", "10");
TestClaims claims = new TestClaims();
EarningsCenterManager manager = new EarningsCenterManager(config, claims, (habbo, rewards) -> {
throw new SQLException("grant failed");
}, FIXED_CLOCK);
EarningsClaimResult failed = manager.claim(null, "daily_gift");
EarningsClaimResult retried = new EarningsCenterManager(config, claims, new TestRewards(), FIXED_CLOCK)
.claim(null, "daily_gift");
assertEquals(EarningsClaimResult.Status.ERROR, failed.getStatus());
assertEquals(EarningsClaimResult.Status.SUCCESS, retried.getStatus());
}
@Test
void nativeMarketplaceRowsUseNativeClaimInsteadOfPeriodicClaimLedger() {
TestConfig config = enabledConfig().with("earnings.marketplace.native.enabled", "1");
TestClaims claims = new TestClaims();
TestNativeIntegration nativeIntegration = new TestNativeIntegration(EarningsCategory.MARKETPLACE)
.withReward(new EarningsReward(EarningsReward.TYPE_CREDITS, 45, 0));
EarningsCenterManager manager = new EarningsCenterManager(config, claims, new TestRewards(), nativeIntegration, FIXED_CLOCK);
EarningsEntry entry = manager.getEntries(null).stream()
.filter(current -> current.getCategory() == EarningsCategory.MARKETPLACE)
.findFirst()
.orElseThrow();
EarningsClaimResult result = manager.claim(null, "marketplace");
assertTrue(entry.isClaimable());
assertEquals(45, entry.getRewards().getFirst().getAmount());
assertEquals(EarningsClaimResult.Status.SUCCESS, result.getStatus());
assertEquals(1, nativeIntegration.claims);
assertTrue(claims.claims.isEmpty());
}
@Test
void nativeRowsWithoutAvailableRewardsAreNotClaimable() {
TestConfig config = enabledConfig().with("earnings.hc_payday.native.enabled", "1");
TestNativeIntegration nativeIntegration = new TestNativeIntegration(EarningsCategory.HC_PAYDAY);
EarningsCenterManager manager = new EarningsCenterManager(config, new TestClaims(), new TestRewards(), nativeIntegration, FIXED_CLOCK);
EarningsEntry entry = manager.getEntries(null).stream()
.filter(current -> current.getCategory() == EarningsCategory.HC_PAYDAY)
.findFirst()
.orElseThrow();
EarningsClaimResult result = manager.claim(null, "hc_payday");
assertFalse(entry.isClaimable());
assertEquals(EarningsClaimResult.Status.NO_REWARD, result.getStatus());
assertEquals(0, nativeIntegration.claims);
}
@Test
void claimAllGrantsClaimableRowsAndSkipsAlreadyClaimedRows() throws SQLException {
TestConfig config = enabledConfig()
.with("earnings.daily_gift.credits", "10")
.with("earnings.games.pixels", "4");
TestClaims claims = new TestClaims();
TestRewards rewards = new TestRewards();
EarningsCenterManager manager = new EarningsCenterManager(config, claims, rewards, FIXED_CLOCK);
claims.recordClaim(0, "daily_gift", String.valueOf(1_800_000_000L / 86400), 1_800_000_000);
List<EarningsClaimResult> results = manager.claimAll(null);
assertEquals(EarningsClaimResult.Status.ALREADY_CLAIMED, results.get(0).getStatus());
assertEquals(EarningsClaimResult.Status.SUCCESS, results.get(1).getStatus());
assertEquals(1, rewards.granted.size());
assertEquals(EarningsReward.TYPE_PIXELS, rewards.granted.getFirst().getType());
assertEquals(4, rewards.granted.getFirst().getAmount());
}
private static TestConfig enabledConfig() {
return new TestConfig().with("earnings.enabled", "1");
}
private static class TestConfig implements ConfigSource {
private final Map<String, String> values = new HashMap<>();
TestConfig with(String key, String value) {
this.values.put(key, value);
return this;
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
return this.values.getOrDefault(key, defaultValue ? "1" : "0").equals("1");
}
@Override
public int getInt(String key, int defaultValue) {
return Integer.parseInt(this.values.getOrDefault(key, String.valueOf(defaultValue)));
}
@Override
public String getValue(String key, String defaultValue) {
return this.values.getOrDefault(key, defaultValue);
}
}
private static class TestClaims implements ClaimRepository {
private final Set<String> claims = new HashSet<>();
@Override
public boolean hasClaim(int userId, String category, String periodKey) {
return this.claims.contains(key(userId, category, periodKey));
}
@Override
public boolean recordClaim(int userId, String category, String periodKey, int claimedAt) {
return this.claims.add(key(userId, category, periodKey));
}
@Override
public void removeClaim(int userId, String category, String periodKey) {
this.claims.remove(key(userId, category, periodKey));
}
private String key(int userId, String category, String periodKey) {
return userId + ":" + category + ":" + periodKey;
}
}
private static class TestRewards implements RewardApplier {
private final List<EarningsReward> granted = new ArrayList<>();
@Override
public void grant(com.eu.habbo.habbohotel.users.Habbo habbo, List<EarningsReward> rewards) {
this.granted.addAll(rewards);
}
}
private static class TestNativeIntegration implements EarningsCenterManager.NativeIntegration {
private final EarningsCategory category;
private final List<EarningsReward> rewards = new ArrayList<>();
private int claims = 0;
private TestNativeIntegration(EarningsCategory category) {
this.category = category;
}
private TestNativeIntegration withReward(EarningsReward reward) {
this.rewards.add(reward);
return this;
}
@Override
public boolean handles(EarningsCategory category) {
return this.category == category;
}
@Override
public boolean hasClaim(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
return handles(category) && !this.rewards.isEmpty();
}
@Override
public List<EarningsReward> rewards(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
return handles(category) ? List.copyOf(this.rewards) : List.of();
}
@Override
public boolean claim(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
if (!hasClaim(habbo, category)) {
return false;
}
this.claims++;
this.rewards.clear();
return true;
}
}
}
@@ -0,0 +1,62 @@
package com.eu.habbo.messages.incoming.guilds.forums;
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 GuildForumInputGuardContractTest {
@Test
void forumHandlersValidateClientProvidedIds() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums");
for (String handler : List.of(
"GuildForumPostThreadEvent.java",
"GuildForumModerateMessageEvent.java",
"GuildForumModerateThreadEvent.java",
"GuildForumThreadUpdateEvent.java",
"GuildForumThreadsEvent.java",
"GuildForumThreadsMessagesEvent.java",
"GuildForumMarkAsReadEvent.java",
"GuildForumUpdateSettingsEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("GuildForumInputGuard.isPositiveId"),
handler + " must reject zero or negative client-provided ids");
}
}
@Test
void forumHandlersBoundExpensiveClientInputs() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums");
String messages = Files.readString(base.resolve("GuildForumThreadsMessagesEvent.java"));
String markRead = Files.readString(base.resolve("GuildForumMarkAsReadEvent.java"));
String settings = Files.readString(base.resolve("GuildForumUpdateSettingsEvent.java"));
String moderateThread = Files.readString(base.resolve("GuildForumModerateThreadEvent.java"));
String moderateMessage = Files.readString(base.resolve("GuildForumModerateMessageEvent.java"));
assertTrue(messages.contains("GuildForumInputGuard.isValidPage(index, limit)"),
"thread message reads must bound index/limit before fetching comments");
assertTrue(markRead.contains("GuildForumInputGuard.isValidMarkReadBatch(count)"),
"mark-as-read must bound the client-provided batch count before DB writes");
assertTrue(settings.contains("GuildForumInputGuard.isSettingsState"),
"forum settings must reject unknown SettingsState values");
assertTrue(moderateThread.contains("GuildForumInputGuard.isThreadModerationState(state)"),
"thread moderation must reject unknown ForumThreadState values");
assertTrue(moderateMessage.contains("GuildForumInputGuard.isMessageModerationState(state)"),
"message moderation must reject unknown ForumThreadState values");
}
@Test
void forumPostsNormalizeTextBeforeFiltering() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java"));
assertTrue(source.contains("GuildForumInputGuard.normalize(this.packet.readString())"),
"forum post subject and body should be normalized before word filtering and length checks");
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.messages.incoming.guilds.forums;
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 GuildForumInputGuardTest {
@Test
void normalizesNullableText() {
assertEquals("", GuildForumInputGuard.normalize(null));
assertEquals("hello", GuildForumInputGuard.normalize(" hello "));
}
@Test
void validatesIdsAndPaging() {
assertFalse(GuildForumInputGuard.isPositiveId(0));
assertTrue(GuildForumInputGuard.isPositiveId(1));
assertFalse(GuildForumInputGuard.isValidPage(-1, 20));
assertFalse(GuildForumInputGuard.isValidPage(0, 0));
assertTrue(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT));
assertFalse(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT + 1));
}
@Test
void validatesBatchAndStates() {
assertFalse(GuildForumInputGuard.isValidMarkReadBatch(0));
assertTrue(GuildForumInputGuard.isValidMarkReadBatch(GuildForumInputGuard.MAX_MARK_READ_BATCH));
assertFalse(GuildForumInputGuard.isValidMarkReadBatch(GuildForumInputGuard.MAX_MARK_READ_BATCH + 1));
assertTrue(GuildForumInputGuard.isSettingsState(0));
assertTrue(GuildForumInputGuard.isSettingsState(3));
assertFalse(GuildForumInputGuard.isSettingsState(4));
assertTrue(GuildForumInputGuard.isThreadModerationState(20));
assertFalse(GuildForumInputGuard.isThreadModerationState(999));
assertTrue(GuildForumInputGuard.isMessageModerationState(10));
assertFalse(GuildForumInputGuard.isMessageModerationState(0));
}
}
@@ -0,0 +1,23 @@
package com.eu.habbo.messages.incoming.modtool;
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 ModToolInputGuardTest {
@Test
void normalizesNullableMessages() {
assertEquals("", ModToolInputGuard.normalize(null));
assertEquals("warn", ModToolInputGuard.normalize(" warn "));
}
@Test
void staffMessagesMustBeNonEmptyAndBounded() {
assertFalse(ModToolInputGuard.isSafeMessage(null));
assertFalse(ModToolInputGuard.isSafeMessage(""));
assertTrue(ModToolInputGuard.isSafeMessage("a".repeat(ModToolInputGuard.MAX_MESSAGE_LENGTH)));
assertFalse(ModToolInputGuard.isSafeMessage("a".repeat(ModToolInputGuard.MAX_MESSAGE_LENGTH + 1)));
}
}
@@ -45,7 +45,7 @@ class ModToolPermissionContractTest {
}
@Test
void modToolSanctionsCannotTargetSameOrHigherRanks() throws Exception {
void modToolSanctionsCannotTargetPeerRanksUnlessOperatorIsCoreRank() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
for (String handler : List.of(
@@ -60,6 +60,53 @@ class ModToolPermissionContractTest {
String manager = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java"));
assertTrue(manager.contains("!canModerateTarget(moderator, target.getHabboInfo().getId())"),
"ModToolManager.alert must refuse alerts/warnings against same-or-higher-rank targets");
"ModToolManager.alert must refuse alerts/warnings against protected targets");
assertTrue(manager.contains("targetRankId < moderatorRankId"),
"non-core moderators must only target lower-ranked users");
assertTrue(manager.contains("isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId"),
"highest/core moderators should be allowed to target peer ranks");
assertTrue(manager.contains("private static boolean isCoreRank(int rankId)"),
"core-rank detection should be centralized in ModToolManager");
}
@Test
void managerEntryPointsShareTargetAndRoomOwnerGuards() throws Exception {
String manager = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java"));
String sanctions = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolSanctions.java"));
String defaultSanction = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java"));
assertTrue(manager.contains("!canModerateTarget(moderator, targetUserId)"),
"ModToolManager.ban must use the central target-rank guard for offline and online users");
assertTrue(manager.contains("!canModerateTarget(moderator, h.getHabboInfo().getId())"),
"IP and machine fan-out bans must skip protected peer-or-higher ranked sessions");
assertTrue(manager.contains("!canModerateTarget(moderator, room.getOwnerId())"),
"ModToolManager.roomAction must refuse mutations on rooms owned by protected ranks");
assertTrue(sanctions.contains("!ModToolManager.canModerateTarget(self, habboId)"),
"ModToolSanctions.run must guard every sanction path before writing or applying it");
assertTrue(defaultSanction.contains("if (issue == null)"),
"default sanctions must tolerate stale or missing ticket ids");
}
@Test
void staffSuppliedModToolMessagesAreBounded() 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",
"ModToolRoomAlertEvent.java",
"ModToolSanctionAlertEvent.java",
"ModToolSanctionBanEvent.java",
"ModToolSanctionMuteEvent.java",
"ModToolSanctionTradeLockEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("ModToolInputGuard.normalize"),
handler + " must normalize staff-supplied text before use");
assertTrue(source.contains("ModToolInputGuard.isSafeMessage"),
handler + " must reject empty or oversized staff-supplied text");
}
}
}
@@ -0,0 +1,69 @@
package com.eu.habbo.messages.incoming.modtool;
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 ModToolReportInputContractTest {
@Test
void reportHandlersNormalizeAndBoundFreeText() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
for (String handler : List.of(
"ReportEvent.java",
"ReportFriendPrivateChatEvent.java",
"ReportCommentEvent.java",
"ReportThreadEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("ModToolReportInputGuard.normalize"),
handler + " must normalize report text before persistence or staff broadcast");
assertTrue(source.contains("ModToolReportInputGuard.isValidReportMessage"),
handler + " must reject empty or oversized report text");
}
}
@Test
void reportHandlersRejectInvalidIdsAndCounts() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
for (String handler : List.of(
"ReportEvent.java",
"ReportFriendPrivateChatEvent.java",
"ReportCommentEvent.java",
"ReportThreadEvent.java",
"ReportBullyEvent.java",
"ReportPhotoEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("ModToolReportInputGuard.isPositiveId"),
handler + " must reject zero or negative ids supplied by the client");
}
String privateChat = Files.readString(base.resolve("ReportFriendPrivateChatEvent.java"));
assertTrue(privateChat.contains("ModToolReportInputGuard.isValidPrivateChatLogCount(count)"),
"private chat reports must reject negative or oversized client-provided chatlog counts");
}
@Test
void reportEventValidatesTopicBeforeUsingReply() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ReportEvent.java"));
assertTrue(source.indexOf("if (cfhTopic == null)") < source.indexOf("cfhTopic.reply"),
"ReportEvent must reject unknown topics before dereferencing the reply text");
}
@Test
void bullyReportUsesSameMutedUserGateAsNormalReports() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ReportBullyEvent.java"));
assertTrue(source.contains("if (!this.client.getHabbo().getHabboStats().allowTalk())"),
"bully reports must reject muted users instead of rejecting users who can talk");
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.incoming.modtool;
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 ModToolReportInputGuardTest {
@Test
void normalizesNullableMessages() {
assertEquals("", ModToolReportInputGuard.normalize(null));
assertEquals("report", ModToolReportInputGuard.normalize(" report "));
}
@Test
void reportMessagesMustBeNonEmptyAndBounded() {
assertFalse(ModToolReportInputGuard.isValidReportMessage(""));
assertFalse(ModToolReportInputGuard.isValidReportMessage(null));
assertTrue(ModToolReportInputGuard.isValidReportMessage("a".repeat(ModToolReportInputGuard.MAX_REPORT_MESSAGE_LENGTH)));
assertFalse(ModToolReportInputGuard.isValidReportMessage("a".repeat(ModToolReportInputGuard.MAX_REPORT_MESSAGE_LENGTH + 1)));
}
@Test
void privateChatLogCountsAreBounded() {
assertFalse(ModToolReportInputGuard.isValidPrivateChatLogCount(0));
assertTrue(ModToolReportInputGuard.isValidPrivateChatLogCount(ModToolReportInputGuard.MAX_PRIVATE_CHAT_LOGS));
assertFalse(ModToolReportInputGuard.isValidPrivateChatLogCount(ModToolReportInputGuard.MAX_PRIVATE_CHAT_LOGS + 1));
}
@Test
void idsMustBePositive() {
assertFalse(ModToolReportInputGuard.isPositiveId(0));
assertFalse(ModToolReportInputGuard.isPositiveId(-1));
assertTrue(ModToolReportInputGuard.isPositiveId(1));
}
}
@@ -0,0 +1,22 @@
package com.eu.habbo.messages.incoming.modtool;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ModToolTicketGuardTest {
@Test
void idsMustBePositive() {
assertFalse(ModToolTicketGuard.isPositiveId(0));
assertFalse(ModToolTicketGuard.isPositiveId(-1));
assertTrue(ModToolTicketGuard.isPositiveId(1));
}
@Test
void releaseBatchIsBounded() {
assertFalse(ModToolTicketGuard.isValidReleaseBatch(0));
assertTrue(ModToolTicketGuard.isValidReleaseBatch(ModToolTicketGuard.MAX_RELEASE_BATCH));
assertFalse(ModToolTicketGuard.isValidReleaseBatch(ModToolTicketGuard.MAX_RELEASE_BATCH + 1));
}
}
@@ -0,0 +1,67 @@
package com.eu.habbo.messages.incoming.modtool;
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 ModToolTicketLifecycleContractTest {
@Test
void mutatingTicketActionsValidateOwnership() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
for (String handler : List.of(
"ModToolCloseTicketEvent.java",
"ModToolIssueChangeTopicEvent.java",
"ModToolReleaseTicketEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo())"),
handler + " must only mutate tickets owned by the acting moderator");
}
}
@Test
void clientDrivenTicketAndChatlogIdsAreValidated() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
for (String handler : List.of(
"ModToolPickTicketEvent.java",
"ModToolCloseTicketEvent.java",
"ModToolIssueChangeTopicEvent.java",
"ModToolRequestIssueChatlogEvent.java",
"ModToolRequestRoomChatlogEvent.java",
"ModToolRequestRoomUserChatlogEvent.java",
"ModToolRequestUserChatlogEvent.java"
)) {
String source = Files.readString(base.resolve(handler));
assertTrue(source.contains("ModToolTicketGuard.isPositiveId"),
handler + " must reject zero or negative client-provided ids");
}
}
@Test
void releaseBatchAndCloseStateAreBounded() throws Exception {
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool");
String release = Files.readString(base.resolve("ModToolReleaseTicketEvent.java"));
String close = Files.readString(base.resolve("ModToolCloseTicketEvent.java"));
assertTrue(release.contains("ModToolTicketGuard.isValidReleaseBatch(count)"),
"release ticket batches must be bounded before reading ticket ids");
assertTrue(close.contains("state < 1 || state > 3"),
"close ticket must reject unknown close states before mutating the ticket");
}
@Test
void changeTopicRequiresKnownCategory() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueChangeTopicEvent.java"));
assertTrue(source.contains("getCfhTopic(categoryId) == null"),
"change-topic must reject unknown CFH categories before persisting");
}
}