You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 07:26:18 +00:00
Merge branch 'dev' into fix/catalog-inventory-safety
This commit is contained in:
+281
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -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");
|
||||
}
|
||||
}
|
||||
+41
@@ -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));
|
||||
}
|
||||
}
|
||||
+23
@@ -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)));
|
||||
}
|
||||
}
|
||||
+49
-2
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+69
@@ -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");
|
||||
}
|
||||
}
|
||||
+37
@@ -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));
|
||||
}
|
||||
}
|
||||
+22
@@ -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));
|
||||
}
|
||||
}
|
||||
+67
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user