Files
utun2/tests/test_chat_member.c
T

309 lines
21 KiB
C

/* Полный снимок участников не выдаёт успех после отказа SQL. */
#define _GNU_SOURCE
#include "../src/chat/chat_member.c"
#include "../src/routing_layer/topo_node_sqlite.h"
#include "../src/transport_layer/etcp.h"
#include "../src/transport_layer/etcp_connections.h"
#include "../lib/strbuf.h"
#include <assert.h>
static int fail_query;
enum fault { F_NONE, F_PREPARE, F_BIND, F_STEP, F_CHANGES, F_FINALIZE, F_EXEC, F_ALLOCATE, F_REALLOCATE };
static enum fault fault;
static const char* target;
static int bind_index, last_target, fail_rollback, arm_after_commit;
static sqlite3* reader;
static uint64_t group_id;
static char channel[32];
static unsigned member_events, mute_events, list_events;
static uint8_t detail[CHAT_MEMBER_DETAIL_MAX_SIZE];
static size_t detail_size;
static unsigned text_events;
static int expect_large_text;
static int matches(const char* sql) { return target && !strncmp(sql, target, strlen(target)); }
static int take(enum fault point, const char* sql, int index) {
if (fault != point || (sql && !matches(sql)) || (point == F_BIND && index != bind_index)) return 0;
fault = F_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member test: fault stage=%d index=%d target=%s", point, index, target); return 1;
}
extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**);
int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int size, sqlite3_stmt** st, const char** tail) {
if (take(F_PREPARE, sql, 0)) { *st = NULL; return SQLITE_NOMEM; }
return __real_sqlite3_prepare_v2(db, sql, size, st, tail);
}
extern int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int index, sqlite3_int64 value) {
if (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM;
return __real_sqlite3_bind_int64(st, index, value);
}
extern int __real_sqlite3_bind_int(sqlite3_stmt*, int, int);
int __wrap_sqlite3_bind_int(sqlite3_stmt* st, int index, int value) {
if (take(F_BIND, sqlite3_sql(st), index)) return SQLITE_NOMEM;
return __real_sqlite3_bind_int(st, index, value);
}
extern int __real_sqlite3_step(sqlite3_stmt*);
int __wrap_sqlite3_step(sqlite3_stmt* st) {
last_target = matches(sqlite3_sql(st)); if (take(F_STEP, sqlite3_sql(st), 0)) return SQLITE_IOERR;
return __real_sqlite3_step(st);
}
extern int __real_sqlite3_changes(sqlite3*);
int __wrap_sqlite3_changes(sqlite3* db) {
if (last_target && take(F_CHANGES, NULL, 0)) return 0;
return __real_sqlite3_changes(db);
}
extern int __real_sqlite3_exec(sqlite3*, const char*, int (*)(void*, int, char**, char**), void*, char**);
int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*cb)(void*, int, char**, char**), void* arg, char** error) {
if (fail_rollback && !strcmp(sql, "ROLLBACK")) { fail_rollback = 0; return SQLITE_IOERR; }
if (take(F_EXEC, sql, 0)) return SQLITE_IOERR;
return __real_sqlite3_exec(db, sql, cb, arg, error);
}
extern void* __real_u_malloc_impl(uint32_t, const char*);
void* __wrap_u_malloc_impl(uint32_t size, const char* location) {
if (fault == F_ALLOCATE && strstr(location, target)) { fault = F_NONE; return NULL; }
return __real_u_malloc_impl(size, location);
}
extern void* __real_u_realloc_impl(void*, uint32_t, const char*);
void* __wrap_u_realloc_impl(void* ptr, uint32_t size, const char* location) {
if (fault == F_REALLOCATE && strstr(location, target)) { fault = F_NONE; return NULL; }
return __real_u_realloc_impl(ptr, size, location);
}
extern int __real_sqlite3_finalize(sqlite3_stmt*);
int __wrap_sqlite3_finalize(sqlite3_stmt* st) {
const char* snapshot = "SELECT p.node_id";
int fail = fail_query && st && !strncmp(sqlite3_sql(st), snapshot, strlen(snapshot));
if (fail) { fail_query = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member test: injected snapshot finalize failure"); }
if (st && take(F_FINALIZE, sqlite3_sql(st), 0)) fail = 1;
int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc;
}
static int stored_mute(void) {
char sql[160]; snprintf(sql, sizeof(sql), "SELECT radio_muted FROM peers_%s WHERE node_id=7", channel); sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(reader, sql, -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
int value = sqlite3_column_int(st, 0); assert(sqlite3_finalize(st) == SQLITE_OK); return value;
}
static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int size) {
if (type == CHAT_EVT_STATUS_REFRESH || type == CHAT_EVT_CONN_METRICS) {
assert(size > 0);
if (expect_large_text) assert(size > 8192 && memmem(data, (size_t)size, "LINK#299", 8));
text_events++; return;
}
if (type == CHAT_EVT_MEMBER_DETAIL) {
assert(size >= 0 && (size_t)size <= sizeof(detail)); memcpy(detail, data, (size_t)size); detail_size = (size_t)size; return;
}
if (type == CHAT_EVT_MEMBER_LIST) {
assert(size >= data[0] + 3); uint16_t count; memcpy(&count, data + data[0] + 1, 2);
assert((size_t)size == data[0] + 3 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE); list_events++; return;
}
if (type != CHAT_EVT_MEMBER_UPDATED) return;
assert(sqlite3_get_autocommit(inst->topo_sqlite_db) && size == data[0] + 2 + CHAT_MEMBER_DISPLAY_SIZE);
const uint8_t* wire = data + data[0] + 2;
if (mf_get64(wire) == 7) assert(!!(wire[10] & CHAT_MEMBER_FLAG_RADIO_MUTED) == stored_mute());
member_events++;
}
static void mute(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node, int muted, void* arg) {
(void)arg; assert(group == group_id && node == 7 && sqlite3_get_autocommit(inst->topo_sqlite_db));
assert(stored_mute() == muted && radio_member_muted(inst, group, node) == muted); mute_events++;
if (arm_after_commit) { arm_after_commit = 0; target = "SELECT p.node_id"; fault = F_FINALIZE; }
}
static void refused_mute(struct UTUN_INSTANCE* inst) {
int before = stored_mute(); unsigned display = member_events, audio = mute_events;
assert(chat_core_toggle_member_radio_mute(inst, channel, 7) < 0 && fault == F_NONE);
assert(sqlite3_get_autocommit(inst->topo_sqlite_db) && stored_mute() == before && radio_member_muted(inst, group_id, 7) == before);
assert(display == member_events && audio == mute_events);
}
static void retry_mute(struct UTUN_INSTANCE* inst) {
int before = stored_mute(); unsigned display = member_events, audio = mute_events;
assert(!chat_core_toggle_member_radio_mute(inst, channel, 7));
assert(stored_mute() != before && member_events == display + 1 && mute_events == audio + 1);
}
static void snapshots(struct UTUN_INSTANCE* inst) {
unsigned cases = 0; target = "SELECT p.node_id"; uint8_t previous[CHAT_MEMBER_DISPLAY_SIZE], output[CHAT_MEMBER_DISPLAY_SIZE];
memset(previous, 0xa5, sizeof(previous));
for (int single = 0; single < 2; single++) for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) {
if (point == F_CHANGES || (point == F_BIND && !single)) continue;
bind_index = 1; fault = point;
if (single) {
memcpy(output, previous, sizeof(output));
assert(chat_core_get_single_member(inst, channel, 7, output) < 0 && !memcmp(output, previous, sizeof(output)));
assert(fault == F_NONE && !chat_core_get_single_member(inst, channel, 7, output));
} else {
uint8_t* wire = NULL; int count = 0;
assert(chat_core_get_member_list(inst, channel, &wire, &count) < 0 && !wire && !count);
assert(fault == F_NONE && !chat_core_get_member_list(inst, channel, &wire, &count) && count == 4); u_free(wire);
}
cases++;
}
for (enum fault point = F_ALLOCATE; point <= F_REALLOCATE; point++) {
uint8_t* wire = NULL; int count = 0; target = "chat_member.c"; fault = point;
assert(chat_core_get_member_list(inst, channel, &wire, &count) < 0 && !wire && !count && fault == F_NONE);
assert(!chat_core_get_member_list(inst, channel, &wire, &count) && count == 4); u_free(wire); cases++;
}
const char* invalid[] = {NULL, "", "0", "01", "1x", "9223372036854775808", "12345678901234567890"};
for (unsigned i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
uint8_t* wire = NULL; int count = 0;
assert(chat_core_get_member_list(inst, invalid[i], &wire, &count) < 0 && !wire && !count);
}
uint8_t* wire = NULL; int count = 0; assert(chat_core_get_member_list(NULL, channel, &wire, &count) < 0 && !wire && !count);
assert(!chat_core_member_online(NULL, channel, 7));
for (int rtt = 0; rtt < 2; rtt++) {
struct chat_member_list_req* req = u_calloc(1, sizeof(*req)); assert(req); strcpy(req->ch_id, channel);
if (rtt) chat_core_request_member_rtt_trampoline(req); else chat_core_request_member_list_trampoline(req);
req = u_malloc(sizeof(*req)); assert(req); req->inst = inst; memset(req->ch_id, '1', sizeof(req->ch_id));
if (rtt) chat_core_request_member_rtt_trampoline(req); else chat_core_request_member_list_trampoline(req);
}
struct chat_member_radio_mute_req* mute_req = u_malloc(sizeof(*mute_req)); assert(mute_req);
mute_req->inst = inst; mute_req->node_id = 7; memset(mute_req->ch_id, '1', sizeof(mute_req->ch_id));
chat_core_toggle_member_radio_mute_trampoline(mute_req);
on_member_props_changed(7, NULL, channel, inst); unsigned before = member_events;
target = "SELECT p.node_id"; fault = F_FINALIZE; on_member_props_changed(7, NULL, channel, inst);
assert(fault == F_NONE && before == member_events); on_member_props_changed(7, NULL, channel, inst); assert(member_events == before + 1);
struct chat_member_display source; memset(&source, 0xa5, sizeof(source)); strcpy(source.name, "N"); strcpy(source.avatar_file, "F");
chat_core_serialize_member(&source, output);
for (unsigned i = 16; i <= 78; i++) assert(!output[i]);
for (unsigned i = 88; i <= 156; i++) assert(!output[i]);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS member snapshots:%u SQL/allocation faults,retries,NULL/canonical guards,property event suppression,zero-padded strings", cases);
}
static int veto(void* arg) { (*(unsigned*)arg)++; return 1; }
static void mutes(struct UTUN_INSTANCE* inst) {
char update[96]; snprintf(update, sizeof(update), "UPDATE \"peers_%s\" SET radio_muted=", channel); target = update; unsigned cases = 0;
for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) {
int bindings = point == F_BIND ? 2 : 1;
for (int index = 1; index <= bindings; index++) {
bind_index = index; fault = point; refused_mute(inst); retry_mute(inst); cases++;
}
}
const char* exec[] = {"BEGIN IMMEDIATE /* radio mute */", "COMMIT /* radio mute */"};
for (unsigned i = 0; i < 2; i++) { target = exec[i]; fault = F_EXEC; refused_mute(inst); retry_mute(inst); cases++; }
assert(!radio_set_member_muted(inst, group_id, 7, 0));
target = "radio.c"; fault = F_REALLOCATE; refused_mute(inst); retry_mute(inst); cases++;
const char* actions[] = {"ABORT,'mute update'", "IGNORE"};
for (unsigned i = 0; i < 2; i++) {
char sql[256]; snprintf(sql, sizeof(sql), "CREATE TEMP TRIGGER refuse_mute BEFORE UPDATE OF radio_muted ON peers_%s "
"BEGIN SELECT RAISE(%s); END", channel, actions[i]);
assert(sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK); refused_mute(inst);
assert(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER refuse_mute", NULL, NULL, NULL) == SQLITE_OK); retry_mute(inst);
}
unsigned commits = 0; sqlite3_commit_hook(inst->topo_sqlite_db, veto, &commits); refused_mute(inst); assert(commits == 1);
sqlite3_commit_hook(inst->topo_sqlite_db, NULL, NULL); retry_mute(inst);
int saved = stored_mute(); unsigned display = member_events, audio = mute_events;
assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN;INSERT INTO ui_state(key,value) VALUES('foreign-mute','1')", NULL, NULL, NULL) == SQLITE_OK);
assert(radio_set_member_muted(inst, group_id, 7, !saved) < 0 && chat_core_toggle_member_radio_mute(inst, channel, 7) < 0);
assert(!sqlite3_get_autocommit(inst->topo_sqlite_db) && stored_mute() == saved && radio_member_muted(inst, group_id, 7) == saved);
assert(display == member_events && audio == mute_events);
assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK);
arm_after_commit = 1; retry_mute(inst); assert(!arm_after_commit && fault == F_FINALIZE);
uint8_t wire[CHAT_MEMBER_DISPLAY_SIZE]; assert(chat_core_get_single_member(inst, channel, 7, wire) < 0 && fault == F_NONE);
assert(!chat_core_get_single_member(inst, channel, 7, wire) && !!(wire[10] & CHAT_MEMBER_FLAG_RADIO_MUTED) == stored_mute());
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS member mute:%u SQL/allocation faults,retries,ABORT/IGNORE/veto,foreign transaction,read failure after commit;DB/cache/audio/events agree", cases);
}
static void details(struct UTUN_INSTANCE* inst) {
char query[96]; snprintf(query, sizeof(query), "SELECT 1 FROM \"peers_%s\" WHERE node_id=", channel); target = query;
for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) {
if (point == F_CHANGES) continue;
struct chat_member_detail_req* req = u_calloc(1, sizeof(*req)); assert(req);
*req = (struct chat_member_detail_req){inst, group_id, 7, 123}; bind_index = 1; fault = point; detail_size = 0;
chat_core_collect_member_detail_trampoline(req); struct chat_member_detail_view view;
assert(fault == F_NONE && !chat_core_parse_member_detail(detail, detail_size, &view));
assert(view.result == CHAT_MEMBER_DETAIL_ERROR && view.request_id == 123 && view.node_id == 7 && view.group_id == group_id);
}
struct chat_member_detail_req* req = u_calloc(1, sizeof(*req)); assert(req); req->inst = inst; req->request_id = 124;
chat_core_collect_member_detail_trampoline(req); struct chat_member_detail_view view;
assert(!chat_core_parse_member_detail(detail, detail_size, &view) && view.result == CHAT_MEMBER_DETAIL_INVALID && view.request_id == 124);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS member detail:4 SQL faults,complete addressed error replies and invalid request codec");
}
static void node_names(struct UTUN_INSTANCE* inst) {
target = "SELECT name FROM nodes"; char out[128];
for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) {
if (point == F_CHANGES) continue;
strcpy(out, "previous"); bind_index = 1; fault = point;
assert(chat_core_get_node_name(inst, inst->node_id, out, sizeof(out)) < 0 && !out[0] && fault == F_NONE);
assert(!chat_core_get_node_name(inst, inst->node_id, out, sizeof(out)) && !strcmp(out, inst->name));
}
out[0] = 'x'; assert(chat_core_get_node_name(inst, inst->node_id, out, 0) < 0 && out[0] == 'x');
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS node names:4 SQL faults/retries,cleared failure output and zero-size guard");
}
static void diagnostics(struct UTUN_INSTANCE* inst) {
struct ETCP_CONN connection = {.instance = inst, .peer_node_id = 9, .unacked_bytes = UINT32_MAX};
struct ETCP_LINK* links = u_calloc(300, sizeof(*links)); assert(links); connection.links = links;
struct ll_queue* acks = queue_new(inst->ua, 0, 0, 0, "member_test_acks"); assert(acks);
for (int i = 0; i < 300; i++) {
links[i].etcp = &connection; links[i].ack_q = acks; links[i].next = i < 299 ? links + i + 1 : NULL;
}
struct ll_entry* entry = queue_entry_new(sizeof(struct conn_queue_entry)); assert(entry);
struct conn_queue_entry* item = (struct conn_queue_entry*)entry->data; item->peer_node_id = 9; item->conn = &connection;
assert(!queue_data_put_with_index(inst->connections, entry)); expect_large_text = 1; unsigned before = text_events;
chat_core_collect_status_trampoline(inst);
struct chat_node_arg* req = u_malloc(sizeof(*req)); assert(req); *req = (struct chat_node_arg){inst, 9}; chat_core_collect_conn_metrics_trampoline(req);
assert(text_events == before + 2);
target = "strbuf.c"; fault = F_REALLOCATE; chat_core_collect_status_trampoline(inst); assert(fault == F_NONE && text_events == before + 2);
chat_core_collect_status_trampoline(inst); assert(text_events == before + 3); expect_large_text = 0;
queue_remove_data(inst->connections, entry); queue_entry_free(entry); u_free(links); queue_free(acks);
if (SIZE_MAX > UINT32_MAX) {
size_t baseline = u_get_allocated_count(); struct strbuf text; strbuf_init(&text, (size_t)UINT32_MAX + 1);
assert(!text.buf && !text.cap && !text.len && u_get_allocated_count() == baseline);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS diagnostics:300 links,full status/metrics beyond8KiB,OOM suppresses partial event,retry,allocator capacity boundary");
}
static void member_limit(struct UTUN_INSTANCE* inst) {
char sql[1024]; snprintf(sql, sizeof(sql), "WITH RECURSIVE n(value) AS(SELECT 100000 UNION ALL SELECT value+1 FROM n WHERE value<200000) "
"INSERT INTO peers_%s(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) "
"SELECT value,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n WHERE value<>%llu LIMIT(65535-(SELECT count(*) FROM peers_%s))",
channel, (unsigned long long)inst->node_id, channel);
assert(sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
uint8_t* wire = NULL; int count = 0; assert(!chat_core_get_member_list(inst, channel, &wire, &count) && count == UINT16_MAX);
assert(wire[8] && mf_get64(wire) == inst->node_id);
for (int i = 2; i < count; i++) assert(mf_get64(wire + (i - 1) * CHAT_MEMBER_DISPLAY_SIZE) < mf_get64(wire + i * CHAT_MEMBER_DISPLAY_SIZE));
u_free(wire); struct chat_member_list_req* req = u_malloc(sizeof(*req)); assert(req);
req->inst = inst; strcpy(req->ch_id, channel); unsigned before = list_events; chat_core_request_member_list_trampoline(req);
assert(list_events == before + 1);
uint64_t extra = inst->node_id == 9 ? 10 : 9;
assert(!topo_node_sqlite_member_placeholder_put(inst->topo_sqlite_db, channel, extra, inst->my_keys.public_key, inst->my_ed25519_pubkey));
wire = NULL; count = 0; assert(chat_core_get_member_list(inst, channel, &wire, &count) < 0 && !wire && !count);
req = u_malloc(sizeof(*req)); assert(req); req->inst = inst; strcpy(req->ch_id, channel); chat_core_request_member_list_trampoline(req);
assert(list_events == before + 1 && !topo_node_sqlite_member_del(inst->topo_sqlite_db, channel, extra));
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS member limit:65535 exact sorted members and complete event;65536 refused without wrapped count/truncated event");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_INFO);
size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0);
char directory[] = "/tmp/utun-member-sql-XXXXXX"; assert(mkdtemp(directory)); char config[512], database[512];
snprintf(config, sizeof(config), "[global]\nmy_node_name=member test\ndb_path=%s\n[ntp]\nenabled=no\n"
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", directory);
struct UASYNC* ua = uasync_create(); assert(ua);
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config);
assert(inst && !utun_core_start(inst) && !chat_service_start(inst));
group_id = chat_core_create_channel_auto(inst, "member SQL"); assert(group_id);
snprintf(channel, sizeof(channel), "%llu", (unsigned long long)group_id);
uint8_t* wire = NULL; int count = 0; fail_query = 1;
int rc = chat_core_get_member_list(inst, channel, &wire, &count);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "member test: finalize fault result=%d count=%d fault_pending=%d", rc, count, fail_query);
assert(rc < 0 && !wire && !count && !fail_query);
assert(!chat_core_get_member_list(inst, channel, &wire, &count) && count == 1); u_free(wire);
const uint64_t peers[] = {7, 8, UINT64_MAX};
for (unsigned i = 0; i < 3; i++) assert(!topo_node_sqlite_member_placeholder_put(inst->topo_sqlite_db, channel, peers[i],
inst->my_keys.public_key, inst->my_ed25519_pubkey));
snprintf(database, sizeof(database), "%s/chats.db", directory); assert(sqlite3_open_v2(database, &reader, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK);
chat_event_set_handler(inst, event); radio_set_mute_cb(inst, mute, NULL);
snapshots(inst); mutes(inst); details(inst); node_names(inst); diagnostics(inst); member_limit(inst);
assert(!radio_set_member_muted(inst, group_id, 7, 0)); unsigned display = member_events, audio = mute_events;
target = "COMMIT /* radio mute */"; fault = F_EXEC; fail_rollback = 1; inst->running = 1;
assert(chat_core_toggle_member_radio_mute(inst, channel, 7) < 0 && fault == F_NONE && !fail_rollback && !inst->running);
assert(!sqlite3_get_autocommit(inst->topo_sqlite_db) && !stored_mute() && !radio_member_muted(inst, group_id, 7));
assert(display == member_events && audio == mute_events);
utun_instance_destroy(inst); assert(!stored_mute() && sqlite3_close(reader) == SQLITE_OK);
uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS member snapshot SQL refusal and retry"); return 0;
}