From b9af72e71da04f2b4c74795e94baaee1e2605a3d Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 4 Oct 2026 15:12:26 +0200 Subject: [PATCH] Test PM invitation cancellation, commit errors and joining another group --- tests/Makefile.am | 5 ++ tests/test_dm_invite.c | 189 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 tests/test_dm_invite.c diff --git a/tests/Makefile.am b/tests/Makefile.am index d2a79120..2f51ab33 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -84,6 +84,7 @@ check_PROGRAMS = \ test_chat_join \ test_chat_join_e2e \ test_dm_e2e \ + test_dm_invite \ test_dm_media \ test_dm_worker \ test_attachment \ @@ -507,6 +508,10 @@ test_dm_e2e_SOURCES = test_dm_e2e.c test_dm_e2e_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/dm -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib test_dm_e2e_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_dm_invite_SOURCES = test_dm_invite.c +test_dm_invite_CFLAGS = $(test_dm_e2e_CFLAGS) +test_dm_invite_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) + test_dm_media_SOURCES = test_dm_media.c test_dm_media_CFLAGS = $(test_dm_e2e_CFLAGS) test_dm_media_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_dm_invite.c b/tests/test_dm_invite.c new file mode 100644 index 00000000..a7a5b3ee --- /dev/null +++ b/tests/test_dm_invite.c @@ -0,0 +1,189 @@ +/* Приглашение в другую группу через настоящую PM: отмена, ошибки, UDP-доставка и JOIN. */ +#include +#include +#include +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../src/utun_instance.h" +#include "../src/chat/chat_core.h" +#include "../src/chat/chat_event.h" +#include "../src/chat/chat_join.h" +#include "../src/chat/chat_sync.h" +#include "../src/chat/invite_build.h" +#include "../src/chat/invite_link.h" +#include "../src/dm/dm_core.h" +#include "../src/dm/dm_crypto.h" +#include "../src/routing_layer/topo_node_sqlite.h" +#include "../src/transport_layer/secure_channel.h" +#include "test_utils.h" + +static struct UTUN_INSTANCE* nodes[2]; +static int results[16], calls[16], joined; +static uint64_t joining_channel; + +static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + if (type == CHAT_EVT_DM_INVITE_RESULT && len == 28) { + uint64_t id, peer, conv; int32_t result; + memcpy(&id, data, 8); memcpy(&result, data + 8, 4); + memcpy(&peer, data + 12, 8); memcpy(&conv, data + 20, 8); + if (id >= 16 || peer != nodes[1]->node_id || conv != dm_derive_conv_id(nodes[0]->node_id, peer)) abort(); + calls[id]++; results[id] = result; + } else if (inst == nodes[1] && type == CHAT_EVT_CONNECT_RESULT && len == 20) { + uint64_t channel; int32_t result; + memcpy(&result, data + 8, 4); memcpy(&channel, data + 12, 8); + if (channel == joining_channel) joined = result == 0 ? 1 : -1; + } +} + +struct link_result { int ready; char link[1024]; }; +static void link_ready(void* arg, int result, const char* link) { + struct link_result* state = arg; + state->ready = result == CHAT_JOIN_OK ? 1 : -1; + if (link) snprintf(state->link, sizeof(state->link), "%s", link); +} + +static int scalar(struct UTUN_INSTANCE* inst, const char* sql, uint64_t* value) { + sqlite3_stmt* statement = NULL; + int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &statement, NULL); + if (rc == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW) { + *value = (uint64_t)sqlite3_column_int64(statement, 0); rc = 0; + } else rc = -1; + sqlite3_finalize(statement); + return rc; +} + +static int message_count(void) { + uint64_t count = 0; + return scalar(nodes[0], "SELECT count(*) FROM dm_messages WHERE dir=1", &count) ? -1 : (int)count; +} + +static void request(uint64_t id, uint64_t group, const char* source, int cancel) { + struct dm_invite_req* req = u_calloc(1, sizeof(*req)); + if (!req) abort(); + req->inst = nodes[0]; req->request_id = id; req->peer_node_id = nodes[1]->node_id; + req->channel_id = group; req->cancel = cancel; + snprintf(req->source_ch_id, sizeof(req->source_ch_id), "%s", source); + snprintf(req->peer_name, sizeof(req->peer_name), "B"); + dm_invite_trampoline(req); +} + +static int wait_result(struct UASYNC* ua, int id, int expected) { + uint64_t deadline = get_time_tb() + 30000; + while (!calls[id] && get_time_tb() < deadline) uasync_poll(ua, 100); + return calls[id] == 1 && results[id] == expected; +} + +static int join_link(struct UASYNC* ua, const char* link) { + struct InviteData invite; char error[128]; uint8_t addresses[1024]; + if (invite_link_decode(link, strlen(link), &invite, error, sizeof(error))) return -1; + int bytes = invite_serialize_addrs(&invite, addresses, sizeof(addresses)); + if (bytes < 0) return -1; + joining_channel = invite.channelId; joined = 0; + chat_sync_connect_from_invite(nodes[1], invite.channelId, invite.nodeId, invite.pubkey, + addresses, invite.addrCount, bytes, NULL, invite.join_key); + uint64_t deadline = get_time_tb() + 200000; + while (!joined && get_time_tb() < deadline) uasync_poll(ua, 100); + if (joined != 1) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite test: JOIN failed ch=%llu result=%d", + (unsigned long long)joining_channel, joined); return -1; } + return 0; +} + +static int start_node(struct UASYNC* ua, const char* directory, int index, int port) { + struct SC_MYKEYS keys; + if (sc_generate_keypair(&keys) != SC_OK) return -1; + char pub[65], priv[65], path[768], database[768]; + for (int i = 0; i < 32; i++) { + snprintf(pub + i * 2, 3, "%02x", keys.public_key[i]); + snprintf(priv + i * 2, 3, "%02x", keys.private_key[i]); + } + snprintf(database, sizeof(database), "%s/db%d", directory, index); + if (utun_mkdir(database, 0700)) return -1; + snprintf(path, sizeof(path), "%s/%d.conf", directory, index); + FILE* config = fopen(path, "w"); + if (!config) return -1; + fprintf(config, "[global]\nmy_public_key=%s\nmy_private_key=%s\ndb_path=%s\nmy_node_name=%c\n" + "[server:s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n" + "[chatserver]\nstorage_autoload=0\n", pub, priv, database, 'A' + index, port); + fclose(config); + nodes[index] = utun_instance_create(ua, path); + if (!nodes[index] || utun_instance_init(nodes[index])) return -1; + chat_event_set_handler(nodes[index], event); + return 0; +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + if (getenv("UTUN_TEST_DEBUG")) { + debug_set_category_level(DEBUG_CATEGORY_DM, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_MEMBER_SYNC, DEBUG_LEVEL_INFO); + } + utun_instance_set_tun_init_enabled(0); + struct UASYNC* ua = uasync_create(); + char directory[512] = "/tmp/utun_dm_invite_XXXXXX"; + int failed = 1; +#define CHECK(condition, reason) do { if (!(condition)) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite test: %s", reason); goto done; } } while (0) + CHECK(ua && !test_mkdtemp(directory), "temporary directory/event loop"); + int port = 32000 + getpid() % 10000; + CHECK(!start_node(ua, directory, 0, port) && !start_node(ua, directory, 1, port + 1), "start nodes"); + chat_core_create_channel_auto(nodes[0], "source"); + uint64_t source = 0, target = 0; + CHECK(!scalar(nodes[0], "SELECT channel_id FROM channels WHERE name='source'", &source), "create source group"); + char source_id[64]; snprintf(source_id, sizeof(source_id), "%llu", (unsigned long long)source); + struct link_result first = {0}; + CHECK(chat_invite_build_link(nodes[0], source, 0, NULL, link_ready, &first), "build initial invite"); + uint64_t deadline = get_time_tb() + 30000; + while (!first.ready && get_time_tb() < deadline) uasync_poll(ua, 100); + CHECK(first.ready == 1 && !join_link(ua, first.link), "join common source group"); + chat_core_create_channel_auto(nodes[0], "target"); + CHECK(!scalar(nodes[0], "SELECT channel_id FROM channels WHERE name='target'", &target), "create different target group"); + CHECK(target != source && !message_count(), "initial empty PM"); + + request(1, target, source_id, 0); + CHECK(!message_count() && !calls[1], "no PM before registration ACK"); + request(2, target, source_id, 0); + CHECK(wait_result(ua, 2, DM_INVITE_BUSY), "busy request retains first operation"); + request(1, target, source_id, 1); + CHECK(wait_result(ua, 1, DM_INVITE_CANCELLED) && !message_count(), "cancel before ACK sends nothing"); + request(3, 0, source_id, 0); + CHECK(wait_result(ua, 3, DM_INVITE_BUILD_ERROR) && !message_count(), "invalid group sends nothing"); + + CHECK(sqlite3_exec(nodes[0]->topo_sqlite_db, "CREATE TEMP TRIGGER fail_invite BEFORE INSERT ON dm_outbox " + "BEGIN SELECT RAISE(ABORT,'test invite commit failure'); END", NULL, NULL, NULL) == SQLITE_OK, "install failed commit"); + request(4, target, source_id, 0); + CHECK(wait_result(ua, 4, DM_INVITE_SEND_ERROR) && !message_count(), "failed commit returns error and rolls back PM"); + CHECK(sqlite3_exec(nodes[0]->topo_sqlite_db, "DROP TRIGGER fail_invite", NULL, NULL, NULL) == SQLITE_OK, "remove failed commit"); + + request(5, target, "", 0); /* из шапки существующей PM исходная группа не передаётся */ + request(4, target, "", 1); /* запоздалая отмена другой операции не отменяет текущую */ + CHECK(wait_result(ua, 5, DM_INVITE_OK) && message_count() == 1, "one invitation committed for fixed peer"); + char conv[64], messages[8192]; size_t length = 0; + snprintf(conv, sizeof(conv), "%llu", (unsigned long long)dm_derive_conv_id(nodes[0]->node_id, nodes[1]->node_id)); + deadline = get_time_tb() + 100000; + char* link = NULL; + do { + uasync_poll(ua, 100); + if (!dm_list_messages_json(nodes[1], conv, 10, 0, messages, sizeof(messages), &length)) link = strstr(messages, "utun://"); + } while (!link && get_time_tb() < deadline); + CHECK(link, "invite delivered to B over source group's UDP route"); + char* end = strchr(link, '"'); CHECK(end, "complete invite text in PM"); *end = '\0'; + struct InviteData received; char error[128]; + CHECK(!invite_link_decode(link, strlen(link), &received, error, sizeof(error)) && received.channelId == target, + "PM invites target group rather than source group"); + CHECK(JOIN_KEY_TTL_SECONDS == 600 && chat_join_lookup_inviter(nodes[0], target, received.join_key) == nodes[0]->node_id, + "delivered link is registered with ten minute lifetime"); + CHECK(!join_link(ua, link), "B joins target group using received PM link"); + char target_id[64]; snprintf(target_id, sizeof(target_id), "%llu", (unsigned long long)target); + CHECK(topo_node_sqlite_member_in_channel(nodes[0]->topo_sqlite_db, target_id, nodes[1]->node_id) && + topo_node_sqlite_member_in_channel(nodes[1]->topo_sqlite_db, target_id, nodes[1]->node_id), "membership committed on both nodes"); + request(6, target, "", 0); + dm_core_destroy(nodes[0]); + CHECK(calls[6] == 1 && results[6] == DM_INVITE_CANCELLED && message_count() == 1, "teardown cancels pending invitation"); + failed = 0; +done: + for (int i = 0; i < 2; i++) if (nodes[i]) utun_instance_destroy(nodes[i]); + if (ua) uasync_destroy(ua, 0); + printf("PM invite: cancellation, failure, delivery and JOIN: %s\n", failed ? "FAIL" : "OK"); + return failed; +}