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>
270 lines
14 KiB
C++
270 lines
14 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include "SlashCommandHandler.h"
|
|
#include "Permissions.h"
|
|
#include "Game.h"
|
|
#include "dConfig.h"
|
|
|
|
namespace {
|
|
// Game::config set to a config with these database values for the length of a test
|
|
class ScopedConfig {
|
|
public:
|
|
explicit ScopedConfig(std::map<std::string, std::string> overrides) : m_Previous(Game::config), m_Config("does_not_exist_config.ini") {
|
|
m_Config.SetDatabaseValues(std::move(overrides), {});
|
|
Game::config = &m_Config;
|
|
}
|
|
~ScopedConfig() { Game::config = m_Previous; }
|
|
private:
|
|
dConfig* m_Previous;
|
|
dConfig m_Config;
|
|
};
|
|
|
|
Command Paired(const std::string& name, eGameMasterLevel level, const std::string& permission) {
|
|
Command command{ .requiredLevel = level, .minLevel = eGameMasterLevel::FORUM_MODERATOR, .dashboardPermission = permission };
|
|
command.name = name;
|
|
return command;
|
|
}
|
|
}
|
|
|
|
// A paired command needs its permission's level, follows it when it changes, and keeps an explicit override
|
|
TEST(SlashCommandPermissionTests, PairedCommandUsesThePermissionLevel) {
|
|
const auto mute = Paired("mute", eGameMasterLevel::JUNIOR_DEVELOPER, "accounts_mute");
|
|
{
|
|
ScopedConfig config({});
|
|
EXPECT_EQ(static_cast<uint8_t>(SlashCommandHandler::GetRequiredLevel(mute)), Permissions::Find("accounts_mute")->defaultLevel);
|
|
}
|
|
{
|
|
ScopedConfig config({ { "permission_accounts_mute", "5" } });
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(mute), eGameMasterLevel::LEAD_MODERATOR);
|
|
}
|
|
{
|
|
// An older command_level_mute stays in force
|
|
ScopedConfig config({ { "permission_accounts_mute", "5" }, { "command_level_mute", "6" } });
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(mute), eGameMasterLevel::JUNIOR_DEVELOPER);
|
|
}
|
|
{
|
|
// ...until the page drops it
|
|
ScopedConfig config({ { "permission_accounts_mute", "5" }, { "command_level_mute", "permission" } });
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(mute), eGameMasterLevel::LEAD_MODERATOR);
|
|
}
|
|
// An unknown permission: an ordinary command with its own level
|
|
const auto unknown = Paired("zz_unknown", eGameMasterLevel::MODERATOR, "no_such_permission");
|
|
ScopedConfig config({ { "command_level_zz_unknown", "4" } });
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(unknown), eGameMasterLevel::SENIOR_MODERATOR);
|
|
}
|
|
|
|
TEST(SlashCommandPermissionTests, FloorsApplyToPairedCommands) {
|
|
Command command{ .requiredLevel = eGameMasterLevel::DEVELOPER, .minLevel = eGameMasterLevel::DEVELOPER, .dashboardPermission = "accounts_kick" };
|
|
command.name = "zz_floor";
|
|
ScopedConfig config({ { "permission_accounts_kick", "2" }, { "command_level_zz_floor", "3" } });
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(command), eGameMasterLevel::DEVELOPER);
|
|
Command fixed{ .requiredLevel = eGameMasterLevel::CIVILIAN, .fixedLevel = true, .dashboardPermission = "accounts_kick" };
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(fixed), eGameMasterLevel::CIVILIAN);
|
|
}
|
|
|
|
// The dashboard's self and rank rules, as the commands apply them
|
|
TEST(SlashCommandPermissionTests, TargetRulesMatchTheDashboard) {
|
|
using SlashCommandLevels::eTargetRule;
|
|
using AccountRules::eManageDenial;
|
|
ScopedConfig config({});
|
|
// Nobody below GM 9 acts on a higher level; GM 9 on anyone
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(8, 1, 9, 2, eTargetRule::TOOLS), eManageDenial::HIGHER_RANK);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(8, 1, 9, 2, eTargetRule::OTHERS), eManageDenial::HIGHER_RANK);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(9, 1, 9, 2, eTargetRule::MODERATION), eManageDenial::NONE);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(9, 1, 9, 1, eTargetRule::MODERATION), eManageDenial::NONE);
|
|
// Same level: manage_equal_rank (GM 9 by default)
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(4, 1, 4, 2, eTargetRule::MODERATION), eManageDenial::EQUAL_RANK);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(4, 1, 3, 2, eTargetRule::MODERATION), eManageDenial::NONE);
|
|
// Themselves: the self permission for the kind (self_tools GM 1, self_items and self_moderation GM 9)
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(2, 1, 2, 1, eTargetRule::TOOLS), eManageDenial::NONE);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(8, 1, 8, 1, eTargetRule::ITEMS), eManageDenial::SELF);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(8, 1, 8, 1, eTargetRule::MODERATION), eManageDenial::SELF);
|
|
// OTHERS: on themselves as before (like /setlevel without a name)
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(6, 1, 6, 1, eTargetRule::OTHERS), eManageDenial::NONE);
|
|
// NONE: not checked
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(1, 1, 9, 2, eTargetRule::NONE), eManageDenial::NONE);
|
|
|
|
// The refusal names the permission
|
|
const auto refusal = SlashCommandHandler::TargetRefusal(eManageDenial::SELF, eTargetRule::ITEMS, "mailitem");
|
|
EXPECT_EQ(refusal.rfind("/mailitem: ", 0), 0u);
|
|
EXPECT_NE(refusal.find("self_items"), std::string::npos);
|
|
EXPECT_NE(SlashCommandHandler::TargetRefusal(eManageDenial::EQUAL_RANK, eTargetRule::MODERATION, "ban").find("manage_equal_rank"), std::string::npos);
|
|
EXPECT_EQ(SlashCommandHandler::TargetRefusal(eManageDenial::NONE, eTargetRule::TOOLS, "kick"), "");
|
|
}
|
|
|
|
TEST(SlashCommandPermissionTests, TargetRulesFollowThePermissionsPage) {
|
|
using SlashCommandLevels::eTargetRule;
|
|
using AccountRules::eManageDenial;
|
|
ScopedConfig config({ { "permission_self_moderation", "4" }, { "permission_manage_equal_rank", "4" } });
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(4, 1, 4, 1, eTargetRule::MODERATION), eManageDenial::NONE);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(4, 1, 4, 2, eTargetRule::MODERATION), eManageDenial::NONE);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(3, 1, 3, 2, eTargetRule::MODERATION), eManageDenial::EQUAL_RANK);
|
|
EXPECT_EQ(SlashCommandHandler::TargetDenial(4, 1, 5, 2, eTargetRule::MODERATION), eManageDenial::HIGHER_RANK);
|
|
}
|
|
|
|
#include "GameDependencies.h"
|
|
|
|
class SlashCommandCatalogTest : public GameDependenciesTest {
|
|
protected:
|
|
void SetUp() override {
|
|
SetUpDependencies();
|
|
static bool started = false;
|
|
if (!started) SlashCommandHandler::Startup();
|
|
started = true;
|
|
}
|
|
void TearDown() override { TearDownDependencies(); }
|
|
};
|
|
|
|
// Which commands share a dashboard permission's level, and which rule they follow on other players
|
|
TEST_F(SlashCommandCatalogTest, PairsAndTargetRules) {
|
|
using SlashCommandLevels::eTargetRule;
|
|
const std::map<std::string, std::string> pairs{
|
|
{ "kick", "accounts_kick" }, { "mute", "accounts_mute" }, { "ban", "accounts_ban" },
|
|
{ "announce", "server_announce" }, { "setanntitle", "server_announce" }, { "setannmsg", "server_announce" },
|
|
{ "mailitem", "mail_items" }, { "approveproperty", "moderate_properties" },
|
|
{ "showall", "players_view" }, { "findplayer", "players_view" }, { "spectate", "players_view" },
|
|
{ "shutdown", "worlds_manage" }, { "shutdownuniverse", "server_restart" },
|
|
{ "uptime", "health_view" }, { "metrics", "health_view" },
|
|
// Only on the user's own character, or a movement tool: their own level (see SlashCommandHandler.cpp)
|
|
{ "teleport", "" }, { "gmadditem", "" }, { "givemoney", "" }, { "setcurrency", "" }, { "giveuscore", "" }, { "setlevel", "" },
|
|
{ "execute", "" }, { "setgmlevel", "" },
|
|
};
|
|
for (const auto& [alias, permission] : pairs) {
|
|
const auto* command = SlashCommandHandler::FindCommand(alias);
|
|
ASSERT_NE(command, nullptr) << alias;
|
|
EXPECT_EQ(command->dashboardPermission, permission) << alias;
|
|
if (!permission.empty()) {
|
|
ASSERT_NE(Permissions::Find(permission), nullptr) << permission;
|
|
EXPECT_FALSE(command->fixedLevel) << alias;
|
|
}
|
|
}
|
|
const std::map<std::string, eTargetRule> rules{
|
|
{ "kick", eTargetRule::TOOLS }, { "kill", eTargetRule::TOOLS }, { "mailitem", eTargetRule::ITEMS },
|
|
{ "ban", eTargetRule::MODERATION }, { "mute", eTargetRule::MODERATION },
|
|
{ "teleport", eTargetRule::OTHERS }, { "setlevel", eTargetRule::OTHERS }, { "execute", eTargetRule::OTHERS }, { "tpall", eTargetRule::OTHERS },
|
|
{ "gmadditem", eTargetRule::NONE }, { "spectate", eTargetRule::NONE }, { "setgmlevel", eTargetRule::NONE },
|
|
};
|
|
for (const auto& [alias, rule] : rules) {
|
|
const auto* command = SlashCommandHandler::FindCommand(alias);
|
|
ASSERT_NE(command, nullptr) << alias;
|
|
EXPECT_EQ(command->targetRule, rule) << alias;
|
|
}
|
|
// /execute keeps its floor even though the self and rank rules now apply to it too
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(*SlashCommandHandler::FindCommand("execute")), eGameMasterLevel::DEVELOPER);
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(*SlashCommandHandler::FindCommand("setgmlevel")), eGameMasterLevel::CIVILIAN);
|
|
}
|
|
|
|
// When commands start using their permission's level, a server that had them keeps each one's old level once
|
|
TEST(SlashCommandPermissionTests, UpgradeOverrideDecision) {
|
|
using SlashCommandLevels::UpgradeOverride;
|
|
// Existing server, code level differs from the permission's: keep it (/mute: GM 6, accounts_mute GM 2)
|
|
EXPECT_EQ(UpgradeOverride(true, false, false, 6, 1, "", 2), std::optional<uint8_t>(6));
|
|
// Same level: nothing to keep
|
|
EXPECT_EQ(UpgradeOverride(true, false, false, 2, 1, "", 2), std::nullopt);
|
|
// Brand-new server (no stored row), or the upgrade already happened: none
|
|
EXPECT_EQ(UpgradeOverride(false, false, false, 6, 1, "", 2), std::nullopt);
|
|
EXPECT_EQ(UpgradeOverride(true, true, false, 6, 1, "", 2), std::nullopt);
|
|
// An explicit value is already an override; "permission" means follow it
|
|
EXPECT_EQ(UpgradeOverride(true, false, false, 6, 1, "4", 2), std::nullopt);
|
|
EXPECT_EQ(UpgradeOverride(true, false, false, 6, 1, "permission", 2), std::nullopt);
|
|
// A bad value meant the code's level
|
|
EXPECT_EQ(UpgradeOverride(true, false, false, 6, 1, "junk", 2), std::optional<uint8_t>(6));
|
|
EXPECT_EQ(UpgradeOverride(true, false, true, 6, 1, "", 2), std::nullopt);
|
|
}
|
|
|
|
namespace {
|
|
// Just the tables ReportCommands uses
|
|
class UpgradeDatabase : public TestSQLDatabase {
|
|
public:
|
|
std::map<std::string, SlashCommand> commands;
|
|
std::map<std::string, Setting> config; // worldconfig.ini, by name
|
|
std::vector<std::string> audits;
|
|
|
|
std::vector<SlashCommand> GetSlashCommands() override {
|
|
std::vector<SlashCommand> rows;
|
|
for (const auto& [_, row] : commands) rows.push_back(row);
|
|
return rows;
|
|
}
|
|
void SetSlashCommand(const SlashCommand& command) override { commands[command.name] = command; }
|
|
std::vector<Setting> GetServerConfig(const std::vector<std::string>&) override {
|
|
std::vector<Setting> rows;
|
|
for (const auto& [_, row] : config) rows.push_back(row);
|
|
return rows;
|
|
}
|
|
void SetWebConfigValue(const std::string& file, const std::string& name, const std::optional<std::string>& value, bool webWins, const std::string& by) override {
|
|
auto& row = config[name];
|
|
row.file = file;
|
|
row.name = name;
|
|
row.webValue = value;
|
|
row.webWins = webWins;
|
|
row.updatedBy = by;
|
|
}
|
|
void InsertAuditLog(uint32_t, const std::string_view accountName, const std::string_view action, const std::string_view description, uint32_t, LWOOBJID) override {
|
|
audits.push_back(std::string(accountName) + " " + std::string(action) + " " + std::string(description));
|
|
}
|
|
// What a world's settings sync would hand it
|
|
std::map<std::string, std::string> WebValues() const {
|
|
std::map<std::string, std::string> values;
|
|
for (const auto& [name, row] : config) if (row.webValue) values[name] = *row.webValue;
|
|
return values;
|
|
}
|
|
};
|
|
|
|
ISlashCommands::SlashCommand OldRow(const std::string& name, uint8_t level, const std::string& permission) {
|
|
return { .name = name, .aliases = { name }, .defaultLevel = level, .minLevel = 1, .dashboardPermission = permission };
|
|
}
|
|
}
|
|
|
|
TEST_F(SlashCommandCatalogTest, ExistingServerKeepsItsLevelsOnce) {
|
|
auto* db = new UpgradeDatabase();
|
|
Database::_setDatabase(db);
|
|
// Rows written before commands followed their permission
|
|
db->commands["mute"] = OldRow("mute", 6, "accounts_mute");
|
|
db->commands["kick"] = OldRow("kick", 2, "accounts_kick");
|
|
db->commands["mailitem"] = OldRow("mailitem", 3, "mail_items");
|
|
db->commands["spectate"] = OldRow("spectate", 2, "");
|
|
db->commands["ban"] = OldRow("ban", 4, "accounts_ban");
|
|
db->config["command_level_ban"] = { .file = "worldconfig.ini", .name = "command_level_ban", .webValue = "3", .webWins = true, .updatedBy = "admin" };
|
|
|
|
SlashCommandHandler::ReportCommands();
|
|
const std::string upgrade(SlashCommandLevels::UPGRADE_ACTOR);
|
|
EXPECT_EQ(db->config.at("command_level_mute").webValue, std::optional<std::string>("6"));
|
|
EXPECT_EQ(db->config.at("command_level_mute").updatedBy, upgrade);
|
|
EXPECT_EQ(db->config.at("command_level_mailitem").webValue, std::optional<std::string>("3"));
|
|
EXPECT_EQ(db->config.at("command_level_spectate").webValue, std::optional<std::string>("2"));
|
|
EXPECT_FALSE(db->config.contains("command_level_kick")); // same level as accounts_kick
|
|
EXPECT_EQ(db->config.at("command_level_ban").updatedBy, "admin"); // its own value was already an override
|
|
EXPECT_FALSE(db->config.contains("command_level_announce")); // not stored before: follows
|
|
EXPECT_EQ(db->audits.size(), 3u);
|
|
for (const auto& audit : db->audits) EXPECT_EQ(audit.rfind(upgrade + " change_command_level ", 0), 0u) << audit;
|
|
EXPECT_TRUE(db->commands.at("mute").followsPermission);
|
|
|
|
// The world uses the kept level
|
|
Game::config->SetDatabaseValues(db->WebValues(), {});
|
|
EXPECT_EQ(SlashCommandHandler::GetRequiredLevel(*SlashCommandHandler::FindCommand("mute")), eGameMasterLevel::JUNIOR_DEVELOPER);
|
|
|
|
// Only once: another start stores nothing new
|
|
SlashCommandHandler::ReportCommands();
|
|
EXPECT_EQ(db->audits.size(), 3u);
|
|
|
|
// Dropped on the Permissions page: it follows accounts_mute, and a later start doesn't bring it back
|
|
db->SetWebConfigValue("worldconfig.ini", "command_level_mute", std::nullopt, false, "admin");
|
|
Game::config->SetDatabaseValues(db->WebValues(), {});
|
|
EXPECT_EQ(static_cast<uint8_t>(SlashCommandHandler::GetRequiredLevel(*SlashCommandHandler::FindCommand("mute"))), Permissions::Level("accounts_mute"));
|
|
SlashCommandHandler::ReportCommands();
|
|
EXPECT_FALSE(db->config.at("command_level_mute").webValue.has_value());
|
|
EXPECT_EQ(db->audits.size(), 3u);
|
|
}
|
|
|
|
TEST_F(SlashCommandCatalogTest, NewServerFollowsThePermissions) {
|
|
auto* db = new UpgradeDatabase();
|
|
Database::_setDatabase(db);
|
|
SlashCommandHandler::ReportCommands();
|
|
EXPECT_TRUE(db->config.empty());
|
|
EXPECT_TRUE(db->audits.empty());
|
|
EXPECT_TRUE(db->commands.at("mute").followsPermission);
|
|
EXPECT_FALSE(db->commands.at("gmadditem").followsPermission);
|
|
Game::config->SetDatabaseValues({}, {});
|
|
EXPECT_EQ(static_cast<uint8_t>(SlashCommandHandler::GetRequiredLevel(*SlashCommandHandler::FindCommand("mute"))), Permissions::Level("accounts_mute"));
|
|
}
|