Files
utun2/tests/test_member_adapter.c
T
evgeny 6429d4ca23 Merge desktop audit with updated chat contracts
Keep the complete member snapshot limit at 65535 and preserve SQL finalization checks, unsigned sorting, and owned GUI commands. Retain dynamic ETCP diagnostics with checked event lengths and no partial event on allocation failure. Validated three native and six GUI test targets and rebuilt vibechat.
2026-10-27 11:12:51 +01:00

363 lines
24 KiB
C

/* Реальный адаптер, SQLite и транзакции; без копии алгоритма и без сетевых таймингов. */
#include "../src/chat/member_sync.c"
#include "../src/chat/chat_member.h"
#include "../src/member_files/protocol.h"
#include <assert.h>
void test_member_purge(struct UASYNC* ua);
static unsigned apply_calls, cancelled_calls;
static int nested_apply;
static void cancelled_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; cancelled_calls++;
}
static void removing_apply(const char* ns, uint64_t node, const uint8_t* x, uint64_t parent, const uint8_t* sig, void* arg) {
struct UTUN_INSTANCE* inst = arg; apply_calls++; assert(!strcmp(ns, "1001"));
if (!nested_apply) { nested_apply = 1; _fire_apply_cbk(inst, ns, node, x, parent, sig); nested_apply = 0; }
member_sync_remove_apply_cbk(inst, removing_apply, inst); member_sync_remove_apply_cbk(inst, cancelled_apply, inst);
}
static void free_namespace(uint64_t node, const char* tags, const char* ns, void* arg) {
(void)node; (void)tags; assert(!strcmp(ns, "1001")); char** owned = arg; u_free(*owned); *owned = NULL;
}
static void check_namespace(uint64_t node, const char* tags, const char* ns, void* arg) {
(void)node; (void)tags; (void)arg; assert(!strcmp(ns, "1001"));
}
static void callback_lifetime(void) {
size_t baseline = u_get_allocated_count(); struct UTUN_INSTANCE inst = {0};
struct chat_core_ctx cc = {.inst = &inst}; inst.chat_core = &cc;
uint8_t key[32] = {1}, signature[64] = {1};
assert(!member_sync_add_apply_cbk(&inst, cancelled_apply, &inst));
assert(!member_sync_add_apply_cbk(&inst, removing_apply, &inst));
_fire_apply_cbk(&inst, "1001", 7, key, 0, signature);
assert(apply_calls == 2 && !cancelled_calls && !cc.apply_cbks && !cc.apply_dispatch_depth);
char* owned = u_strdup("1001"); assert(owned);
assert(!member_sync_add_props_cbk(&inst, check_namespace, &owned));
assert(!member_sync_add_props_cbk(&inst, free_namespace, &owned));
_fire_props_changed(&inst, 7, NULL, owned); assert(!owned);
member_sync_remove_props_cbk(&inst, check_namespace, &owned); member_sync_remove_props_cbk(&inst, free_namespace, &owned);
assert(!cc.props_cbks && !cc.props_dispatch_depth && u_get_allocated_count() == baseline);
puts("PASS: nested apply dispatch,self/other cancellation,and namespace owner freed during properties callback");
}
static void setup(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
memset(inst, 0, sizeof(*inst)); inst->ua = ua; inst->node_id = 1;
assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK);
assert(topo_node_sqlite_init(inst->topo_sqlite_db) == 0);
uint8_t key[32] = {1}, signature[64] = {0};
assert(topo_node_sqlite_channel_put(inst->topo_sqlite_db, "1001", "test", 1, key, NULL, key, NULL, signature) == 0);
assert(member_sync_init(inst) == 0);
}
static void put(struct UTUN_INSTANCE* inst, uint64_t id) {
uint8_t key[32] = {1};
assert(member_sync_put(inst, "1001", id, key, key, NULL, 0, NULL, 0, "{}", NULL, NULL, 0, 0, NULL) >= 0);
}
static int snapshot_list_events, snapshot_updates, snapshot_bad;
static uint64_t snapshot_first, snapshot_last;
/* Событие проверяем синхронно, не сохраняем заимствованный буфер и не печатаем тысячи записей. */
static void snapshot_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
(void)inst;
if (type == CHAT_EVT_MEMBER_UPDATED) { snapshot_updates++; return; }
if (type != CHAT_EVT_MEMBER_LIST) return;
snapshot_list_events++;
if (!data || len < 7 || data[0] != 4 || memcmp(data + 1, "1001", 4)) { snapshot_bad++; return; }
uint16_t count;
memcpy(&count, data + 5, sizeof(count));
if ((size_t)len != 7 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE) { snapshot_bad++; return; }
snapshot_first = count ? mf_get64(data + 7) : 0;
snapshot_last = count ? mf_get64(data + 7 + (size_t)(count - 1) * CHAT_MEMBER_DISPLAY_SIZE) : 0;
}
/* Настоящие SQLite-запрос/сериализатор/trampoline: count должен соответствовать числу wire-записей. */
static int test_snapshot_event_bounds(struct UASYNC* ua) {
struct UTUN_INSTANCE inst; setup(&inst, ua);
struct chat_core_ctx cc = { .inst = &inst, .db = inst.topo_sqlite_db, .initialized = 1 };
inst.chat_core = &cc;
chat_event_set_handler(&inst, snapshot_event);
const int sizes[] = {0, 1, UINT16_MAX, UINT16_MAX + 1};
int result = 0;
for (unsigned test = 0; test < sizeof(sizes) / sizeof(sizes[0]); test++) {
const int count = sizes[test]; inst.node_id = count; cc.my_node_id = count;
assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001", NULL, NULL, NULL) == SQLITE_OK);
if (count) {
char sql[512];
snprintf(sql, sizeof(sql), "WITH RECURSIVE n(id) AS (VALUES(1) UNION ALL SELECT id+1 FROM n WHERE id<%d) "
"INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) "
"SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", count);
assert(sqlite3_exec(cc.db, sql, NULL, NULL, NULL) == SQLITE_OK);
}
snapshot_list_events = snapshot_updates = snapshot_bad = 0; snapshot_first = snapshot_last = 0;
const size_t before = u_get_allocated_count();
struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request);
request->inst = &inst; strcpy(request->ch_id, "1001");
chat_core_request_member_list_trampoline(request);
if (snapshot_bad || snapshot_list_events != (count <= UINT16_MAX) ||
(count <= UINT16_MAX && count && (snapshot_first != (uint64_t)count || snapshot_last != (uint64_t)(count > 1 ? count - 1 : 1)))) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member event boundary failed count=%d events=%d malformed=%d first=%llu last=%llu",
count, snapshot_list_events, snapshot_bad, (unsigned long long)snapshot_first, (unsigned long long)snapshot_last);
result = -1;
}
assert(u_get_allocated_count() == before);
if (count <= 1) {
request = u_calloc(1, sizeof(*request)); assert(request);
request->inst = &inst; strcpy(request->ch_id, "1001");
chat_core_request_member_rtt_trampoline(request);
assert(snapshot_updates == count && u_get_allocated_count() == before);
}
}
if (!result) {
uint8_t* wire = NULL; int count = -1;
assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == -1 && !wire && !count);
u_free(wire); wire = NULL;
assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001;"
"WITH n(id) AS (VALUES(1),(2),(-1),(-9223372036854775808)) "
"INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) "
"SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", NULL, NULL, NULL) == SQLITE_OK);
inst.node_id = cc.my_node_id = 2;
assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == 0 && count == 4);
const uint64_t order[] = {2, 1, UINT64_C(1) << 63, UINT64_MAX};
for (int i = 0; i < 4; i++) {
assert(mf_get64(wire + i * CHAT_MEMBER_DISPLAY_SIZE) == order[i]);
assert(wire[i * CHAT_MEMBER_DISPLAY_SIZE + 8] == (i == 0));
}
u_free(wire); wire = NULL;
assert(chat_core_get_member_list(NULL, "1001", &wire, &count) < 0 && !wire && count == 0);
struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request);
chat_core_request_member_list_trampoline(request);
request = u_calloc(1, sizeof(*request)); assert(request);
chat_core_request_member_rtt_trampoline(request);
}
inst.chat_core = NULL;
member_sync_destroy(&inst);
assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK);
return result;
}
static void root(struct UTUN_INSTANCE* inst, uint8_t hash[32]) {
assert(merkle_sync_read_hash(inst, MT_DATA_MEMBERS, "1001", 0, 0, hash) == 0);
}
static void public_key(const uint8_t priv[32], uint8_t pub[32]) {
EVP_PKEY* pk = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, priv, 32); assert(pk);
size_t len = 32; assert(EVP_PKEY_get_raw_public_key(pk, pub, &len) == 1); EVP_PKEY_free(pk);
}
/* Подтверждение корня доставляется независимо от версии профиля и переживает нулевой блок на проводе. */
static void test_invitation_certificate(struct UASYNC* ua) {
struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua);
uint8_t x[32] = {9}, priv[32] = {7}, pub[32], owner_priv[32] = {8}, owner_pub[32], zero[64] = {0};
public_key(priv, pub); public_key(owner_priv, owner_pub);
struct UTUN_INSTANCE* nodes[] = { &source, &target };
for (unsigned i = 0; i < 2; i++) {
assert(topo_node_sqlite_channel_delete(nodes[i]->topo_sqlite_db, "1001") == 0);
assert(topo_node_sqlite_channel_put(nodes[i]->topo_sqlite_db, "1001", "root", 1, x, NULL, owner_pub, NULL, zero) == 0);
}
uint64_t nid = sc_derive_node_id_from_pubkey(x); uint8_t join[64], tree[64], update[64], body[256];
int len = member_sync_build_join_msg(x, owner_pub, nid, x, 1, body, sizeof(body)); assert(len == 112);
assert(sc_ed25519_sign(priv, body, len, join) == SC_OK && member_sync_sign_pubkey(owner_priv, x, tree) == 0);
struct ms_member_rec rec = { .node_id = nid, .x25519 = x, .ed25519 = pub, .join_sig = join, .join_ts = 1,
.update_sig = update, .update_ts = 2, .userinfo = "{\"name\":\"profile\"}", .signature = tree };
len = member_sync_build_update_msg(join, rec.update_ts, rec.userinfo, body, sizeof(body)); assert(len > 0);
assert(sc_ed25519_sign(priv, body, len, update) == SC_OK);
assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED);
rec.signature = zero;
assert(member_sync_apply_record(&target, "1001", target.node_id, &rec) & MS_APPLY_CHANGED);
assert(member_sync_verify_signature(target.topo_sqlite_db, "1001", &rec) == 0);
rec.signature = tree;
assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) & MS_APPLY_CHANGED);
uint8_t expected[32], actual[32], stored[64]; uint64_t parent;
root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32));
assert(topo_node_sqlite_member_get_sign(target.topo_sqlite_db, "1001", nid, &parent, stored) == 0);
assert(parent == 0 && !memcmp(tree, stored, 64));
assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) == 0);
tree[0] ^= 1;
assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) < 0);
root(&target, actual); assert(!memcmp(expected, actual, 32)); tree[0] ^= 1;
/* Более свежий подписанный профиль с нулевой подписью приглашения сохраняет сертификат. */
rec.update_ts = 3; rec.userinfo = "{\"name\":\"new profile\"}";
len = member_sync_build_update_msg(join, rec.update_ts, rec.userinfo, body, sizeof(body)); assert(len > 0);
assert(sc_ed25519_sign(priv, body, len, update) == SC_OK);
assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED);
rec.signature = zero;
assert(member_sync_apply_record(&target, "1001", source.node_id + 1, &rec) & MS_APPLY_CHANGED);
assert(topo_node_sqlite_member_get_sign(target.topo_sqlite_db, "1001", nid, &parent, stored) == 0);
assert(parent == 0 && !memcmp(tree, stored, 64));
root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32));
/* Старый профиль может донести подтверждение, сохранив более свежие userinfo/update_ts. */
assert(sqlite3_exec(target.topo_sqlite_db, "UPDATE peers_1001 SET signature=zeroblob(64)", NULL, NULL, NULL) == SQLITE_OK);
assert(merkle_sync_recompute_path(&target, MT_DATA_MEMBERS, "1001", nid) == 1);
rec.update_ts = 2; rec.userinfo = "{\"name\":\"old profile\"}"; rec.signature = tree;
len = member_sync_build_update_msg(join, rec.update_ts, rec.userinfo, body, sizeof(body)); assert(len > 0);
assert(sc_ed25519_sign(priv, body, len, update) == SC_OK);
int result = member_sync_apply_record(&target, "1001", source.node_id + 1, &rec);
assert((result & MS_APPLY_CHANGED) && (result & MS_APPLY_STALE));
root(&target, actual); assert(!memcmp(expected, actual, 32));
puts("PASS: root certificate validates with channel key; equal/stale profile versions do not block confirmation; empty wire signature preserves it");
for (unsigned i = 0; i < 2; i++) { member_sync_destroy(nodes[i]); assert(sqlite3_close(nodes[i]->topo_sqlite_db) == SQLITE_OK); }
}
static void test_owner_sizes(struct UASYNC* ua) {
struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua);
uint8_t x[32] = {9}, priv[32] = {7}, pub[32], owner_priv[32] = {8}, owner_pub[32], zero[64] = {0};
public_key(priv, pub); public_key(owner_priv, owner_pub);
/* setup создаёт пустой канал с тестовым ключом; новую идентичность создаём как новый канал. */
assert(topo_node_sqlite_channel_delete(source.topo_sqlite_db, "1001") == 0);
assert(topo_node_sqlite_channel_delete(target.topo_sqlite_db, "1001") == 0);
assert(topo_node_sqlite_channel_put(source.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, owner_priv, zero) == 0);
assert(topo_node_sqlite_channel_put(target.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, NULL, zero) == 0);
uint64_t nid = sc_derive_node_id_from_pubkey(x);
uint8_t join[64], tree[64], owner[64], update[64], msg[MS_ADM_TAGS_MAX + 8];
int n = member_sync_build_join_msg(x, owner_pub, nid, x, 1, msg, sizeof(msg)); assert(n > 0);
assert(sc_ed25519_sign(priv, msg, n, join) == SC_OK);
assert(member_sync_sign_pubkey(owner_priv, x, tree) == 0);
char tags[MS_ADM_TAGS_MAX + 2];
char userinfo[MS_USERINFO_MAX + 1];
struct ms_member_rec rec = { .node_id = nid, .x25519 = x, .ed25519 = pub,
.join_sig = join, .join_ts = 1, .userinfo = userinfo, .update_sig = update,
.adm_tags = tags, .adm_tags_sig = owner, .signature = tree };
const size_t sizes[] = {191, 192, 255, 256, 4096, MS_ADM_TAGS_MAX};
uint8_t page[MT_PAGE_SIZE], expected[32], actual[32];
for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) {
const size_t profile_sizes[] = {191, 255, 256, 512, 1023, MS_USERINFO_MAX};
int head = snprintf(userinfo, sizeof(userinfo), "{\"name\":\"avatar\",\"pad\":\"");
size_t end = profile_sizes[i] - 2;
memset(userinfo + head, 'p', end - head); memcpy(userinfo + end, "\"}", 3);
rec.update_ts = i + 1;
n = member_sync_build_update_msg(join, rec.update_ts, userinfo, msg, sizeof(msg)); assert(n > 0);
assert(sc_ed25519_sign(priv, msg, n, update) == SC_OK);
size_t len = sizes[i];
int prefix = snprintf(tags, sizeof(tags), "{\"ver\":\"%zu\",\"pad\":\"", i + 1);
const char* suffix = "\",\"storage\":\"yes\"}";
size_t tail = len - strlen(suffix);
memset(tags + prefix, 'a', tail - (size_t)prefix); memcpy(tags + tail, suffix, strlen(suffix) + 1);
n = member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)); assert(n == (int)len + 8);
assert(sc_ed25519_sign(owner_priv, msg, n, owner) == SC_OK);
assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED);
assert(member_sync_verify_local_record(source.topo_sqlite_db, "1001", &rec) == 1);
size_t bytes = sizeof(page); merkle_key_t next, after = {{0}}; int more;
assert(_member_get_page(&source, "1001", merkle_sync_level_prefix(nid, 5), 0, &after, page, &bytes, &next, &more) == 0);
assert(member_read64(next.bytes) == nid && !more && bytes < MT_PAGE_SIZE);
struct member_wire w; size_t used;
assert(member_decode(page + 2, bytes - 2, &w, &used) == 0 && used == bytes - 2 && !strcmp(w.rec.adm_tags, tags));
assert(!strcmp(w.rec.userinfo, userinfo));
assert(_member_apply_items(&target, "1001", 2, page, bytes) == 0);
root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32));
sqlite3_stmt* st;
assert(sqlite3_prepare_v2(target.topo_sqlite_db, "SELECT adm_tags,storage FROM peers_1001", -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == (int)len);
assert(!strcmp((const char*)sqlite3_column_text(st, 0), tags));
char storage[8];
assert(json_flat_get((const char*)sqlite3_column_text(st, 0), "storage", storage, sizeof(storage)) == 0);
assert(!strcmp(storage, "yes"));
sqlite3_finalize(st);
size_t flip = tail - 1; tags[flip] = 'b';
assert(member_sync_apply_record(&target, "1001", 2, &rec) < 0);
assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) < 0);
assert(member_sync_verify_local_record(target.topo_sqlite_db, "1001", &rec) == 0);
root(&target, actual); assert(!memcmp(expected, actual, 32));
assert(sqlite3_exec(target.topo_sqlite_db, "SAVEPOINT hash_tail", NULL, NULL, NULL) == SQLITE_OK);
assert(topo_node_sqlite_member_owner_put(target.topo_sqlite_db, "1001", nid, tags, owner, 0) == 0);
assert(merkle_sync_recompute_path(&target, MT_DATA_MEMBERS, "1001", nid) == 1);
root(&target, actual); assert(memcmp(expected, actual, 32));
assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK TO hash_tail; RELEASE hash_tail", NULL, NULL, NULL) == SQLITE_OK);
tags[flip] = 'a';
assert(_member_apply_items(&target, "1001", 2, page, bytes - 1) < 0);
size_t length_offset = (size_t)(w.rec.adm_tags_sig - page) - len - 2;
page[length_offset] = 0xff; page[length_offset + 1] = 0xff;
assert(_member_apply_items(&target, "1001", 2, page, bytes) < 0);
}
puts("PASS: full owner signatures and wire roundtrip at 191/192/255/256/4096/8192 bytes; suffix tampering rejected");
struct chat_core_ctx cc = { .inst = &source, .db = source.topo_sqlite_db, .initialized = 1 };
source.chat_core = &cc;
char listing[MT_PAGE_SIZE]; size_t listed;
assert(chat_core_get_members_json(&source, "1001", listing, sizeof(listing), &listed) == 0 && listed > MS_ADM_TAGS_MAX);
struct chat_member_tags* edit = chat_member_tags_load(&source, "1001", nid); assert(edit);
const char* pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096);
assert(chat_member_tags_set(edit, "storage", "no") == 0);
assert(chat_member_tags_commit(&source, edit) == 0);
chat_member_tags_free(edit);
edit = chat_member_tags_load(&source, "1001", nid); assert(edit);
pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096);
assert(!strcmp(chat_member_tags_get(edit, "storage"), "no"));
root(&source, expected);
assert(sqlite3_exec(source.topo_sqlite_db, "CREATE TRIGGER reject_owner BEFORE UPDATE OF adm_tags ON peers_1001 "
"BEGIN SELECT RAISE(ABORT,'injected owner failure'); END", NULL, NULL, NULL) == SQLITE_OK);
assert(chat_member_tags_set(edit, "storage", "yes") == 0);
assert(chat_member_tags_commit(&source, edit) < 0);
root(&source, actual); assert(!memcmp(expected, actual, 32));
assert(sqlite3_exec(source.topo_sqlite_db, "DROP TRIGGER reject_owner", NULL, NULL, NULL) == SQLITE_OK);
memset(tags, 'x', MS_ADM_TAGS_MAX + 1); tags[MS_ADM_TAGS_MAX + 1] = 0;
assert(member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)) < 0);
assert(chat_member_tags_set(edit, "pad", tags) < 0);
tags[MS_ADM_TAGS_MAX] = 0;
assert(chat_member_tags_set(edit, "pad", tags) == 0 && chat_member_tags_commit(&source, edit) < 0);
root(&source, actual); assert(!memcmp(expected, actual, 32));
assert(chat_member_tags_set(edit, "pad", "quoted\"\\value\n\r\t\b\f") == 0 && chat_member_tags_commit(&source, edit) == 0);
chat_member_tags_free(edit);
edit = chat_member_tags_load(&source, "1001", nid); assert(edit);
assert(!strcmp(chat_member_tags_get(edit, "pad"), "quoted\"\\value\n\r\t\b\f")); chat_member_tags_free(edit);
char small[8]; size_t output;
assert(chat_core_get_members_json(&source, "1001", small, sizeof(small), &output) < 0 && output == 0);
source.chat_core = NULL;
puts("PASS: long JSON edit/reload; escaping; size and SQLite errors preserve data and tree");
member_sync_destroy(&source); member_sync_destroy(&target);
assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
callback_lifetime();
size_t baseline = u_get_allocated_count();
struct UASYNC* ua = uasync_create(); assert(ua);
if (test_snapshot_event_bounds(ua) < 0) { uasync_destroy(ua, 0); return 1; }
test_member_purge(ua);
test_invitation_certificate(ua);
test_owner_sizes(ua);
struct UTUN_INSTANCE source, target;
setup(&source, ua); setup(&target, ua);
put(&source, 1); put(&source, 2);
uint8_t raw[2049], *page = raw + 1; /* проверяем невыровненный буфер */
size_t len = 300; merkle_key_t next, after = {{0}}; int more;
assert(_member_get_page(&source, "1001", 0, 0, &after, page, &len, &next, &more) == 0);
assert(page[0] == 0 && page[1] == 1 && member_read64(next.bytes) == 1 && more == 1 && len <= 300);
struct member_wire w; size_t used;
assert(member_decode(page + 2, len - 2, &w, &used) == 0 && w.rec.node_id == 1 && used == len - 2);
len = 300;
assert(_member_get_page(&source, "1001", 0, 1, &next, page, &len, &next, &more) == 0 && member_read64(next.bytes) == 2 && !more);
puts("PASS: bounded member pages, canonical integers and unaligned buffers");
len = 2048;
assert(_member_get_page(&source, "1001", 0, 0, &after, page, &len, &next, &more) == 0 && page[1] == 2 && !more);
assert(_member_apply_items(&target, "1001", 1, page, len - 1) < 0 && member_sync_count(&target, "1001") == 0);
sqlite3* db = target.topo_sqlite_db;
assert(sqlite3_exec(db, "CREATE TRIGGER fail_second BEFORE INSERT ON peers_1001 WHEN NEW.node_id=2 "
"BEGIN SELECT RAISE(ABORT,'injected second record failure'); END", NULL, NULL, NULL) == SQLITE_OK);
assert(_member_apply_items(&target, "1001", 1, page, len) < 0);
uint8_t hash[32], empty[32] = {0}; root(&target, hash);
assert(member_sync_count(&target, "1001") == 0 && !memcmp(hash, empty, 32) && sqlite3_get_autocommit(db));
assert(sqlite3_exec(db, "DROP TRIGGER fail_second", NULL, NULL, NULL) == SQLITE_OK);
assert(_member_apply_items(&target, "1001", 1, page, len) == 0 && member_sync_count(&target, "1001") == 2);
uint8_t expected[32]; root(&source, expected); root(&target, hash); assert(!memcmp(hash, expected, 32));
puts("PASS: malformed page rejected; second-record error rolls back data and tree; complete page converges");
assert(sqlite3_exec(db, "UPDATE peers_1001 SET x25519_pubkey=X'01' WHERE node_id=1", NULL, NULL, NULL) == SQLITE_OK);
assert(merkle_sync_recompute_path(&target, MT_DATA_MEMBERS, "1001", 1) < 0);
root(&target, hash); assert(!memcmp(hash, expected, 32));
len = 2048;
assert(_member_get_page(&target, "1001", 0, 0, &after, page, &len, &next, &more) < 0);
puts("PASS: corrupt BLOB cannot be hashed or serialized");
member_sync_destroy(&target); member_sync_destroy(&source);
assert(sqlite3_close(target.topo_sqlite_db) == SQLITE_OK && sqlite3_close(source.topo_sqlite_db) == SQLITE_OK);
uasync_poll(ua, 0); uasync_destroy(ua, 0);
assert(u_get_allocated_count() == baseline);
puts("ALL PASS: member adapter");
return 0;
}