From 09e3ba62efa190332e56f1e6dc28741080b807d9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 07:27:44 +0100 Subject: [PATCH] test(chat): verify preference rollback, committed sound delivery, and request validation --- tests/Makefile.am | 4 +- tests/test_chat_preferences.c | 140 +++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/tests/Makefile.am b/tests/Makefile.am index a9209572..4d6db00f 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -342,7 +342,9 @@ test_chat_json_LDFLAGS = -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_bind_t test_chat_profile_SOURCES = test_chat_profile.c test_chat_preferences_SOURCES = test_chat_preferences.c test_chat_preferences_LDADD = $(LIBUTUN) $(COMMON_LIBS) -test_chat_preferences_LDFLAGS = -Wl,--wrap=sqlite3_finalize +test_chat_preferences_LDFLAGS = -Wl,--wrap=sqlite3_finalize -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_bind_text \ + -Wl,--wrap=sqlite3_step -Wl,--wrap=sqlite3_changes -Wl,--wrap=sqlite3_exec -Wl,--wrap=sqlite3_column_text \ + -Wl,--wrap=chat_sync_broadcast_sound_setting -Wl,--wrap=chat_core_apply_group_autoconnect test_chat_profile_LDADD = $(LIBUTUN) $(COMMON_LIBS) test_chat_profile_LDFLAGS = -Wl,--wrap=sqlite3_finalize -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_bind_text \ -Wl,--wrap=sqlite3_bind_blob -Wl,--wrap=sqlite3_bind_int64 -Wl,--wrap=sqlite3_bind_int -Wl,--wrap=sqlite3_bind_null \ diff --git a/tests/test_chat_preferences.c b/tests/test_chat_preferences.c index 70414636..f51f365a 100644 --- a/tests/test_chat_preferences.c +++ b/tests/test_chat_preferences.c @@ -2,6 +2,7 @@ #define _GNU_SOURCE #include "../src/utun_instance.h" #include "../src/chat/chat_core.h" +#include "../src/chat/chat_setting.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include @@ -9,11 +10,50 @@ #include static int fail_finalize; +enum fault { F_NONE, F_PREPARE, F_BIND, F_STEP, F_CHANGES, F_FINALIZE, F_EXEC, F_TEXT }; +static enum fault fault; +static const char* target; +static int argument, last_target, fail_rollback, observe_sound; +static unsigned broadcasts, autoconnect_calls; +static sqlite3* reader; +static char channel[32]; + +static int matches(const char* sql) { return sql && 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 || point == F_TEXT) && index != argument)) return 0; + fault = F_NONE; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "preferences test: SQL fault point=%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 bytes, sqlite3_stmt** st, const char** tail) { + if (take(F_PREPARE, sql, 0)) { *st = NULL; return SQLITE_NOMEM; } + return __real_sqlite3_prepare_v2(db, sql, bytes, st, tail); +} +extern int __real_sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void (*)(void*)); +int __wrap_sqlite3_bind_text(sqlite3_stmt* st, int index, const char* text, int bytes, void (*destroy)(void*)) { + return take(F_BIND, sqlite3_sql(st), index) ? SQLITE_NOMEM : __real_sqlite3_bind_text(st, index, text, bytes, destroy); +} +extern int __real_sqlite3_step(sqlite3_stmt*); +int __wrap_sqlite3_step(sqlite3_stmt* st) { + last_target = matches(sqlite3_sql(st)); + return take(F_STEP, sqlite3_sql(st), 0) ? SQLITE_IOERR : __real_sqlite3_step(st); +} +extern int __real_sqlite3_changes(sqlite3*); +int __wrap_sqlite3_changes(sqlite3* db) { return last_target && take(F_CHANGES, NULL, 0) ? 0 : __real_sqlite3_changes(db); } +extern const unsigned char* __real_sqlite3_column_text(sqlite3_stmt*, int); +const unsigned char* __wrap_sqlite3_column_text(sqlite3_stmt* st, int index) { + return take(F_TEXT, sqlite3_sql(st), index) ? NULL : __real_sqlite3_column_text(st, index); +} +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 (*fn)(void*, int, char**, char**), void* arg, char** error) { + if (fail_rollback && !strcmp(sql, "ROLLBACK")) { fail_rollback = 0; return SQLITE_IOERR; } + return take(F_EXEC, sql, 0) ? SQLITE_IOERR : __real_sqlite3_exec(db, sql, fn, arg, error); +} extern int __real_sqlite3_finalize(sqlite3_stmt*); int __wrap_sqlite3_finalize(sqlite3_stmt* st) { const char* sql = st ? sqlite3_sql(st) : NULL; - int fail = fail_finalize && sql && !strncmp(sql, "INSERT OR REPLACE INTO ui_state", 30); + int fail = fail_finalize && sql && strstr(sql, "INTO ui_state"); if (fail) fail_finalize = 0; + fail |= sql && take(F_FINALIZE, sql, 0); int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc; } @@ -24,6 +64,91 @@ static void value_is(sqlite3* db, const char* expected) { assert(sqlite3_finalize(st) == SQLITE_OK); } +extern void __real_chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE*, const char*, int); +void __wrap_chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE* inst, const char* ch, int on) { + if (observe_sound) { + assert(sqlite3_get_autocommit(inst->topo_sqlite_db) && !strcmp(ch, channel)); + char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch); + sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(reader, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); + const char* text = (const char*)sqlite3_column_text(st, 0); assert(text && text[0] == '0' + on && !text[1]); + assert(sqlite3_finalize(st) == SQLITE_OK); broadcasts++; + } + __real_chat_sync_broadcast_sound_setting(inst, ch, on); +} +extern void __real_chat_core_apply_group_autoconnect(struct UTUN_INSTANCE*); +void __wrap_chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst) { + autoconnect_calls++; __real_chat_core_apply_group_autoconnect(inst); +} + +static void write_faults(struct UTUN_INSTANCE* inst, int sound) { + const char* queries[] = {"SELECT value FROM ui_state", "INSERT INTO ui_state"}; unsigned cases = 0; + for (size_t q = 0; q < 2; q++) for (enum fault point = F_PREPARE; point <= F_TEXT; point++) { + if (point == F_EXEC || (point == F_CHANGES && !q) || (point == F_TEXT && q)) continue; + for (int index = 1; index <= (q && point == F_BIND ? 2 : 1); index++) { + assert(sound ? !chat_core_set_sound_on_message(inst, channel, 1) : !chat_core_save_ui_state(inst, "preferences-test", "old")); + unsigned count = broadcasts; target = queries[q]; fault = point; argument = point == F_TEXT ? 0 : index; + int rc = sound ? chat_core_set_sound_on_message(inst, channel, 0) : chat_core_save_ui_state(inst, "preferences-test", "new"); + assert(rc < 0 && fault == F_NONE && sqlite3_get_autocommit(inst->topo_sqlite_db) && broadcasts == count); + if (sound) assert(chat_core_get_sound_on_message(inst, channel) == 1); + else value_is(reader, "old"); + assert(sound ? !chat_core_set_sound_on_message(inst, channel, 0) : !chat_core_save_ui_state(inst, "preferences-test", "new")); + assert(broadcasts == count + (unsigned)sound); cases++; + } + } + const char* commands[] = {"BEGIN IMMEDIATE /* ui_state */", "COMMIT /* ui_state */"}; + for (size_t i = 0; i < 2; i++) { + assert(sound ? !chat_core_set_sound_on_message(inst, channel, 1) : !chat_core_save_ui_state(inst, "preferences-test", "old")); + unsigned count = broadcasts; target = commands[i]; fault = F_EXEC; + int rc = sound ? chat_core_set_sound_on_message(inst, channel, 0) : chat_core_save_ui_state(inst, "preferences-test", "new"); + assert(rc < 0 && fault == F_NONE && broadcasts == count && sqlite3_get_autocommit(inst->topo_sqlite_db)); + if (sound) assert(chat_core_get_sound_on_message(inst, channel) == 1); + else value_is(reader, "old"); + assert(sound ? !chat_core_set_sound_on_message(inst, channel, 0) : !chat_core_save_ui_state(inst, "preferences-test", "new")); cases++; + } + printf("PASS preferences writer sound=%d:%u SQL faults with unchanged reader/no broadcast and successful retries\n", sound, cases); +} + +static int veto(void* arg) { (*(unsigned*)arg)++; return 1; } + +static void semantics(struct UTUN_INSTANCE* inst) { + assert(!chat_core_save_ui_state(inst, "preferences-test", "old") && !chat_core_set_sound_on_message(inst, channel, 1)); + unsigned commits = 0, count = broadcasts; sqlite3_commit_hook(inst->topo_sqlite_db, veto, &commits); + assert(!chat_core_save_ui_state(inst, "preferences-test", "old") && !chat_core_set_sound_on_message(inst, channel, 1)); + assert(!commits && broadcasts == count + 1); + assert(chat_core_set_sound_on_message(inst, channel, 0) < 0 && commits == 1 && broadcasts == count + 1); + sqlite3_commit_hook(inst->topo_sqlite_db, NULL, NULL); assert(chat_core_get_sound_on_message(inst, channel) == 1); + assert(!chat_core_set_sound_on_message(inst, channel, 0)); + const char* modes[] = {"ABORT", "IGNORE"}; + for (size_t i = 0; i < 2; i++) { + char sql[256]; snprintf(sql, sizeof(sql), "CREATE TRIGGER reject_pref BEFORE UPDATE ON ui_state BEGIN SELECT RAISE(%s%s);END", + modes[i], i ? "" : ",'injected preference error'"); + assert(sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK); count = broadcasts; + assert(chat_core_set_sound_on_message(inst, channel, 1) < 0 && broadcasts == count && chat_core_get_sound_on_message(inst, channel) == 0); + assert(sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_pref", NULL, NULL, NULL) == SQLITE_OK); + } + assert(!chat_core_set_sound_on_message(inst, channel, 1)); + assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN;INSERT INTO ui_state VALUES('pref-foreign','1')", NULL, NULL, NULL) == SQLITE_OK); + count = broadcasts; assert(chat_core_set_sound_on_message(inst, channel, 1) < 0 && chat_core_save_ui_state(inst, "preferences-test", "old") < 0); + assert(!sqlite3_get_autocommit(inst->topo_sqlite_db) && broadcasts == count); + assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); + unsigned applied = autoconnect_calls; struct chat_setting_state before = inst->chat_settings; + assert(chat_core_set_setting(inst, "group_autoconnect", "bogus") == -2 && autoconnect_calls == applied); + assert(!memcmp(&before, &inst->chat_settings, sizeof(before)) && chat_core_set_setting(inst, "unknown-pref", "1") == -1); + assert(!chat_core_set_setting(inst, "group_autoconnect", "0") && autoconnect_calls == applied + 1); + assert(chat_core_set_setting(NULL, "group_autoconnect", "0") == -2 && autoconnect_calls == applied + 1); + chat_core_save_ui_state_trampoline(NULL); chat_core_set_setting_trampoline(NULL); chat_core_set_sound_on_message_trampoline(NULL); + struct save_ui_state_arg* a = u_malloc(sizeof(*a)); assert(a); a->inst = inst; memset(a->data, 'x', sizeof(a->data)); + chat_core_save_ui_state_trampoline(a); + a = u_malloc(sizeof(*a)); assert(a); a->inst = inst; memset(a->data, 'x', sizeof(a->data)); a->data[1] = 0; + chat_core_save_ui_state_trampoline(a); + struct chat_setting_arg* b = u_malloc(sizeof(*b)); assert(b); b->inst = inst; memset(b->data, 'x', sizeof(b->data)); + chat_core_set_setting_trampoline(b); + struct chat_sound_arg* c = u_malloc(sizeof(*c)); assert(c); c->inst = inst; memset(c->ch_id, 'x', sizeof(c->ch_id)); + c->on = 0; count = broadcasts; chat_core_set_sound_on_message_trampoline(c); assert(broadcasts == count); + puts("PASS preferences:no-op retries delivery without commit,veto/ABORT/IGNORE/foreign txn,validation and malformed requests"); +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0); struct UASYNC* ua = uasync_create(); assert(ua); @@ -32,11 +157,18 @@ int main(void) { "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", directory); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst && !utun_core_start(inst) && !chat_service_start(inst)); - snprintf(database, sizeof(database), "%s/chats.db", directory); sqlite3* reader = NULL; + snprintf(database, sizeof(database), "%s/chats.db", directory); assert(sqlite3_open_v2(database, &reader, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK); - chat_core_save_ui_state(inst, "preferences-test", "old"); value_is(reader, "old"); - fail_finalize = 1; chat_core_save_ui_state(inst, "preferences-test", "new"); assert(!fail_finalize); + assert(!chat_core_save_ui_state(inst, "preferences-test", "old")); value_is(reader, "old"); + fail_finalize = 1; assert(chat_core_save_ui_state(inst, "preferences-test", "new") < 0 && !fail_finalize); value_is(reader, "old"); + uint64_t group = chat_core_create_channel_auto(inst, "preferences"); assert(group); + snprintf(channel, sizeof(channel), "%llu", (unsigned long long)group); observe_sound = 1; + assert(chat_core_get_sound_on_message(inst, channel) == 1); write_faults(inst, 0); write_faults(inst, 1); semantics(inst); + assert(!chat_core_save_ui_state(inst, "preferences-test", "old")); target = "COMMIT /* ui_state */"; + fault = F_EXEC; fail_rollback = 1; inst->running = 1; + assert(chat_core_save_ui_state(inst, "preferences-test", "uncommitted") < 0 && !fail_rollback && !inst->running); + assert(!sqlite3_get_autocommit(inst->topo_sqlite_db)); value_is(reader, "old"); utun_instance_destroy(inst); assert(sqlite3_close(reader) == SQLITE_OK); uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline); puts("PASS preferences:failed finalize preserves committed UI state"); return 0;