diff --git a/src/dm/dm_core.c b/src/dm/dm_core.c index dd32153bb..9e01260c0 100644 --- a/src/dm/dm_core.c +++ b/src/dm/dm_core.c @@ -1140,7 +1140,7 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf int failed = strbuf_addf(&json, "[") < 0; sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(dm->db, - "SELECT c.conv_id,peer_node_id,COALESCE(peer_name,''),last_ts,last_out_seq,last_in_seq,COALESCE(h.generation,1)" + "SELECT c.conv_id,peer_node_id,COALESCE(peer_name,''),last_ts,last_out_seq,last_in_seq,COALESCE(h.generation,1),mode,closed" " FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id) WHERE COALESCE(h.hidden,0)=0" " ORDER BY last_ts DESC LIMIT 200", -1, &st, NULL) == SQLITE_OK) { @@ -1166,11 +1166,12 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf json_escape(safe_name, esc_name, sizeof(esc_name)); if (strbuf_addf(&json, "%s{\"conv_id\":\"%s\",\"peer_node_id\":%llu,\"peer_name\":\"%s\"," - "\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu,\"generation\":%llu}", + "\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu,\"generation\":%llu,\"mode\":%d,\"closed\":%s}", first ? "" : ",", conv_id, (unsigned long long)peer, esc_name, (unsigned long long)last_ts, (unsigned long long)out_seq, (unsigned long long)in_seq, - (unsigned long long)sqlite3_column_int64(st, 6)) < 0) { failed = 1; break; } + (unsigned long long)sqlite3_column_int64(st, 6), sqlite3_column_int(st, 7), + sqlite3_column_int(st, 8) ? "true" : "false") < 0) { failed = 1; break; } first = 0; } if (sqlite3_errcode(dm->db) != SQLITE_DONE && sqlite3_errcode(dm->db) != SQLITE_OK) failed = 1; @@ -1204,15 +1205,16 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c const char* sql = "SELECT * FROM (SELECT m.dir,m.seq,m.ts,m.author,m.ct,m.data,o.seq IS NOT NULL AS pending," "COALESCE(f.ready,0) AS ready,COALESCE(f.downloading,0) AS downloading,f.receipt," "COALESCE(f.local_path,'') AS path,COALESCE(f.plain_size,0) AS size" - " FROM dm_messages m LEFT JOIN dm_outbox o ON m.dir=1 AND o.conv_id=m.conv_id AND o.seq=m.seq" - " LEFT JOIN dm_files f ON f.conv_id=m.conv_id AND f.dir=m.dir AND f.seq=m.seq" - " WHERE m.conv_id=? ORDER BY m.ts DESC,m.seq DESC,m.dir DESC LIMIT ? OFFSET ?) ORDER BY ts,seq,dir"; + " FROM dm_messages m LEFT JOIN dm_outbox o ON m.author=?4 AND o.conv_id=m.conv_id AND o.seq=m.seq" + " LEFT JOIN dm_files f ON f.conv_id=m.conv_id AND f.author=m.author AND f.seq=m.seq" + " WHERE m.conv_id=?1 ORDER BY m.ts DESC,m.seq DESC,m.author DESC LIMIT ?2 OFFSET ?3) ORDER BY ts,seq,author"; struct strbuf sb = strbuf_new(); uint64_t conv_num = (uint64_t)strtoull(conv_id, NULL, 10); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(dm->db, sql, -1, &st, NULL) != SQLITE_OK || strbuf_addf(&sb, "[") < 0) goto fail; sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); sqlite3_bind_int(st, 2, count); sqlite3_bind_int(st, 3, offset); + sqlite3_bind_int64(st, 4, (sqlite3_int64)inst->node_id); int first = 1, rc; while ((rc = sqlite3_step(st)) == SQLITE_ROW) { int dir = sqlite3_column_int(st, 0); diff --git a/tests/test_dm_groups.c b/tests/test_dm_groups.c new file mode 100644 index 000000000..d1b796402 --- /dev/null +++ b/tests/test_dm_groups.c @@ -0,0 +1,149 @@ +/* Настоящие ядра/SQLite: снимок адресатов, собственные реплики, ACK и выход. */ +#include "utun_instance.h" +#include "chat/chat_core.h" +#include "chat/chat_devgroup.h" +#include "chat/member_sync.h" +#include "routing_layer/topo_group.h" +#include "routing_layer/topo_node_sqlite.h" +#include "dm/dm_core.h" +#include "dm/dm_crypto.h" +#include "dm/dm_history.h" +#include "../lib/debug_config.h" +#include "test_utils.h" +#include +#include +#include +#include + +static struct UTUN_INSTANCE* nodes[5]; +static char channel[64]; + +static int scalar(int node, const char* sql) { + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(nodes[node]->topo_sqlite_db, sql, -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_step(st) == SQLITE_ROW); + int value = sqlite3_column_int(st, 0); assert(sqlite3_finalize(st) == SQLITE_OK); return value; +} + +static void create_nodes(struct UASYNC* ua, const char* root) { + for (unsigned i = 0; i < 5; i++) { + struct SC_MYKEYS keys; assert(sc_generate_keypair(&keys) == SC_OK); + char pub[65], priv[65], config[2048]; + for (unsigned k = 0; k < 32; k++) { + snprintf(pub + 2 * k, 3, "%02x", keys.public_key[k]); + snprintf(priv + 2 * k, 3, "%02x", keys.private_key[k]); + } + snprintf(config, sizeof(config), "[global]\nmy_public_key=%s\nmy_private_key=%s\ndb_path=%s/%u\n" + "[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", pub, priv, root, i); + nodes[i] = utun_instance_create_from_str(ua, config); + assert(nodes[i] && !utun_core_start(nodes[i]) && !chat_service_start(nodes[i])); + } +} + +/* Общая CHAT-область даёт права маршрутизации; действующих маршрутов специально нет. */ +static void seed_channel(void) { + chat_core_create_channel_auto(nodes[0], "DM group test"); + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(nodes[0]->topo_sqlite_db, "SELECT channel_id FROM channels", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_step(st) == SQLITE_ROW); + snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0)); sqlite3_finalize(st); + uint8_t x[32], ed[32], sig[64], priv[32]; + assert(!topo_node_sqlite_channel_get(nodes[0]->topo_sqlite_db, channel, NULL, 0, NULL, x, ed, sig)); + assert(!topo_node_sqlite_channel_get_priv(nodes[0]->topo_sqlite_db, channel, priv)); + for (unsigned i = 1; i < 5; i++) { + assert(!topo_node_sqlite_channel_put(nodes[i]->topo_sqlite_db, channel, "DM group test", nodes[0]->node_id, + x, NULL, ed, NULL, sig)); + chat_core_ensure_channel_ready(nodes[i], channel); + } + for (unsigned dst = 0; dst < 5; dst++) for (unsigned author = 0; author < 5; author++) { + uint8_t body[112], join[64], tree[64]; + assert(member_sync_build_join_msg(x, ed, nodes[author]->node_id, nodes[author]->my_keys.public_key, 1, body, sizeof(body)) == 112); + assert(sc_ed25519_sign(nodes[author]->my_ed25519_privkey, body, sizeof(body), join) == SC_OK); + assert(!member_sync_sign_pubkey(priv, nodes[author]->my_keys.public_key, tree)); + assert(member_sync_put(nodes[dst], channel, nodes[author]->node_id, nodes[author]->my_keys.public_key, + nodes[author]->my_ed25519_pubkey, join, 1, NULL, 0, "{}", NULL, NULL, 0, 0, tree) >= 0); + } +} + +static void seed_groups(void) { + for (unsigned i = 0; i < 4; i += 2) { + assert(!chat_devgroup_rename(nodes[i], i ? "B" : "A")); + struct devgroup_record r; uint8_t ed[32], x[32], receipt[16] = {0}; receipt[0] = (uint8_t)(i + 1); + assert(devgroup_load_local(nodes[i], &r, ed, x) == 1); + assert(!chat_devgroup_import(nodes[i + 1], &r, ed, x, receipt, nodes[i]->node_id)); + } + for (unsigned dst = 0; dst < 5; dst++) for (unsigned src = 0; src < 5; src++) { + struct TOPO_NODE* n = topo_node_registry_find(nodes[src]->topo_groups, nodes[src]->node_id); assert(n); + assert(!devgroup_project(nodes[dst]->topo_sqlite_db, n)); + } +} + +static uint64_t start(int from, int to, enum dm_mode mode, char conv[64]) { + uint64_t id = 0; + assert(!dm_start(nodes[from], nodes[to]->node_id, nodes[to]->my_keys.public_key, nodes[to]->my_ed25519_pubkey, + "peer", channel, mode, &id)); + assert(id); snprintf(conv, 64, "%llu", (unsigned long long)id); return id; +} + +static size_t send_body(int from, const char* conv, const char* text, uint8_t body[DM_BODY_MAX]) { + assert(!dm_send(nodes[from], conv, "text", (const uint8_t*)text, (uint32_t)strlen(text))); + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(nodes[from]->topo_sqlite_db, + "SELECT body FROM dm_outbox WHERE conv_id=? ORDER BY seq DESC LIMIT 1", -1, &st, NULL) == SQLITE_OK); + sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); assert(sqlite3_step(st) == SQLITE_ROW); + size_t len = (size_t)sqlite3_column_bytes(st, 0); assert(len && len <= DM_BODY_MAX); + memcpy(body, sqlite3_column_blob(st, 0), len); sqlite3_finalize(st); return len; +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_DM, DEBUG_LEVEL_DEBUG); + char root[512] = "/tmp/utun_dm_groups_XXXXXX"; assert(!test_mkdtemp(root)); + struct UASYNC* ua = uasync_create(); assert(ua); create_nodes(ua, root); seed_channel(); seed_groups(); + char conv[64], other[64]; uint64_t group_id = start(0, 2, DM_MODE_GROUPS, conv); + assert(start(0, 3, DM_MODE_GROUPS, other) == group_id); + uint8_t body[DM_BODY_MAX], reply[DM_BODY_MAX], receipt[DM_RECEIPT_SIZE]; + size_t len = send_body(0, conv, "from A1", body); + struct dm_message_view view; assert(!dm_message_parse(body, len, &view) && view.target_count == 4 && view.seq == 1); + assert(scalar(0, "SELECT count(*) FROM dm_delivery") == 3); + for (int dst = 1; dst <= 3; dst++) { + assert(!dm_accept_message(nodes[dst], body, len, receipt)); + assert(!dm_accept_receipt(nodes[0], receipt)); + assert(scalar(0, "SELECT count(*) FROM dm_delivery WHERE receipt IS NULL") == 3 - dst); + assert(scalar(0, "SELECT count(*) FROM dm_outbox") == (dst < 3)); + assert(!dm_accept_message(nodes[dst], body, len, receipt)); + assert(scalar(dst, "SELECT count(*) FROM dm_messages") == 1); + } + assert(scalar(1, "SELECT dir FROM dm_messages") == 1 && scalar(1, "SELECT count(*) FROM dm_outbox") == 0); + assert(scalar(1, "SELECT last_out_seq FROM dm_conversations") == 0); + for (int author = 1; author <= 3; author++) { + size_t n = send_body(author, conv, "another author", reply); + assert(!dm_message_parse(reply, n, &view) && view.seq == 1); + assert(!dm_accept_message(nodes[0], reply, n, receipt)); + } + assert(scalar(0, "SELECT count(*) FROM dm_messages WHERE seq=1") == 4); + assert(scalar(0, "SELECT count(*) FROM dm_messages WHERE dir=0") == 2); + char json[16384]; size_t json_len; + assert(!dm_list_messages_json(nodes[0], conv, 200, 0, json, sizeof(json), &json_len)); + assert(strstr(json, "from A1") && strstr(json, "another author") && !strstr(json, "\"pending\":true")); + uint64_t mixed = start(0, 4, DM_MODE_GROUPS, other); + assert(mixed != group_id && mixed != start(0, 4, DM_MODE_DEVICE, other)); + size_t n = send_body(0, other, "device to D", reply); assert(!dm_message_parse(reply, n, &view)); + assert(view.mode == DM_MODE_DEVICE && view.seq == 2 && !dm_accept_message(nodes[4], reply, n, receipt)); + assert(start(0, 1, DM_MODE_DEVICE, other) == dm_derive_conv_id(nodes[0]->node_id, nodes[1]->node_id)); + n = send_body(0, other, "own device", reply); assert(!dm_accept_message(nodes[1], reply, n, receipt)); + /* Выход атомарен: ошибка SQL не удаляет секреты и не закрывает беседу. */ + assert(!sqlite3_exec(nodes[0]->topo_sqlite_db, "CREATE TEMP TRIGGER fail_close BEFORE UPDATE OF closed ON dm_conversations" + " BEGIN SELECT RAISE(ABORT,'injected closure failure'); END", NULL, NULL, NULL)); + assert(chat_devgroup_leave(nodes[0]) < 0 && !dm_conversation_access(nodes[0], conv, NULL, NULL)); + assert(!sqlite3_exec(nodes[0]->topo_sqlite_db, "DROP TRIGGER fail_close", NULL, NULL, NULL)); + send_body(0, conv, "pending at exit", reply); + assert(!chat_devgroup_leave(nodes[0])); + assert(scalar(0, "SELECT count(*) FROM devgroup_local") == 0); + assert(scalar(0, "SELECT count(*) FROM dm_outbox o JOIN dm_conversations c USING(conv_id) WHERE c.mode=1") == 0); + assert(dm_send(nodes[0], conv, "text", (const uint8_t*)"forbidden", 9) < 0); + n = send_body(2, conv, "after exit", reply); assert(dm_accept_message(nodes[0], reply, n, receipt) < 0); + assert(!dm_list_messages_json(nodes[0], conv, 200, 0, json, sizeof(json), &json_len) && strstr(json, "from A1")); + assert(!dm_conversation_access(nodes[0], other, NULL, NULL)); /* DEVICE остаётся действующей. */ + for (unsigned i = 0; i < 5; i++) utun_instance_destroy(nodes[i]); + uasync_destroy(ua); test_remove_dir(root); return 0; +}