#include "moderation.hpp" #include #include #include using namespace utils::moderation; static const long long NOW = timestamp("2026-10-15T12:00:00.000Z"); static const std::string END = "2026-10-15T12:00:30.000Z"; static json reply(int revision, json sanctions = json::array(), json actions = json::array()) { if (sanctions.is_null()) sanctions = json::array(); if (sanctions.is_object()) sanctions = json::array({sanctions}); if (actions.is_object()) actions = json::array({actions}); return {{"ok", true}, {"revision", revision}, {"serverTime", "2026-10-15T12:00:00.000Z"}, {"sanctions", sanctions}, {"actions", actions}, {"pollAfterMs", 3000}}; } static json policy(std::string kind, std::string expiry = END) { return {{"id", "policy"}, {"revision", 1}, {"guid", "18446744073709551615"}, {"kind", kind}, {"reason", "rule"}, {"expiresAt", expiry.empty() ? json(nullptr) : json(expiry)}}; } static json act(std::string id, std::string connection, std::string kind = "kick") { return {{"id", id}, {"guid", "18446744073709551615"}, {"connection", connection}, {"kind", kind}, {"reason", "rule; say bad\n\"bad\\^1"}, {"expiresAt", END}}; } template void rejects(F f) { bool threw = false; try { f(); } catch (...) { threw = true; } assert(threw); } int main(int argc, char** argv) { assert(argc >= 2); auto root = std::filesystem::path(argv[1]); runtime game(root, "server", "https://controller/api/moderation/runtime", 1, "credential-a"); if (argc == 3) { if (std::string(argv[2]) == "write") { assert(game.request()["sessionSequence"] == 1); game.observe(0, "18446744073709551615", "one"); auto id = game.players.at(0).connection; game.accept(reply(1, {policy("ban", "")}, {act("crash", id)}).dump(), NOW); game.enforce(NOW, [](const auto&, int, const auto&) -> std::string { return "unknown"; }); } else { assert(game.request()["sessionSequence"] == 2); assert(game.revision() == 1); assert(game.request()["receipts"][0]["id"] == "crash"); assert(game.request()["receipts"][0]["state"] == "unknown"); int kicks = 0; game.observe(0, "18446744073709551615", "one"); game.enforce(NOW + 10, [&](const auto& kind, int, const auto&) { if (kind == "kick") ++kicks; return "confirmed"; }); assert(kicks == 1); // cached permanent policy enforces without a network response } return 0; } assert(canonical_guid("000123") == "123" && canonical_guid("000") == "0"); assert(canonical_guid("18446744073709551615") == "18446744073709551615" && canonical_guid("abc001") == "abc001"); assert(timestamp("2026-10-15T12:00:00Z") == NOW); for (const auto* invalid : {"garbage", "2026-02-31T12:00:00Z", "2026-10-15T12:00:00+01:00", "2026-10-15T12:00:00.abcZ"}) rejects([&] { timestamp(invalid); }); assert(command_text("a\";\\\n\r^1b") == "a 1b"); assert(command_text(std::string(239, 'a') + "é") == std::string(239, 'a')); assert(command_text(std::string(238, 'a') + "é") == std::string(238, 'a') + "é"); game.observe(1, "000123", std::string(254, 'a') + "é"); assert(game.players.at(1).guid == "123" && game.players.at(1).name == std::string(254, 'a')); game.observe(1, "123", std::string("bad") + char(0xff)); assert(game.players.at(1).name == "bad?"); game.observe(1, "123", std::string("bad") + char(1) + "name"); assert(game.players.at(1).name == "bad name"); for (const std::string blank : {"", " ", "\t\r\n\x01\x7f", "\xc2\xa0\xe2\x80\x83\xef\xbb\xbf"}) { game.observe(1, "123", blank); assert(game.players.at(1).name == "Unknown player"); assert(game.request()["players"][0]["name"] == "Unknown player"); } rejects([&] { game.observe(1, "abc.001", "name"); }); game.request().dump(); game.disconnect(1); game.observe(0, "18446744073709551615", "same name"); auto first = game.players.at(0).connection; game.observe(0, "18446744073709551615", "new name"); assert(game.players.at(0).connection == first); game.disconnect(0); game.observe(0, "18446744073709551615", "same name"); assert(game.players.at(0).connection != first); int executed = 0; auto execute = [&](const std::string&, int, const std::string& text) { ++executed; assert(text.find_first_of("\r\n;\"\\^") == std::string::npos); return "confirmed"; }; game.accept(reply(1, {}, {act("stale", first)}).dump(), NOW); game.enforce(NOW, execute); assert(executed == 0); assert(game.request()["receipts"][0]["state"] == "failed"); const auto valid = act("once", game.players.at(0).connection); game.accept(reply(1, {}, {valid}).dump(), NOW); game.enforce(NOW, execute); assert(executed == 1); game.accept(reply(1, {}, {valid}).dump(), NOW); game.enforce(NOW, execute); assert(executed == 1); auto changed = valid; changed["reason"] = "different action"; rejects([&] { game.accept(reply(1, {}, {changed}).dump(), NOW); }); auto expired = act("expired", game.players.at(0).connection); game.accept(reply(1, {}, {expired}).dump(), NOW + 31); game.enforce(NOW + 31, execute); assert(executed == 1); auto future = reply(2); future["serverTime"] = "2026-10-16T12:00:00Z"; rejects([&] { game.accept(future.dump(), NOW); }); rejects([&] { game.accept(reply(0).dump(), NOW); }); auto invalid = reply(2, {policy("mute")}); invalid["sanctions"][0]["guid"] = "bad guid"; rejects([&] { game.accept(invalid.dump(), NOW); }); assert(game.revision() == 1); for (const auto* malformed : {"{}", "[]", "not json", "{\"ok\":false}"}) rejects([&] { game.accept(malformed, NOW); }); auto too_many = reply(2); for (int i = 0; i < 1001; ++i) too_many["sanctions"].push_back(policy("mute")); rejects([&] { game.accept(too_many.dump(), NOW); }); rejects([&] { game.accept(std::string(1024 * 1024 + 1, ' '), NOW); }); rejects([&] { game.accept(reply(1, {policy("mute")}).dump(), NOW); }); std::vector effects; auto record = [&](const auto& kind, int, const auto&) { effects.push_back(kind); return "unknown"; }; game.accept(reply(2, {policy("mute")}).dump(), NOW); game.enforce(NOW, record); game.enforce(NOW + 1, record); assert(effects == std::vector{"mute"}); game.enforce(NOW + 31, record); assert(effects == (std::vector{"mute", "unmute"})); auto expired_snapshot = reply(2); expired_snapshot["serverTime"] = "2026-10-15T12:00:31.000Z"; game.accept(expired_snapshot.dump(), NOW); // server expiry wins even when local clock lags within allowed skew game.accept(reply(3, {policy("mute", "")}).dump(), NOW); game.enforce(NOW, record); game.accept(reply(4).dump(), NOW); game.enforce(NOW, record); assert(effects.back() == "unmute"); game.accept(reply(5, {policy("ban", "")}).dump(), NOW); auto disconnecting = [&](const auto& kind, int slot, const auto&) { assert(kind == "kick"); game.disconnect(slot); return "confirmed"; }; game.enforce(NOW, disconnecting); assert(game.players.empty()); // Slow host time and buffered responses cannot extend a one-time deadline. { runtime slow(root / "slow-clock", "server", "endpoint", 1, "credential-a"); slow.observe(0, "18446744073709551615", "one"); slow.accept(reply(1, {}, {act("slow", slow.players.at(0).connection)}).dump(), NOW - 60, 31000); const auto count = executed; slow.enforce(NOW - 60, execute); assert(executed == count); assert(slow.request()["receipts"][0]["state"] == "expired"); } // A faster next response cannot roll the conservative server-time anchor back. { runtime monotonic(root / "monotonic-clock", "server", "endpoint", 1, "credential-a"); monotonic.observe(0, "18446744073709551615", "one"); auto timed = reply(1, {policy("mute", "2026-10-15T12:00:07.000Z")}); const auto count = effects.size(); monotonic.accept(timed.dump(), NOW, 8000); monotonic.enforce(NOW, record); monotonic.accept(timed.dump(), NOW, 0); monotonic.enforce(NOW, record); assert(effects.size() == count); } // A superseded runtime cannot overwrite a newer session counter or execute. { const auto replacement_root = root / "replacement"; runtime older(replacement_root, "server", "endpoint", 1, "credential-a"); older.observe(0, "18446744073709551615", "one"); older.accept(reply(1, {}, {act("superseded", older.players.at(0).connection)}).dump(), NOW); runtime newer(replacement_root, "server", "endpoint", 1, "credential-a"); const auto count = executed; rejects([&] { older.enforce(NOW, execute); }); assert(executed == count); newer.accept(reply(2).dump(), NOW); assert(newer.request()["sessionSequence"] == 2); runtime recovered(replacement_root, "server", "endpoint", 1, "credential-a"); assert(recovered.request()["sessionSequence"] == 3 && recovered.revision() == 2); for (const auto& file : std::filesystem::directory_iterator(replacement_root)) std::filesystem::remove(file.path()); rejects([&] { recovered.accept(reply(3).dump(), NOW); }); // cache deletion cannot silently reset its sequence } // Managed mute ownership survives restart and allows revocation without clearing unrelated mutes. const auto mute_root = root / "mute-restart"; { runtime original(mute_root, "server", "endpoint", 1, "credential-a"); original.observe(0, "18446744073709551615", "one"); original.accept(reply(1, {policy("mute", "")}).dump(), NOW); original.enforce(NOW, record); } { runtime recovered(mute_root, "server", "endpoint", 1, "credential-a"); recovered.accept(reply(2).dump(), NOW); recovered.observe(1, "18446744073709551615", "one"); const auto before = effects.size(); recovered.enforce(NOW, record); assert(effects.size() == before + 1 && effects.back() == "unmute"); recovered.observe(2, "someone-else", "two"); recovered.enforce(NOW, record); assert(effects.size() == before + 1); } // Pre-dispatch disk failure must prevent the irreversible effect. game.observe(0, "18446744073709551615", "one"); game.accept(reply(6, {}, {act("disk", game.players.at(0).connection)}).dump(), NOW); std::filesystem::path journal; for (const auto& file : std::filesystem::directory_iterator(root)) if (file.path().extension() == ".json") journal = file.path(); auto temporary = journal; temporary += ".tmp"; std::filesystem::create_directory(temporary); const auto before_disk = executed; rejects([&] { game.enforce(NOW, execute); }); assert(executed == before_disk); std::filesystem::remove(temporary); runtime different_credential(root, "server", "https://controller/api/moderation/runtime", 1, "credential-b"); assert(different_credential.revision() == 0 && different_credential.request()["receipts"].empty()); // Corruption is visible: runtime never replaces a bad persistent cache with empty policy. { std::ofstream file(journal); file << "corrupt"; } rejects([&] { runtime broken(root, "server", "https://controller/api/moderation/runtime", 1, "credential-a"); }); std::cout << "moderation policy, identity, expiry, revocation, journal and schema checks passed\n"; }