[Report] Add report reopen action for closed reports

- New Command_ReportReopen moderator command (extension 1206) with server
  handler that reopens resolved/dismissed reports, resets resolution state
  and notified, and records the reopening note in the audit log
- Reopen... button in the report queue action bar, gated to resolved and
  dismissed reports, with optional note prompt and tooltip
- Success confirmations for Assign, Resolve/Dismiss and Reopen now survive
  the list refresh via a pending status message instead of being clobbered
- Dispatch/auth coverage in server_moderator_role_test
This commit is contained in:
Lukas Brübach 2026-10-07 22:56:00 +02:00
parent d0991b68f6
commit a51f564b3c
9 changed files with 269 additions and 2 deletions

View file

@ -11,6 +11,7 @@ add_test(NAME playmat_resolver_test COMMAND playmat_resolver_test)
add_test(NAME server_card_counter_test COMMAND server_card_counter_test)
add_test(NAME server_counter_test COMMAND server_counter_test)
add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test)
add_test(NAME server_moderator_role_test COMMAND server_moderator_role_test)
add_test(NAME server_developer_role_test COMMAND server_developer_role_test)
add_test(NAME server_game_join_test COMMAND server_game_join_test)
add_test(NAME warning_categories_test COMMAND warning_categories_test)
@ -39,6 +40,7 @@ add_executable(deck_hash_performance_test deck_hash_performance_test.cpp)
add_executable(server_card_counter_test server_card_counter_test.cpp)
add_executable(server_counter_test server_counter_test.cpp)
add_executable(server_rate_limiter_test server_rate_limiter_test.cpp)
add_executable(server_moderator_role_test server_moderator_role_test.cpp)
add_executable(server_developer_role_test server_developer_role_test.cpp)
add_executable(server_game_join_test server_game_join_test.cpp)
add_executable(warning_categories_test warning_categories_test.cpp)
@ -133,6 +135,7 @@ if(NOT GTEST_FOUND)
add_dependencies(server_card_counter_test gtest)
add_dependencies(server_counter_test gtest)
add_dependencies(server_rate_limiter_test gtest)
add_dependencies(server_moderator_role_test gtest)
add_dependencies(server_developer_role_test gtest)
add_dependencies(server_game_join_test gtest)
add_dependencies(warning_categories_test gtest)
@ -176,6 +179,10 @@ target_link_libraries(
target_link_libraries(
server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
)
target_link_libraries(
server_moderator_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
${TEST_QT_MODULES}
)
target_link_libraries(
server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
${TEST_QT_MODULES}

View file

@ -0,0 +1,150 @@
/** @file server_moderator_role_test.cpp
* @brief Tests for the moderator staff role authorization and dispatch.
* @ingroup Tests
*/
#include <gtest/gtest.h>
#include <libcockatrice/network/server/remote/server.h>
#include <libcockatrice/network/server/remote/server_protocolhandler.h>
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
#include <libcockatrice/protocol/pb/commands.pb.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
// The server_remote library references the global RNG, which is normally
// defined by the servatrice/client executable main(). Provide a stub so the
// unit test can link against it.
RNG_Abstract *rng = nullptr;
namespace
{
class TestModeratorHandler : public Server_ProtocolHandler
{
public:
explicit TestModeratorHandler(Server *_server) : Server_ProtocolHandler(_server, nullptr)
{
}
QString getAddress() const override
{
return {};
}
QString getConnectionType() const override
{
return {};
}
// Buffer the last response code sent to the client so tests can assert on
// the outcome of processCommandContainer().
Response::ResponseCode lastResponseCode = Response::RespNothing;
int dispatchCount = 0;
protected:
void transmitProtocolItem(const ServerMessage &item) override
{
if (item.message_type() == ServerMessage::RESPONSE) {
lastResponseCode = item.response().response_code();
}
}
Response::ResponseCode
processExtendedModeratorCommand(int cmdType, const ModeratorCommand &, ResponseContainer &) override
{
++dispatchCount;
// Fail closed for anything not explicitly handled.
if (cmdType != ModeratorCommand::REPORT_REOPEN) {
return Response::RespFunctionNotAllowed;
}
return Response::RespOk;
}
};
class ModeratorRoleTest : public ::testing::Test
{
protected:
Server server;
TestModeratorHandler handler{&server};
void setUserLevel(uint32_t level)
{
ServerInfo_User user;
user.set_user_level(level);
handler.setUserInfo(user);
}
};
TEST_F(ModeratorRoleTest, RejectsWhenNotLoggedIn)
{
CommandContainer cont;
cont.add_moderator_command();
handler.processCommandContainer(cont);
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
EXPECT_EQ(handler.dispatchCount, 0);
}
TEST_F(ModeratorRoleTest, RejectsPlainUser)
{
setUserLevel(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
CommandContainer cont;
cont.add_moderator_command();
handler.processCommandContainer(cont);
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
EXPECT_EQ(handler.dispatchCount, 0);
}
TEST_F(ModeratorRoleTest, DispatchesToModeratorCommandForModerator)
{
setUserLevel(ServerInfo_User::IsModerator);
CommandContainer cont;
ModeratorCommand *cmd = cont.add_moderator_command();
cmd->MutableExtension(Command_ReportReopen::ext);
handler.processCommandContainer(cont);
EXPECT_EQ(handler.lastResponseCode, Response::RespOk);
EXPECT_EQ(handler.dispatchCount, 1);
}
TEST_F(ModeratorRoleTest, DispatchesToModeratorCommandForAdmin)
{
setUserLevel(ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin);
CommandContainer cont;
ModeratorCommand *cmd = cont.add_moderator_command();
cmd->MutableExtension(Command_ReportReopen::ext);
handler.processCommandContainer(cont);
EXPECT_EQ(handler.lastResponseCode, Response::RespOk);
EXPECT_EQ(handler.dispatchCount, 1);
}
TEST_F(ModeratorRoleTest, RejectsDeveloperThatIsNotModerator)
{
setUserLevel(ServerInfo_User::IsDeveloper);
CommandContainer cont;
cont.add_moderator_command();
handler.processCommandContainer(cont);
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
EXPECT_EQ(handler.dispatchCount, 0);
}
TEST_F(ModeratorRoleTest, FailClosedForUnknownModeratorCommand)
{
setUserLevel(ServerInfo_User::IsModerator);
CommandContainer cont;
cont.add_moderator_command(); // no extension set -> getPbExtension() returns -1
handler.processCommandContainer(cont);
EXPECT_EQ(handler.lastResponseCode, Response::RespFunctionNotAllowed);
EXPECT_EQ(handler.dispatchCount, 1);
}
} // namespace
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}