From beac8afba885ceb001140773b11816215ecde52b Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 06:46:15 +0100 Subject: [PATCH] test(chat): cover profile trust, owner rollback, avatar policy, and startup failure --- tests/test_chat_profile.c | 105 +++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/tests/test_chat_profile.c b/tests/test_chat_profile.c index da7c2661..89ba65a0 100644 --- a/tests/test_chat_profile.c +++ b/tests/test_chat_profile.c @@ -6,6 +6,7 @@ #include "../src/chat/chat_event.h" #include "../src/chat/chat_core_priv.h" #include "../src/routing_layer/topo_node_sqlite.h" +#include "../src/routing_layer/topo_group.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/json_flat.h" @@ -237,6 +238,107 @@ static void callback_deletion(struct UTUN_INSTANCE* inst) { puts("PASS profile:channel deleted from committed property callback without using old group/namespace"); } +static int owner_update(struct UTUN_INSTANCE* inst) { + char tags[96]; snprintf(tags, sizeof(tags), "{\"ver\":\"%u\",\"storage\":\"yes\"}", ++sequence); + uint8_t key[32], body[MS_ADM_TAGS_MAX + 8], signature[64]; + assert(!topo_node_sqlite_channel_get_priv(inst->topo_sqlite_db, channel, key)); + int bytes = member_sync_build_owner_msg(inst->node_id, tags, body, sizeof(body)); assert(bytes > 0); + assert(sc_ed25519_sign(key, body, (size_t)bytes, signature) == SC_OK); OPENSSL_cleanse(key, sizeof(key)); + return member_sync_put(inst, channel, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey, + NULL, 0, NULL, 0, NULL, tags, signature, 0, 0, NULL); +} + +static void owner_faults(struct UTUN_INSTANCE* inst) { + char query[128]; snprintf(query, sizeof(query), "UPDATE \"peers_%s\" SET adm_tags=", channel); target = query; unsigned cases = 0; + for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) { + for (int index = 1; index <= (point == F_BIND ? 5 : 1); index++) { + uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events; + fault = point; index_number = index; assert(owner_update(inst) < 0 && fault == F_NONE && sqlite3_get_autocommit(inst->topo_sqlite_db)); + snapshot(reader, after); assert(!memcmp(before, after, 32) && events == count); + assert((owner_update(inst) & MS_APPLY_CHANGED) && events == count + 1); cases++; + } + } + uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events; + fault = F_FINALIZE; fail_member_rollback = 1; assert(owner_update(inst) < 0 && fault == F_NONE && !fail_member_rollback); + assert(member_rollback_failed && !releases_after_failure && sqlite3_get_autocommit(inst->topo_sqlite_db)); + member_rollback_failed = 0; snapshot(reader, after); assert(!memcmp(before, after, 32) && events == count); + assert(owner_update(inst) & MS_APPLY_CHANGED); + for (int bind = 1; bind <= 2; bind++) { + assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN", NULL, NULL, NULL) == SQLITE_OK); + fault = F_BIND; index_number = bind; + assert(topo_node_sqlite_member_owner_put(inst->topo_sqlite_db, channel, inst->node_id, NULL, NULL, 0) < 0 && fault == F_NONE); + assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); + } + printf("PASS owner block:%u SQL faults/retries,NULL binds,and failed standalone ROLLBACK TO never commits partial owner data\n", cases); +} + +static void avatar(struct UTUN_INSTANCE* inst) { + char file[70]; memset(file, 'a', 64); memcpy(file + 64, ".json", 6); + const char* query = "SELECT adm_tags,source,adm_tags_sig,deleted"; + for (enum fault point = F_PREPARE; point <= F_FINALIZE; point++) { + if (point == F_CHANGES) continue; + uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events; + target = query; fault = point; index_number = 1; + assert(chat_core_set_my_avatar(inst, channel, inst->node_id, file) < 0 && fault == F_NONE); + snapshot(reader, after); assert(!memcmp(before, after, 32) && events == count); + } + assert(!chat_core_set_my_avatar(inst, channel, inst->node_id, file)); unsigned count = events; + assert(!chat_core_set_my_avatar(inst, channel, inst->node_id, file) && events == count); + char json[MS_USERINFO_MAX + 1], value[70]; uint8_t signature[64]; uint64_t version; + assert(!chat_core_get_my_profile(inst, channel, json, sizeof(json), signature, &version)); + assert(!json_flat_get(json, "avatar_file", value, sizeof(value)) && !strcmp(value, file)); + assert(!chat_core_set_my_avatar(inst, channel, 0, "")); count = events; + assert(!chat_core_set_my_avatar(inst, channel, 0, "") && events == count); + puts("PASS avatar profile:4 ACL SQL failures,publish/identical retry/delete keep signed options and committed events"); +} + +static void no_apply(const char* ns, uint64_t node, const uint8_t* x, uint64_t parent, const uint8_t* sig, void* arg) { + (void)ns; (void)node; (void)x; (void)parent; (void)sig; (void)arg; +} + +static void registration_failure(struct UTUN_INSTANCE* inst, struct UASYNC* ua) { + size_t count = u_get_allocated_count(); fail_member_malloc = 1; + assert(member_sync_add_apply_cbk(inst, no_apply, inst) < 0 && !fail_member_malloc && u_get_allocated_count() == count); + struct UTUN_INSTANCE* other = utun_instance_create_from_str(ua, + "[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"); + assert(other && !utun_core_start(other)); fail_apply_registration = 1; + assert(chat_service_start(other) < 0 && !fail_apply_registration && !fail_member_malloc); + assert(other->core_started && !other->chat_started && !other->chat_sync && !other->chat_core); + assert(!chat_service_start(other)); utun_instance_destroy(other); + puts("PASS profile/JOIN callback allocation:refused registration rolls service startup back;core survives,retry succeeds"); +} + +static void set_profile_row(struct UTUN_INSTANCE* inst, const char* json, const uint8_t signature[64], uint64_t version, const uint8_t join[64]) { + char sql[192]; snprintf(sql, sizeof(sql), "UPDATE peers_%s SET userinfo=?,update_sig=?,update_ts=?,join_sig=? WHERE node_id=?", channel); + sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC) == SQLITE_OK); + assert((signature ? sqlite3_bind_blob(st, 2, signature, 64, SQLITE_STATIC) : sqlite3_bind_null(st, 2)) == SQLITE_OK); + assert(sqlite3_bind_int64(st, 3, (sqlite3_int64)version) == SQLITE_OK && sqlite3_bind_blob(st, 4, join, 64, SQLITE_STATIC) == SQLITE_OK); + assert(sqlite3_bind_int64(st, 5, (sqlite3_int64)inst->node_id) == SQLITE_OK && sqlite3_step(st) == SQLITE_DONE); + assert(sqlite3_finalize(st) == SQLITE_OK); +} + +static void damaged_profile(struct UTUN_INSTANCE* inst) { + char json[MS_USERINFO_MAX + 1]; uint8_t signature[64], changed[64], body[MS_USERINFO_MAX + 73]; uint64_t version; + assert(!chat_core_get_my_profile(inst, channel, json, sizeof(json), signature, &version)); + set_profile_row(inst, "{\"untrusted\":\"do not sign\"}", signature, version, original_join); read_refused(inst); refused(inst); + set_profile_row(inst, json, signature, version, original_join); + memcpy(changed, original_join, 64); changed[0] ^= 1; + int bytes = member_sync_build_update_msg(changed, version, json, body, sizeof(body)); assert(bytes > 0); uint8_t forged[64]; + assert(sc_ed25519_sign(inst->my_ed25519_privkey, body, (size_t)bytes, forged) == SC_OK); + set_profile_row(inst, json, forged, version, changed); read_refused(inst); refused(inst); + set_profile_row(inst, json, signature, version, original_join); + const char* duplicate = "{\"name\":\"profile test\",\"x\":\"one\",\"x\":\"two\"}"; + bytes = member_sync_build_update_msg(original_join, version, duplicate, body, sizeof(body)); assert(bytes > 0); + assert(sc_ed25519_sign(inst->my_ed25519_privkey, body, (size_t)bytes, forged) == SC_OK); + set_profile_row(inst, duplicate, forged, version, original_join); read_refused(inst); refused(inst); + set_profile_row(inst, "{\"untrusted\":\"discard unsigned attributes\"}", NULL, 0, original_join); + assert(!update(inst)); + assert(!chat_core_get_my_profile(inst, channel, json, sizeof(json), signature, &version)); + char value[64]; assert(json_flat_get(json, "untrusted", value, sizeof(value)) < 0); + puts("PASS profile trust:bad old signature,JOIN,and signed duplicate JSON rejected;unsigned foreign attributes never re-signed"); +} + static void unicode(void) { const char* json = "{\"name\":\"\\u0410\\uD83D\\uDE00\",\"protect\":\"yes\",\"control\":\"\\u0001\"}"; char value[64]; assert(!json_flat_get(json, "name", value, sizeof(value)) && !strcmp(value, "А😀")); @@ -277,7 +379,8 @@ int main(void) { if (rc >= 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile test: failed finalize rc=%d pending=%d", rc, fail_read); assert(rc < 0 && !fail_read && timestamp == 123 && !memcmp(profile, previous, sizeof(profile)) && !memcmp(signature, old_signature, 64)); assert(!chat_core_get_my_profile(inst, channel, profile, sizeof(profile), signature, ×tamp) && timestamp > 0); - chat_event_set_handler(inst, event); reader_faults(inst); writer_faults(inst); semantic_profile(inst); callback_deletion(inst); + chat_event_set_handler(inst, event); reader_faults(inst); writer_faults(inst); damaged_profile(inst); semantic_profile(inst); + owner_faults(inst); avatar(inst); registration_failure(inst, ua); callback_deletion(inst); uint8_t before[32], after[32]; snapshot(reader, before); unsigned count = events; target = "COMMIT /* profile */"; fault = F_EXEC; fail_rollback = 1; inst->running = 1; assert(update(inst) < 0 && fault == F_NONE && !fail_rollback && !inst->running && !sqlite3_get_autocommit(inst->topo_sqlite_db));