diff --git a/tests/Makefile.am b/tests/Makefile.am index e1f4ba5f..56b2646c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -84,6 +84,7 @@ check_PROGRAMS = \ test_dm_media \ test_dm_worker \ test_attachment \ + test_attachment_send \ test_call \ test_call_headless \ test_radio \ @@ -194,6 +195,8 @@ test_reality_config_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_dm_SOURCES = test_dm.c test_attachment_SOURCES = test_attachment.c test_attachment_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_attachment_send_SOURCES = test_attachment_send.c +test_attachment_send_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_dm_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib test_dm_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_attachment_send.c b/tests/test_attachment_send.c new file mode 100644 index 00000000..ef68be6d --- /dev/null +++ b/tests/test_attachment_send.c @@ -0,0 +1,99 @@ +/* Общая подготовка: закреплённый target, реальный Opus, отмена и собственный cache. */ +#include +#include +#include +#include "../lib/mem.h" +#include "../lib/debug_config.h" +#include "../src/utun_instance.h" +#include "../src/chat/chat_core.h" +#include "../src/chat/chat_event.h" +#include "../src/dm/dm_core.h" +#include "../src/dm/dm_crypto.h" +#include "../src/media_async/attachment_send.h" +#include "test_utils.h" + +#define REQUIRE(value) do { if (!(value)) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment send check line=%d", __LINE__); return 1; } } while (0) +struct test_state { + pthread_t event_thread; + char target[68]; + int released, release_in_worker, starts, ready, failed; + int16_t pcm[48000]; +}; +static struct test_state test; + +static void release_pcm(void* owner) { + struct test_state* state = owner; + state->released++; state->release_in_worker = !pthread_equal(pthread_self(), state->event_thread); +} + +static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int length) { + (void)inst; + if (type != CHAT_EVT_ATTACHMENT_PREPARATION) return; + if (length < 18 || length != 18 + data[0] || data[0] >= sizeof(test.target)) { test.failed++; return; } + memcpy(test.target, data + 1, data[0]); test.target[data[0]] = 0; + int state = data[length - 1]; + if (state == 0) test.starts++; + else if (state == 1) test.ready++; + else test.failed++; +} + +static int count(struct UTUN_INSTANCE* inst) { + sqlite3_stmt* statement = NULL; + if (sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT count(*) FROM dm_messages", -1, &statement, NULL)) return -1; + int result = sqlite3_step(statement) == SQLITE_ROW ? sqlite3_column_int(statement, 0) : -1; + sqlite3_finalize(statement); return result; +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + test.event_thread = pthread_self(); + char dir[512] = "/tmp/utun_attachment_XXXXXX", config[1024], conv[64]; + REQUIRE(!test_mkdtemp(dir)); + snprintf(config, sizeof(config), "[global]\ndb_path=%s\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", dir); + struct UASYNC* ua = uasync_create(); REQUIRE(ua); + struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); REQUIRE(inst); + REQUIRE(!utun_core_start(inst) && !chat_service_start(inst)); + chat_event_set_handler(inst, event); + struct SC_MYKEYS peer; uint8_t ed[32]; + REQUIRE(!sc_generate_keypair(&peer) && !sc_derive_ed25519_pubkey(peer.private_key, ed)); + uint64_t peer_id = sc_derive_node_id_from_pubkey(peer.public_key); + REQUIRE(!dm_start(inst, peer_id, peer.public_key, ed, "test", NULL)); + snprintf(conv, sizeof(conv), "%llu", (unsigned long long)dm_derive_conv_id(inst->node_id, peer_id)); + struct attachment_send_req* req = u_calloc(1, sizeof(*req)); REQUIRE(req); + req->inst = inst; req->is_dm = 1; req->info.kind = ATTACHMENT_VOICE; req->preset = 1; + snprintf(req->target, sizeof(req->target), "%s", conv); + req->pcm = test.pcm; req->pcm_count = 48000; req->pcm_owner = &test; req->pcm_release = release_pcm; + char active_target[64]; snprintf(active_target, sizeof(active_target), "%s", conv); + attachment_send_trampoline(req); + snprintf(active_target, sizeof(active_target), "999"); /* Выбор GUI изменился до completion. */ + for (unsigned i = 0; i < 500 && count(inst) == 0; i++) uasync_poll(ua, 20); + REQUIRE(count(inst) == 1 && test.released == 1 && test.release_in_worker && test.starts == 2 && test.ready == 2 && !test.failed); + char expected[68], json[8192]; size_t length; + snprintf(expected, sizeof(expected), "dm:%s", conv); + REQUIRE(!strcmp(test.target, expected) && strcmp(active_target, conv)); + REQUIRE(!dm_list_messages_json(inst, conv, 20, 0, json, sizeof(json), &length)); + REQUIRE(strstr(json, "\"content_type\":\"audio/opus\"") && strstr(json, "\"duration_ms\":1000")); + REQUIRE(!dm_list_messages_json(inst, active_target, 20, 0, json, sizeof(json), &length) && !strcmp(json, "[]")); + + /* Остановка до poll завершает preparation с CANCELLED, без регистрации сообщения. */ + req = u_calloc(1, sizeof(*req)); REQUIRE(req); + req->inst = inst; req->is_dm = 1; req->info.kind = ATTACHMENT_VOICE; req->preset = 1; + snprintf(req->target, sizeof(req->target), "%s", conv); + req->pcm = test.pcm; req->pcm_count = 48000; req->pcm_owner = &test; req->pcm_release = release_pcm; + attachment_send_trampoline(req); + chat_service_stop(inst); + REQUIRE(test.released == 2 && test.failed == 1 && count(inst) == 1); + uasync_poll(ua, 0); REQUIRE(test.failed == 1); + + /* Отклонение после stop удаляет только переданный собственный cache. */ + req = u_calloc(1, sizeof(*req)); REQUIRE(req); + req->inst = inst; req->is_dm = 1; req->temporary_source = 1; snprintf(req->target, sizeof(req->target), "%s", conv); + snprintf(req->source, sizeof(req->source), "%s/cache.mp4", dir); + char cache[1024]; snprintf(cache, sizeof(cache), "%s", req->source); + FILE* file = fopen(cache, "wb"); REQUIRE(file); REQUIRE(fputc(42, file) != EOF && !fclose(file)); + attachment_send_trampoline(req); + REQUIRE(test.failed == 2 && access(cache, F_OK) != 0); + utun_instance_destroy(inst); uasync_destroy(ua, 0); + printf("test_attachment_send: pinned PM target, worker PCM ownership, cancel and cache cleanup PASS\n"); + return 0; +} diff --git a/tests/test_dm_media.c b/tests/test_dm_media.c index ab11ca5e..d76a78eb 100644 --- a/tests/test_dm_media.c +++ b/tests/test_dm_media.c @@ -275,6 +275,12 @@ static void tick(void* arg) { case 5: if (file_correct(t) && value(m, "SELECT count(*) FROM dm_custody") == 0 && value(s, "SELECT count(*) FROM dm_media_jobs WHERE receipt IS NOT NULL AND body IS NULL") == 1) { + char json[8192]; size_t length; + if (dm_list_messages_json(b, t->conv, 20, 0, json, sizeof(json), &length) || + !strstr(json, "\"content_type\":\"video/mp4\"") || !strstr(json, "\"duration_ms\":3000") || + !strstr(json, "\"width\":640") || !strstr(json, "\"height\":480")) { + failure(t, "encrypted video metadata lost after offline custody restart"); return; + } uint8_t stored[24]; memcpy(stored, t->id, 16); uint64_t expiry = ntp_time_get_seconds(s) + 100; memcpy(stored + 16, &expiry, 8); control(t, S, t->ids[M], DM_MEDIA_STORED, stored, sizeof(stored));