// test_media_delivery_full.c — полный сценарий: создание, передача, сборка, репликация // // 4 узла: n1 (автор), n2 (получатель), s1 (суперузел), s2 (суперузел) // Топология: цепь 0→1 1→2 2→3 // Фазы: // P1: CHAT-группа (topo_groups_create_group + member_sync_put) // A1: SUPER_HELLO, HAVE_BLOCK, репликация // A2: admission control (OVERLOADED) // B1: n1 создаёт файл → n2 инициирует загрузку → стриминг → сборка → проверка #include "media_delivery.h" #include "media_delivery_proto.h" #include "media_download.h" #include "media_index.h" #include "member_sync.h" #include "../routing_layer/topo_node_sqlite.h" #include "../utun_instance.h" #include "../transport_layer/etcp.h" #include "../config_parser.h" #include "../config_updater.h" #include "../routing_layer/topo_group.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include #include #include #include #include #include static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0; #define TEST(n) do { G_TOTAL++; printf(" %-60s", n); fflush(stdout); } while(0) #define OK() do { G_PASSED++; printf("OK\n"); } while(0) #define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0) #define TIMEOUT_TB 300000 #define POLL_MS 5 #define N_NODES 4 /* n1=0, n2=1, s1=2, s2=3 */ #define CH_ID "31415926535" static struct UASYNC* g_ua = NULL; static struct UTUN_INSTANCE* g_inst[N_NODES]; static uint64_t g_nid[N_NODES]; static uint64_t g_group_id = 0; static uint8_t g_test_mid[16], g_test_bid0[16], g_test_bid1[16]; static char g_tdir[256] = "/tmp/utun_mdf_XXXXXX"; static char g_cfg[N_NODES][256]; static char g_db_dir[N_NODES][320]; static int g_port[N_NODES]; static int g_connected = 0, g_result = 0; static int wf(const char* p, const char* f, ...) { va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1; va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0; } static char* gv(const char* p, const char* k) { struct utun_config* c = parse_config(p); if (!c) return NULL; char* r = (strcmp(k, "pub") == 0) ? u_strdup(c->global.my_public_key_hex) : u_strdup(c->global.my_private_key_hex); free_config(c); return r; } static void t_mkdtemp(char* t) { (void)!mkdtemp(t); } static void t_rmdir(const char* p) { char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", p); system(cmd); } static int db_count(sqlite3* db, const char* tbl, const char* wh, uint64_t val) { char sql[256]; if (wh) snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM %s WHERE %s=%llu", tbl, wh, (unsigned long long)val); else snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM %s", tbl); sqlite3_stmt* s = NULL; if (sqlite3_prepare_v2(db, sql, -1, &s, NULL) != SQLITE_OK) return -1; int n = -1; if (sqlite3_step(s) == SQLITE_ROW) n = sqlite3_column_int(s, 0); sqlite3_finalize(s); return n; } static void to_cb(void* arg) { (void)arg; fprintf(stderr, " TIMEOUT\n"); g_result = 2; } static void mon(void* arg) { (void)arg; if (g_result) return; int ok = 0; for (int i = 0; i < N_NODES; i++) { struct ll_entry* e = g_inst[i]->connections->head; while (e) { struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; if (ce->conn && ce->conn->links) { struct ETCP_LINK* l; for (l = ce->conn->links; l; l = l->next) if (l->initialized && ce->conn->crypto_ctx.initialized) ok++; } e = e->next; } } if (ok >= N_NODES * 2 - 2) g_connected = 1; if (!g_result) uasync_set_timeout(g_ua, 10, NULL, mon, "md_mon"); } static int msend(struct UTUN_INSTANCE* inst, uint64_t dst, const uint8_t* data, size_t len) { struct ll_entry* e = queue_entry_new(0); if (!e) return -1; e->dgram = u_malloc(len + 1); if (!e->dgram) { queue_entry_free(e); return -1; } e->dgram[0] = ETCP_RT_ID_MEDIA_DELIVERY; memcpy(e->dgram + 1, data, len); e->len = (uint16_t)(len + 1); int rc = etcp_route_send(inst, TOPO_GROUP_UTUN, dst, e, 1); if (rc != 0) { u_free(e->dgram); queue_entry_free(e); } return rc; } /* ── Phase P1: create CHAT group + members on all nodes ── */ static void setup_chat_group(void) { uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ch_sig[64] = {0}; int i; TEST("generate channel keys"); { EVP_PKEY* xpkey = NULL, *epkey = NULL; EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_generate(ctx, &xpkey); EVP_PKEY_CTX_free(ctx); } if (xpkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(xpkey, x25519_pub, &l); EVP_PKEY_free(xpkey); } ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_generate(ctx, &epkey); EVP_PKEY_CTX_free(ctx); } if (epkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(epkey, ed_pub, &l); /* sign channel */ EVP_MD_CTX* mctx = EVP_MD_CTX_new(); if (mctx) { EVP_DigestSignInit(mctx, NULL, NULL, NULL, epkey); size_t sl = 64; EVP_DigestSign(mctx, ch_sig, &sl, (const uint8_t*)CH_ID, strlen(CH_ID)); EVP_MD_CTX_free(mctx); } EVP_PKEY_free(epkey); } if (x25519_pub[0] || ed_pub[0]) OK(); else FAIL(); } TEST("create CHAT groups + peers tables"); { for (i = 0; i < N_NODES; i++) { sqlite3* db = g_inst[i]->topo_sqlite_db; struct TOPO_GROUPS* tg = g_inst[i]->topo_groups; if (!db || !tg) continue; topo_node_sqlite_channel_put(db, CH_ID, "test", g_nid[0], x25519_pub, NULL, ed_pub, NULL, ch_sig); if (topo_groups_find(tg, g_group_id)) continue; topo_groups_create_group(tg, g_group_id, TOPO_GROUP_TYPE_CHAT, CH_ID); } OK(); } TEST("add all 4 members (n1, n2, s1 supernode, s2 supernode)"); { const char* adm[4] = { NULL, NULL, "supernode=yes", "supernode=yes" }; for (i = 0; i < N_NODES; i++) { sqlite3* db = g_inst[i]->topo_sqlite_db; if (!db) continue; for (int m = 0; m < N_NODES; m++) { member_sync_put(g_inst[i], CH_ID, g_nid[m], g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, NULL, 0, NULL, 0, "{\"name\":\"node\"}", NULL, 0, adm[m], NULL, 0); } } int ok = 1; for (i = 0; i < N_NODES; i++) if (member_sync_count(g_inst[i], CH_ID) < N_NODES) ok = 0; if (ok) OK(); else FAIL(); } } /* ── Phase A1: SUPER_HELLO ── */ static void phase_a1_super_hello(void) { TEST("s1,s2 are supernodes"); { media_delivery_set_supernode(g_inst[2], 1); media_delivery_set_supernode(g_inst[3], 1); topo_node_sqlite_nodeinfo_updated(g_inst[2]->topo_sqlite_db, g_nid[2]); topo_node_sqlite_nodeinfo_updated(g_inst[3]->topo_sqlite_db, g_nid[3]); if (g_inst[2]->md.is_supernode && g_inst[3]->md.is_supernode) OK(); else FAIL(); } TEST("SUPER_HELLO s1↔s2"); { struct media_pkt_super_hello h; h.subcmd = MEDIA_SUBCMD_SUPER_HELLO; h.last_recv_id = 0; msend(g_inst[2], g_nid[3], (const uint8_t*)&h, sizeof(h)); msend(g_inst[3], g_nid[2], (const uint8_t*)&h, sizeof(h)); int a = 0; while (a < 500) { uasync_poll(g_ua, POLL_MS); a++; } int ok1 = g_inst[2]->md.super_peers && g_inst[2]->md.super_peers->head != NULL; int ok2 = g_inst[3]->md.super_peers && g_inst[3]->md.super_peers->head != NULL; if (ok1 && ok2) OK(); else FAIL("s1=%d s2=%d", ok1, ok2); } } /* ── Phase A2: HAVE_BLOCK + replication ── */ static void phase_a2_have_block_replication(void) { uint8_t uuid[16]; memset(uuid, 0xAB, 16); TEST("HAVE_BLOCK n2→s1"); { struct media_pkt_have_block hb; memset(&hb, 0, sizeof(hb)); hb.subcmd = MEDIA_SUBCMD_HAVE_BLOCK; hb.group_id = g_group_id; memcpy(hb.block_id, uuid, 16); memcpy(hb.media_id, uuid, 16); hb.chunk = 0; hb.timestamp = (int64_t)time(NULL); msend(g_inst[1], g_nid[2], (const uint8_t*)&hb, sizeof(hb)); int a = 0; while (a < 500) { if (db_count(g_inst[2]->topo_sqlite_db, "block_availability", "node_id", g_nid[1]) >= 1) break; uasync_poll(g_ua, POLL_MS); a++; } if (db_count(g_inst[2]->topo_sqlite_db, "block_availability", "node_id", g_nid[1]) >= 1) OK(); else FAIL(); } TEST("SUPER_REPL s1→s2"); { int a = 0; while (a < 500) { uasync_poll(g_ua, POLL_MS); a++; } if (db_count(g_inst[3]->topo_sqlite_db, "block_availability", NULL, 0) >= 1) OK(); else FAIL(); } TEST("s2 super_sync updated"); { sqlite3_stmt* st = NULL; uint64_t lr = 0; sqlite3_prepare_v2(g_inst[3]->topo_sqlite_db, "SELECT last_recv_id FROM super_sync WHERE peer_node_id=?", -1, &st, NULL); sqlite3_bind_int64(st, 1, (sqlite3_int64)g_nid[2]); if (sqlite3_step(st) == SQLITE_ROW) lr = (uint64_t)sqlite3_column_int64(st, 0); sqlite3_finalize(st); if (lr > 0) OK(); else FAIL(); } } /* ── Phase A3: admission control ── */ static void phase_a3_admission(void) { TEST("1st BLOCK_REQ → stream"); { uint8_t mid[16], bid[16]; memset(mid, 0xCD, 16); memset(bid, 0xCE, 16); struct media_pkt_block_req req; memset(&req, 0, sizeof(req)); req.subcmd = MEDIA_SUBCMD_BLOCK_REQ; req.group_id = g_group_id; memcpy(req.media_id, mid, 16); memcpy(req.block_id, bid, 16); msend(g_inst[1], g_nid[0], (const uint8_t*)&req, sizeof(req)); int a = 0; while (a < 200) { uasync_poll(g_ua, POLL_MS); a++; } if (g_inst[0]->md.active_streams >= 1) OK(); else FAIL("active=%d", g_inst[0]->md.active_streams); } TEST("fill → 3 → OVERLOADED"); { for (int i = 0; i < 3; i++) { uint8_t mid[16], bid[16]; memset(mid, i+10, 16); memset(bid, i+20, 16); struct media_pkt_block_req req; memset(&req, 0, sizeof(req)); req.subcmd = MEDIA_SUBCMD_BLOCK_REQ; req.group_id = g_group_id; req.chunk = (uint32_t)i; memcpy(req.media_id, mid, 16); memcpy(req.block_id, bid, 16); msend(g_inst[1], g_nid[0], (const uint8_t*)&req, sizeof(req)); } int a = 0; while (a < 200) { uasync_poll(g_ua, POLL_MS); a++; } if (g_inst[0]->md.active_streams == 3) OK(); else FAIL("active=%d", g_inst[0]->md.active_streams); } g_inst[0]->md.active_streams = 0; } /* ══════════════════════════════════════════════════════════ Phase B1: create file → register → stream → assemble → verify ══════════════════════════════════════════════════════════ */ static void phase_b1_file_transfer(void) { char src_tmp[512]; snprintf(src_tmp, sizeof(src_tmp), "/tmp/mdl_test_%d.bin", getpid()); char media_dir[512]; snprintf(media_dir, sizeof(media_dir), "%s/media", g_db_dir[0]); utun_mkdir(media_dir, 0755); char dst_path[512]; snprintf(dst_path, sizeof(dst_path), "%s/test_src.bin", media_dir); char media_base[512]; snprintf(media_base, sizeof(media_base), "%s", g_db_dir[0]); uint8_t file_data[2048]; for (int i = 0; i < 2048; i++) file_data[i] = (uint8_t)(i & 0xFF); FILE* f = fopen(src_tmp, "wb"); if (f) { fwrite(file_data, 1, 2048, f); fclose(f); } { FILE* fin = fopen(src_tmp, "rb"), *fout = fopen(dst_path, "wb"); if (fin && fout) { uint8_t buf[4096]; size_t rd; while ((rd = fread(buf, 1, sizeof(buf), fin)) > 0) fwrite(buf, 1, rd, fout); } if (fin) fclose(fin); if (fout) fclose(fout); } TEST("media_index_commit + ui_state"); { uint8_t hash[32]; EVP_MD_CTX* ctx = EVP_MD_CTX_new(); EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); EVP_DigestUpdate(ctx, file_data, 2048); EVP_DigestFinal_ex(ctx, hash, NULL); EVP_MD_CTX_free(ctx); media_index_init(g_inst[0]->topo_sqlite_db); sqlite3_exec(g_inst[0]->topo_sqlite_db, "CREATE TABLE IF NOT EXISTS ui_state(key TEXT PRIMARY KEY, value TEXT)", NULL, NULL, NULL); { sqlite3_stmt* us = NULL; sqlite3_prepare_v2(g_inst[0]->topo_sqlite_db, "INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base',?)", -1, &us, NULL); sqlite3_bind_text(us, 1, media_base, -1, SQLITE_STATIC); sqlite3_step(us); sqlite3_finalize(us); } struct media_index_result result; memset(&result, 0, sizeof(result)); media_index_generate_uuid(result.media_id); memcpy(result.content_hash, hash, 32); result.file_size = 2048; result.block_size = 1024; result.num_blocks = 2; result.block_ids = u_malloc(32); result.block_sigs = u_malloc(128); for (int i = 0; i < 2; i++) { media_index_generate_uuid(result.block_ids + i * 16); uint8_t smsg[2048]; size_t soff = 0; memcpy(smsg + soff, file_data + i * 1024, 1024); soff += 1024; uint64_t nid = g_nid[0]; memcpy(smsg + soff, &nid, 8); soff += 8; sc_ed25519_sign(g_inst[0]->my_ed25519_privkey, smsg, soff, result.block_sigs + i * 64); } int rc = media_index_commit(g_inst[0]->topo_sqlite_db, &result, g_nid[0], g_inst[0]->my_ed25519_privkey, "test_ch", dst_path, media_base); if (rc != 0) { FAIL("commit rc=%d", rc); media_index_result_free(&result); return; } memcpy(g_test_mid, result.media_id, 16); memcpy(g_test_bid0, result.block_ids, 16); memcpy(g_test_bid1, result.block_ids + 16, 16); int ndb = db_count(g_inst[0]->topo_sqlite_db, "media_files", NULL, 0); media_index_result_free(&result); if (ndb == 2) OK(); else FAIL("media_files has %d rows (expected 2)", ndb); } TEST("n2→n1 BLOCK_REQ streams data"); { struct media_pkt_block_req req; memset(&req, 0, sizeof(req)); req.subcmd = MEDIA_SUBCMD_BLOCK_REQ; req.group_id = g_group_id; memcpy(req.media_id, g_test_mid, 16); memcpy(req.block_id, g_test_bid0, 16); req.chunk = 0; g_inst[0]->md.stream_completed = 0; msend(g_inst[1], g_nid[0], (const uint8_t*)&req, sizeof(req)); int a = 0; while (a < 2000 && !g_inst[0]->md.stream_completed) { uasync_poll(g_ua, POLL_MS); a++; } if (g_inst[0]->md.stream_completed) OK(); else FAIL("stream not completed after %dms", 2000 * POLL_MS); } } /* ── main ── */ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); utun_instance_set_tun_init_enabled(0); srand((unsigned)time(NULL)); printf("=== test_media_delivery_full ===\n"); t_mkdtemp(g_tdir); for (int i = 0; i < N_NODES; i++) { snprintf(g_cfg[i], sizeof(g_cfg[i]), "%s/n%d.conf", g_tdir, i); snprintf(g_db_dir[i], sizeof(g_db_dir[i]), "%s/db%d", g_tdir, i); utun_mkdir(g_db_dir[i], 0755); g_port[i] = 51000 + (getpid() % 5000) + i * 1000; } g_ua = uasync_create(); for (int i = 0; i < N_NODES; i++) wf(g_cfg[i], "[global]\ntun_ip=10.99.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s\n" "[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", i, i, g_db_dir[i], g_port[i]); for (int i = 0; i < N_NODES; i++) { config_ensure_keys_and_node_id(g_cfg[i]); struct utun_config* c = parse_config(g_cfg[i]); g_nid[i] = c->global.my_node_id; free_config(c); } for (int i = 0; i < N_NODES; i++) { char *pr = gv(g_cfg[i], "priv"), *pu = gv(g_cfg[i], "pub"); if (i == 0) { wf(g_cfg[i], "[global]\nmy_private_key=%s\nmy_public_key=%s\ntun_ip=10.99.%d.1/24\ntun_ifname=tun%d0\n" "db_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", pr, pu, i, i, g_db_dir[i], g_port[i]); } else { int prev = i - 1; char *pv_pu = gv(g_cfg[prev], "pub"); char link[256]; snprintf(link, sizeof(link), "[client: to_n%d]\nkeepalive=1\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", prev, pv_pu, g_port[prev]); wf(g_cfg[i], "[global]\nmy_private_key=%s\nmy_public_key=%s\ntun_ip=10.99.%d.1/24\ntun_ifname=tun%d0\n" "db_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n%s[allowed_keys]\nallow_all=1\n", pr, pu, i, i, g_db_dir[i], g_port[i], link); u_free(pv_pu); } u_free(pr); u_free(pu); } for (int i = 0; i < N_NODES; i++) { g_inst[i] = utun_instance_create(g_ua, g_cfg[i]); if (!g_inst[i]) { printf("FAIL: create n%d\n", i); goto done; } utun_instance_init(g_inst[i]); } mon(NULL); uasync_set_timeout(g_ua, TIMEOUT_TB, NULL, (timeout_callback_t)to_cb, "md_to"); { int el = 0; while (!g_connected && !g_result && el < TIMEOUT_TB + 5000) { uasync_poll(g_ua, POLL_MS); el += POLL_MS; } if (!g_connected) { printf("FAIL: connection timeout\n"); goto done; } } printf(" connected\n"); g_group_id = strtoull(CH_ID, NULL, 10); /* ══ Phase P1: create CHAT groups + members ══ */ setup_chat_group(); phase_a1_super_hello(); phase_a2_have_block_replication(); phase_a3_admission(); phase_b1_file_transfer(); fflush(stdout); fflush(stderr); printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED); fflush(stdout); done: g_result = 1; for (int i = 0; i < N_NODES; i++) { if (g_inst[i]) { g_inst[i]->running = 0; utun_instance_destroy(g_inst[i]); g_inst[i] = NULL; } } if (g_ua) { uasync_destroy(g_ua, 0); g_ua = NULL; } t_rmdir(g_tdir); return G_FAILED > 0 ? 1 : 0; }