test(chat): verify preference rollback, committed sound delivery, and request validation
This commit is contained in:
1 parent
fdb0381f5c
commit
09e3ba62ef
2 files changed
+139
-5
No files matched your search
+3
-1
@@ -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 \
|
||||
|
||||
@@ -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 <assert.h>
|
||||
@@ -9,11 +10,50 @@
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user