Files
tomato-testing/auto_tests/scenarios/scenario_group_moderation_test.c
Green Sky 565efa4f39 Squashed 'external/toxcore/c-toxcore/' changes from 1828c5356..c9cdae001
c9cdae001 fix(toxav): remove extra copy of video frame on encode
4f6d4546b test: Improve the fake network library.
a2581e700 refactor(toxcore): generate `Friend_Request` and `Dht_Nodes_Response`
2aaa11770 refactor(toxcore): use Tox_Memory in generated events
5c367452b test(toxcore): fix incorrect mutex in tox_scenario_get_time
8f92e710f perf: Add a timed limit of number of cookie requests.
695b6417a test: Add some more simulated network support.
815ae9ce9 test(toxcore): fix thread-safety in scenario framework
6d85c754e test(toxcore): add unit tests for net_crypto
9c22e79cc test(support): add SimulatedEnvironment for deterministic testing
f34fcb195 chore: Update windows Dockerfile to debian stable (trixie).
ece0e8980 fix(group_moderation): allow validating unsorted sanction list signatures
a4fa754d7 refactor: rename struct Packet to struct Net_Packet
d6f330f85 cleanup: Fix some warnings from coverity.
e206bffa2 fix(group_chats): fix sync packets reverting topics
0e4715598 test: Add new scenario testing framework.
668291f44 refactor(toxcore): decouple Network_Funcs from sockaddr via IP_Port
fc4396cef fix: potential division by zero in toxav and unsafe hex parsing
8e8b352ab refactor: Add nullable annotations to struct members.
7740bb421 refactor: decouple net_crypto from DHT
1936d4296 test: add benchmark for toxav audio and video
46bfdc2df fix: correct printf format specifiers for unsigned integers
REVERT: 1828c5356 fix(toxav): remove extra copy of video frame on encode

git-subtree-dir: external/toxcore/c-toxcore
git-subtree-split: c9cdae001341e701fca980c9bb9febfeb95d2902
2026-01-11 14:42:31 +01:00

368 lines
15 KiB
C

#include "framework/framework.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define NUM_PEERS 5
#define GROUP_NAME "Moderation Test Group"
#define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1)
typedef struct {
uint32_t group_number;
uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE];
bool chat_id_ready;
uint32_t peer_count;
bool connected;
Tox_Group_Role self_role;
Tox_Group_Voice_State voice_state;
bool kicked;
uint32_t peer_ids[NUM_PEERS]; // Map node index to group peer_id
Tox_Group_Mod_Event last_mod_event;
uint32_t last_mod_target;
} ModState;
static void on_group_self_join(const Tox_Event_Group_Self_Join *event, void *user_data)
{
ToxNode *self = (ToxNode *)user_data;
ModState *state = (ModState *)tox_node_get_script_ctx(self);
state->connected = true;
uint32_t group_number = tox_event_group_self_join_get_group_number(event);
uint32_t self_id = tox_group_self_get_peer_id(tox_node_get_tox(self), group_number, nullptr);
state->self_role = tox_group_self_get_role(tox_node_get_tox(self), group_number, nullptr);
tox_node_log(self, "Joined group %u (Peer ID: %u) with role %s", group_number, self_id, tox_group_role_to_string(state->self_role));
}
static void on_group_peer_join(const Tox_Event_Group_Peer_Join *event, void *user_data)
{
ToxNode *self = (ToxNode *)user_data;
ModState *state = (ModState *)tox_node_get_script_ctx(self);
uint32_t group_number = tox_event_group_peer_join_get_group_number(event);
uint32_t peer_id = tox_event_group_peer_join_get_peer_id(event);
tox_node_log(self, "Peer %u joined the group", peer_id);
state->peer_count++;
Tox_Err_Group_Peer_Query q_err;
size_t length = tox_group_peer_get_name_size(tox_node_get_tox(self), group_number, peer_id, &q_err);
if (q_err == TOX_ERR_GROUP_PEER_QUERY_OK && length > 0) {
uint8_t name[TOX_MAX_NAME_LENGTH];
tox_group_peer_get_name(tox_node_get_tox(self), group_number, peer_id, name, &q_err);
if (q_err == TOX_ERR_GROUP_PEER_QUERY_OK) {
tox_node_log(self, "Peer %u name identified: %.*s", peer_id, (int)length, name);
if (length == 7 && memcmp(name, "Founder", 7) == 0) {
state->peer_ids[0] = peer_id;
} else if (length >= 5 && memcmp(name, "Peer", 4) == 0) {
int idx = atoi((const char *)name + 4);
if (idx > 0 && idx < NUM_PEERS) {
state->peer_ids[idx] = peer_id;
}
}
}
}
}
static void on_group_moderation(const Tox_Event_Group_Moderation *event, void *user_data)
{
ToxNode *self = (ToxNode *)user_data;
ModState *state = (ModState *)tox_node_get_script_ctx(self);
state->last_mod_event = tox_event_group_moderation_get_mod_type(event);
state->last_mod_target = tox_event_group_moderation_get_target_peer_id(event);
Tox_Err_Group_Self_Query err;
state->self_role = tox_group_self_get_role(tox_node_get_tox(self), state->group_number, &err);
if (state->last_mod_event == TOX_GROUP_MOD_EVENT_KICK && state->last_mod_target == tox_group_self_get_peer_id(tox_node_get_tox(self), state->group_number, nullptr)) {
state->kicked = true;
}
tox_node_log(self, "Moderation event: %s on peer %u. My role is now %s",
tox_group_mod_event_to_string(state->last_mod_event),
state->last_mod_target,
tox_group_role_to_string(state->self_role));
}
static void on_group_voice_state(const Tox_Event_Group_Voice_State *event, void *user_data)
{
ToxNode *self = (ToxNode *)user_data;
ModState *state = (ModState *)tox_node_get_script_ctx(self);
state->voice_state = tox_event_group_voice_state_get_voice_state(event);
tox_node_log(self, "Voice state updated: %u", state->voice_state);
}
static void common_init(ToxNode *self, ModState *state)
{
Tox_Dispatch *dispatch = tox_node_get_dispatch(self);
tox_events_callback_group_self_join(dispatch, on_group_self_join);
tox_events_callback_group_peer_join(dispatch, on_group_peer_join);
tox_events_callback_group_moderation(dispatch, on_group_moderation);
tox_events_callback_group_voice_state(dispatch, on_group_voice_state);
for (int i = 0; i < NUM_PEERS; ++i) {
state->peer_ids[i] = UINT32_MAX;
}
tox_node_log(self, "Waiting for self connection...");
tox_node_wait_for_self_connected(self);
tox_node_log(self, "Connected!");
}
static void wait_for_peer_role(ToxNode *self, uint32_t peer_idx, Tox_Group_Role expected_role)
{
const ToxNode *peer = tox_scenario_get_node(tox_node_get_scenario(self), peer_idx);
const ModState *peer_view = (const ModState *)tox_node_get_peer_ctx(peer);
tox_node_log(self, "Waiting for Peer %u to have role %s", peer_idx, tox_group_role_to_string(expected_role));
WAIT_UNTIL(peer_view->self_role == expected_role);
tox_node_log(self, "Peer %u now has role %s", peer_idx, tox_group_role_to_string(expected_role));
}
static void founder_script(ToxNode *self, void *ctx)
{
ModState *state = (ModState *)ctx;
Tox *tox = tox_node_get_tox(self);
common_init(self, state);
Tox_Err_Group_New err_new;
state->group_number = tox_group_new(tox, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)GROUP_NAME, GROUP_NAME_LEN, (const uint8_t *)"Founder", 7, &err_new);
ck_assert(err_new == TOX_ERR_GROUP_NEW_OK);
state->self_role = TOX_GROUP_ROLE_FOUNDER;
state->peer_ids[0] = tox_group_self_get_peer_id(tox, state->group_number, nullptr);
tox_group_get_chat_id(tox, state->group_number, state->chat_id, nullptr);
state->chat_id_ready = true;
// Barrier 1: Wait for all peers to join and be seen by everyone
tox_scenario_barrier_wait(self);
WAIT_UNTIL(state->peer_count == NUM_PEERS - 1);
tox_node_log(self, "All peers joined");
// Wait until we know all peer IDs
for (int i = 1; i < NUM_PEERS; ++i) {
WAIT_UNTIL(state->peer_ids[i] != UINT32_MAX);
}
tox_node_log(self, "All peer IDs identified");
tox_scenario_barrier_wait(self); // Sync point after everyone sees everyone
// Barrier 2: Peer 1 becomes Moderator
tox_group_set_role(tox, state->group_number, state->peer_ids[1], TOX_GROUP_ROLE_MODERATOR, nullptr);
for (int i = 0; i < NUM_PEERS; ++i) {
wait_for_peer_role(self, 1, TOX_GROUP_ROLE_MODERATOR);
}
tox_scenario_barrier_wait(self);
// Barrier 3: Peer 2 and 3 become Observer
tox_group_set_role(tox, state->group_number, state->peer_ids[2], TOX_GROUP_ROLE_OBSERVER, nullptr);
tox_group_set_role(tox, state->group_number, state->peer_ids[3], TOX_GROUP_ROLE_OBSERVER, nullptr);
for (int i = 0; i < NUM_PEERS; ++i) {
wait_for_peer_role(self, 2, TOX_GROUP_ROLE_OBSERVER);
wait_for_peer_role(self, 3, TOX_GROUP_ROLE_OBSERVER);
}
tox_scenario_barrier_wait(self);
// Barrier 4: Voice State tests
tox_node_log(self, "Setting voice state to MODERATOR");
tox_group_set_voice_state(tox, state->group_number, TOX_GROUP_VOICE_STATE_MODERATOR, nullptr);
tox_scenario_barrier_wait(self); // Phase 1 set
tox_scenario_barrier_wait(self); // Phase 1 done
tox_node_log(self, "Setting voice state to FOUNDER");
tox_group_set_voice_state(tox, state->group_number, TOX_GROUP_VOICE_STATE_FOUNDER, nullptr);
tox_scenario_barrier_wait(self); // Phase 2 set
tox_scenario_barrier_wait(self); // Phase 2 done
tox_node_log(self, "Setting voice state to ALL");
tox_group_set_voice_state(tox, state->group_number, TOX_GROUP_VOICE_STATE_ALL, nullptr);
tox_scenario_barrier_wait(self); // Phase 3 set
tox_scenario_barrier_wait(self); // Phase 3 done
// Barrier 5: Peer 1 (Mod) promotes Peer 2 back to User
tox_scenario_barrier_wait(self);
wait_for_peer_role(self, 2, TOX_GROUP_ROLE_USER);
// Barrier 6: Founder promotes Peer 3 to Moderator
tox_group_set_role(tox, state->group_number, state->peer_ids[3], TOX_GROUP_ROLE_MODERATOR, nullptr);
wait_for_peer_role(self, 3, TOX_GROUP_ROLE_MODERATOR);
tox_scenario_barrier_wait(self);
// Barrier 7: Moderator (Peer 1) attempts to kick/demote Founder (should fail)
tox_scenario_barrier_wait(self);
// Barrier 8: Founder kicks Moderator (Peer 1)
tox_group_kick_peer(tox, state->group_number, state->peer_ids[1], nullptr);
tox_scenario_barrier_wait(self);
// Barrier 9: Founder demotes Moderator (Peer 3) to User
tox_group_set_role(tox, state->group_number, state->peer_ids[3], TOX_GROUP_ROLE_USER, nullptr);
wait_for_peer_role(self, 3, TOX_GROUP_ROLE_USER);
tox_scenario_barrier_wait(self);
tox_scenario_barrier_wait(self); // Done
}
static void peer_script(ToxNode *self, void *ctx)
{
ModState *state = (ModState *)ctx;
Tox *tox = tox_node_get_tox(self);
common_init(self, state);
const ToxNode *founder = tox_scenario_get_node(tox_node_get_scenario(self), 0);
const ModState *founder_view = (const ModState *)tox_node_get_peer_ctx(founder);
while (!founder_view->chat_id_ready) {
tox_scenario_yield(self);
}
tox_node_log(self, "Got chat ID from founder");
char name[16];
snprintf(name, sizeof(name), "Peer%u", tox_node_get_index(self));
Tox_Err_Group_Join err_join;
state->group_number = tox_group_join(tox, founder_view->chat_id, (const uint8_t *)name, strlen(name), nullptr, 0, &err_join);
if (state->group_number == UINT32_MAX) {
tox_node_log(self, "tox_group_join failed with error %u", err_join);
}
ck_assert(state->group_number != UINT32_MAX);
WAIT_UNTIL(state->connected);
uint32_t self_id = tox_group_self_get_peer_id(tox, state->group_number, nullptr);
state->peer_ids[tox_node_get_index(self)] = self_id;
tox_scenario_barrier_wait(self); // Barrier 1: Joined
// Wait until we know all peer IDs
for (uint32_t i = 0; i < NUM_PEERS; ++i) {
if (tox_node_get_index(self) != i) {
WAIT_UNTIL(state->peer_ids[i] != UINT32_MAX);
}
}
tox_scenario_barrier_wait(self); // Sync point after everyone sees everyone
tox_scenario_barrier_wait(self); // Barrier 2: Peer 1 Moderator
wait_for_peer_role(self, 1, TOX_GROUP_ROLE_MODERATOR);
tox_scenario_barrier_wait(self); // Barrier 3: Peer 2/3 Observer
wait_for_peer_role(self, 2, TOX_GROUP_ROLE_OBSERVER);
wait_for_peer_role(self, 3, TOX_GROUP_ROLE_OBSERVER);
// Barrier 4: Voice State tests
// Sub-phase 1: MODERATOR
tox_scenario_barrier_wait(self); // Phase 1 set
WAIT_UNTIL(state->voice_state == TOX_GROUP_VOICE_STATE_MODERATOR);
Tox_Err_Group_Send_Message err_msg;
tox_group_send_message(tox, state->group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)"hello", 5, &err_msg);
if (state->self_role == TOX_GROUP_ROLE_MODERATOR || state->self_role == TOX_GROUP_ROLE_FOUNDER) {
if (err_msg != TOX_ERR_GROUP_SEND_MESSAGE_OK) {
tox_node_log(self, "Expected OK, got %u. Role: %s", err_msg, tox_group_role_to_string(state->self_role));
}
ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_OK);
} else {
if (err_msg != TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS) {
tox_node_log(self, "Expected PERMISSIONS, got %u. Role: %s", err_msg, tox_group_role_to_string(state->self_role));
}
ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS);
}
tox_scenario_barrier_wait(self); // Phase 1 done
// Sub-phase 2: FOUNDER
tox_scenario_barrier_wait(self); // Phase 2 set
WAIT_UNTIL(state->voice_state == TOX_GROUP_VOICE_STATE_FOUNDER);
tox_group_send_message(tox, state->group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)"hello", 5, &err_msg);
if (state->self_role == TOX_GROUP_ROLE_FOUNDER) {
ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_OK);
} else {
ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS);
}
tox_scenario_barrier_wait(self); // Phase 2 done
// Sub-phase 3: ALL
tox_scenario_barrier_wait(self); // Phase 3 set
WAIT_UNTIL(state->voice_state == TOX_GROUP_VOICE_STATE_ALL);
tox_group_send_message(tox, state->group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)"hello", 5, &err_msg);
if (state->self_role == TOX_GROUP_ROLE_OBSERVER) {
ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS);
} else {
ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_OK);
}
tox_scenario_barrier_wait(self); // Phase 3 done
// Barrier 5: Peer 1 (Mod) promotes Peer 2 back to User
if (tox_node_get_index(self) == 1) { // Peer 1
uint32_t peer2_id = state->peer_ids[2];
tox_group_set_role(tox, state->group_number, peer2_id, TOX_GROUP_ROLE_USER, nullptr);
}
tox_scenario_barrier_wait(self);
wait_for_peer_role(self, 2, TOX_GROUP_ROLE_USER);
// Barrier 6: Founder promotes Peer 3 to Moderator
tox_scenario_barrier_wait(self);
wait_for_peer_role(self, 3, TOX_GROUP_ROLE_MODERATOR);
// Barrier 7: Moderator (Peer 1) attempts to kick/demote Founder (should fail)
if (tox_node_get_index(self) == 1) {
Tox_Err_Group_Kick_Peer err_kick;
uint32_t founder_peer_id = state->peer_ids[0];
tox_group_kick_peer(tox, state->group_number, founder_peer_id, &err_kick);
ck_assert(err_kick != TOX_ERR_GROUP_KICK_PEER_OK);
Tox_Err_Group_Set_Role err_role;
tox_group_set_role(tox, state->group_number, founder_peer_id, TOX_GROUP_ROLE_USER, &err_role);
ck_assert(err_role != TOX_ERR_GROUP_SET_ROLE_OK);
}
tox_scenario_barrier_wait(self);
// Barrier 8: Founder kicks Moderator (Peer 1)
tox_scenario_barrier_wait(self);
if (tox_node_get_index(self) == 1) {
WAIT_UNTIL(state->kicked);
return; // Exit script
}
// Barrier 9: Founder demotes Moderator (Peer 3) to User
tox_scenario_barrier_wait(self);
wait_for_peer_role(self, 3, TOX_GROUP_ROLE_USER);
tox_scenario_barrier_wait(self); // Done
}
int main(int argc, char *argv[])
{
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
ToxScenario *s = tox_scenario_new(argc, argv, 300000); // 5 virtual minutes
ModState states[NUM_PEERS] = {0};
ToxNode *nodes[NUM_PEERS];
nodes[0] = tox_scenario_add_node(s, "Founder", founder_script, &states[0], sizeof(ModState));
static char aliases[NUM_PEERS][16];
for (int i = 1; i < NUM_PEERS; ++i) {
snprintf(aliases[i], sizeof(aliases[i]), "Peer%d", i);
nodes[i] = tox_scenario_add_node(s, aliases[i], peer_script, &states[i], sizeof(ModState));
}
for (int i = 0; i < NUM_PEERS; ++i) {
for (int j = 0; j < NUM_PEERS; ++j) {
if (i != j) {
tox_node_bootstrap(nodes[i], nodes[j]);
tox_node_friend_add(nodes[i], nodes[j]);
}
}
}
ToxScenarioStatus res = tox_scenario_run(s);
if (res != TOX_SCENARIO_DONE) {
tox_scenario_log(s, "Test failed with status %u", res);
return 1;
}
tox_scenario_free(s);
return 0;
}
#undef GROUP_NAME
#undef GROUP_NAME_LEN
#undef NUM_PEERS