mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
5.3 KiB
C++
124 lines
5.3 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
|
|
#include "BackupFiles.h"
|
|
#include "GameLabels.h"
|
|
#include "LoginThrottle.h"
|
|
#include "PlayerAction.h"
|
|
#include "ServerState.h"
|
|
|
|
// ---- Failed sign-in throttling (DashboardAuthService) ----
|
|
|
|
TEST(LoginThrottleTests, BlocksOnlyTheGuessingAddressForThatAccount) {
|
|
LoginThrottle throttle(3, 900, 900);
|
|
EXPECT_FALSE(throttle.RecordFailure("1.1.1.1", 7, 100));
|
|
EXPECT_FALSE(throttle.RecordFailure("1.1.1.1", 7, 101));
|
|
EXPECT_TRUE(throttle.RecordFailure("1.1.1.1", 7, 102));
|
|
EXPECT_GT(throttle.BlockedFor("1.1.1.1", 7, 103), 0);
|
|
// The owner, from their own address, is not locked out by a stranger's guesses
|
|
EXPECT_EQ(throttle.BlockedFor("2.2.2.2", 7, 103), 0);
|
|
// Nor is the stranger blocked from other accounts
|
|
EXPECT_EQ(throttle.BlockedFor("1.1.1.1", 8, 103), 0);
|
|
}
|
|
|
|
TEST(LoginThrottleTests, BlockEndsAndOldFailuresExpire) {
|
|
LoginThrottle throttle(3, 60, 300);
|
|
throttle.RecordFailure("a", 1, 0);
|
|
throttle.RecordFailure("a", 1, 10);
|
|
// The first two are outside the 60 s window by now, so this is only the first in the window
|
|
EXPECT_FALSE(throttle.RecordFailure("a", 1, 200));
|
|
throttle.RecordFailure("a", 1, 201);
|
|
EXPECT_TRUE(throttle.RecordFailure("a", 1, 202));
|
|
EXPECT_EQ(throttle.BlockedFor("a", 1, 202), 300);
|
|
EXPECT_EQ(throttle.BlockedFor("a", 1, 502), 0); // time-limited, never permanent
|
|
}
|
|
|
|
TEST(LoginThrottleTests, SuccessAndUnlockForget) {
|
|
LoginThrottle throttle(2, 900, 900);
|
|
throttle.RecordFailure("a", 1, 0);
|
|
throttle.Clear("a", 1);
|
|
EXPECT_FALSE(throttle.RecordFailure("a", 1, 1));
|
|
EXPECT_TRUE(throttle.RecordFailure("a", 1, 2));
|
|
throttle.RecordFailure("b", 1, 2);
|
|
throttle.ClearAccount(1);
|
|
EXPECT_EQ(throttle.BlockedFor("a", 1, 3), 0);
|
|
EXPECT_EQ(throttle.Size(), 0u);
|
|
}
|
|
|
|
// ---- What players see of running worlds (/api/status, dashboard_update) ----
|
|
|
|
TEST(ServerStatePlayerSafeTests, PropertiesAreMergedAndPrivateDetailsDropped) {
|
|
nlohmann::json state{ {"worlds", nlohmann::json::array({
|
|
{ {"mapID", 1100}, {"instanceID", 1}, {"cloneID", 0}, {"players", 4}, {"isPrivate", false}, {"zoneName", "Avant Gardens"} },
|
|
{ {"mapID", 1150}, {"instanceID", 2}, {"cloneID", 12345}, {"players", 1}, {"isPrivate", true}, {"zoneName", "Block Yard"} },
|
|
{ {"mapID", 1150}, {"instanceID", 3}, {"cloneID", 67890}, {"players", 2}, {"isPrivate", false}, {"zoneName", "Block Yard"} },
|
|
}) }, {"stats", { {"onlinePlayers", 7}, {"worlds", 3} }} };
|
|
|
|
const auto safe = ServerState::PlayerSafe(state);
|
|
ASSERT_EQ(safe["worlds"].size(), 2u);
|
|
for (const auto& world : safe["worlds"]) {
|
|
EXPECT_FALSE(world.contains("cloneID"));
|
|
EXPECT_FALSE(world.contains("isPrivate"));
|
|
}
|
|
EXPECT_EQ(safe["worlds"][0]["players"], 4);
|
|
EXPECT_EQ(safe["worlds"][1]["mapID"], 1150);
|
|
EXPECT_EQ(safe["worlds"][1]["players"], 3);
|
|
EXPECT_EQ(safe["worlds"][1]["properties"], 2);
|
|
EXPECT_EQ(safe["worlds"][1]["instanceID"], 0);
|
|
// Totals are unchanged
|
|
EXPECT_EQ(safe["stats"]["worlds"], 3);
|
|
EXPECT_EQ(safe["stats"]["onlinePlayers"], 7);
|
|
}
|
|
|
|
// ---- Player action text is cut on a character boundary ----
|
|
|
|
TEST(PlayerActionTextTests, TruncationKeepsUtf8Whole) {
|
|
EXPECT_EQ(PlayerActionRequest::Utf8Length("abc", 10), 3u);
|
|
// "é" is 2 bytes; cutting at 2 would split it
|
|
const std::string text = "a\xC3\xA9" "b";
|
|
EXPECT_EQ(PlayerActionRequest::Utf8Length(text, 2), 1u);
|
|
EXPECT_EQ(PlayerActionRequest::Utf8Length(text, 3), 3u);
|
|
// A 4-byte emoji at the limit is dropped whole
|
|
const std::string emoji = std::string(PlayerActionRequest::MAX_TEXT - 2, 'x') + "\xF0\x9F\x98\x80";
|
|
EXPECT_EQ(PlayerActionRequest::Utf8Length(emoji, PlayerActionRequest::MAX_TEXT), PlayerActionRequest::MAX_TEXT - 2u);
|
|
|
|
PlayerActionRequest request;
|
|
request.text = emoji;
|
|
RakNet::BitStream stream;
|
|
request.Serialize(stream);
|
|
PlayerActionRequest read;
|
|
ASSERT_TRUE(read.Deserialize(stream));
|
|
EXPECT_EQ(read.text, std::string(PlayerActionRequest::MAX_TEXT - 2, 'x'));
|
|
}
|
|
|
|
// ---- Labels the pages used to keep their own copies of ----
|
|
|
|
TEST(GameLabelsTests, TransferFlagAndTaskNamesComeFromTheEnums) {
|
|
const auto& labels = GameLabels::Json();
|
|
const auto name = [&](const char* kind, int value) {
|
|
for (const auto& entry : labels[kind]) if (entry["value"] == value) return entry["name"].get<std::string>();
|
|
return std::string();
|
|
};
|
|
EXPECT_EQ(name("transferMethods", 1), "Trade");
|
|
EXPECT_EQ(name("transferMethods", 3), "Mail Claimed");
|
|
EXPECT_EQ(name("flagKinds", 3), "Duplicate");
|
|
EXPECT_EQ(name("flagStatus", 0), "Open");
|
|
EXPECT_EQ(name("taskStatus", 3), "Timed Out");
|
|
}
|
|
|
|
// ---- mysqldump runs without a shell ----
|
|
|
|
TEST(BackupFilesTests, MysqldumpArgumentsAreAVector) {
|
|
const auto arguments = BackupFiles::MysqldumpArguments({ "/usr/bin/mysqldump", "/tmp/opt", "db.local:3307", "dlu$(touch x)", "/b/dlu.sql.partial", "/tmp/err" });
|
|
ASSERT_FALSE(arguments.empty());
|
|
EXPECT_EQ(arguments.front(), "/usr/bin/mysqldump");
|
|
EXPECT_EQ(arguments[1], "--defaults-extra-file=/tmp/opt"); // must be first
|
|
EXPECT_NE(std::find(arguments.begin(), arguments.end(), "--host=db.local"), arguments.end());
|
|
EXPECT_NE(std::find(arguments.begin(), arguments.end(), "--port=3307"), arguments.end());
|
|
EXPECT_NE(std::find(arguments.begin(), arguments.end(), "--log-error=/tmp/err"), arguments.end());
|
|
EXPECT_NE(std::find(arguments.begin(), arguments.end(), "--result-file=/b/dlu.sql.partial"), arguments.end());
|
|
// Passed as one literal argument, never interpreted
|
|
EXPECT_EQ(arguments.back(), "dlu$(touch x)");
|
|
}
|