From 87724433b1b51a136d595c3049b4b754ed21052a Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 22 Oct 2026 14:52:27 +0200 Subject: [PATCH] Exercise media index failure recovery and pending channel deletion --- tests/Makefile.am | 4 + tests/test_media_index_regressions.c | 212 +++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 tests/test_media_index_regressions.c diff --git a/tests/Makefile.am b/tests/Makefile.am index 5b8c3f5f..1a165480 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -874,6 +874,10 @@ test_media_index_SOURCES = test_media_index.c test_media_index_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_delivery -I$(top_srcdir)/src/media_async -I$(top_srcdir)/lib test_media_index_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +check_PROGRAMS += test_media_index_regressions +test_media_index_regressions_SOURCES = test_media_index_regressions.c +test_media_index_regressions_LDADD = $(LIBUTUN) $(COMMON_LIBS) + test_media_delivery_sql_SOURCES = test_media_delivery_sql.c test_media_delivery_sql_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_delivery -I$(top_srcdir)/lib test_media_delivery_sql_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_media_index_regressions.c b/tests/test_media_index_regressions.c new file mode 100644 index 00000000..774e5922 --- /dev/null +++ b/tests/test_media_index_regressions.c @@ -0,0 +1,212 @@ +/* Пути, атомарность SQLite, повтор удаления и поздние completion исходящих медиа. */ +#include "../lib/platform_compat.h" +#include "../lib/file_io.h" +#include "../lib/debug_config.h" +#include "../src/media_delivery/media_index.h" +#include "../src/media_async/media_async.h" +#include "../src/utun_instance.h" +#include "../src/chat/chat_core.h" +#include "../src/chat/chat_core_priv.h" +#include "../src/chat/chat_event.h" +#include "test_utils.h" +#include +#include +#include + +static char root[256] = "/tmp/media_index_regressions_XXXXXX"; +static uint8_t key[32] = {1}, ids[32] = {1}; +static struct media_index_result result = { .media_id = {2}, .content_hash = {3}, + .file_size = 14, .block_size = 7, .num_blocks = 2, .block_ids = ids }; + +static void sql(sqlite3* db, const char* query) { assert(sqlite3_exec(db, query, NULL, NULL, NULL) == SQLITE_OK); } +static int scalar(sqlite3* db, const char* query) { + sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(db, query, -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 sqlite3* database(void) { + sqlite3* db = NULL; assert(sqlite3_open(":memory:", &db) == SQLITE_OK); + assert(!media_index_init(db)); return db; +} +static void write_file(const char* path) { + FILE* file = fopen(path, "wb"); assert(file && fwrite("source-payload", 1, 14, file) == 14 && !fclose(file)); +} +static int commit(sqlite3* db, const char* channel, const char* path) { + return media_index_commit(db, &result, 1, key, channel, path, root); +} + +static void paths(void) { + char relative[MEDIA_PATH_MAX], path[MEDIA_PATH_MAX], base[MEDIA_BASE_MAX]; + assert(!media_index_location("/root///a/./file", "/root///", relative) && !strcmp(relative, "a/file")); + assert(!media_index_path("/", "a/file", path) && !strcmp(path, "/a/file")); + assert(!media_index_location("/a/file", "/", relative) && !strcmp(relative, "a/file")); + assert(!media_index_location("./a/file", ".", relative) && !strcmp(relative, "a/file")); + assert(media_index_location("/root2/file", "/root", relative) == -1); + assert(media_index_location("/root/../file", "/root", relative) == -1); + assert(media_index_location("/root", "/root", relative) == -1); + assert(media_index_path(root, "../file", path) == -1 && media_index_path(root, "/file", path) == -1); + assert(media_index_path(root, "a//file", path) == -1 && media_index_path(root, "a/./file", path) == -1); + assert(media_index_path(root, "C:/file", path) == -1 && media_index_path(root, "a\\file", path) == -1); + memset(relative, 'a', sizeof(relative)); relative[sizeof(relative) - 1] = 0; + assert(media_index_path("/", relative, path) == -1); + relative[sizeof(relative) - 2] = 0; + assert(!media_index_path("/", relative, path) && strlen(path) == MEDIA_PATH_MAX - 1); + sqlite3* db = database(); + assert(!media_index_set_base(db, "/root///") && !media_index_set_base(db, "/root/./")); + assert(!media_index_get_base(db, base) && !strcmp(base, "/root")); + assert(media_index_set_base(db, "/other") == -1); + assert(!media_index_get_base(db, base) && !strcmp(base, "/root")); sqlite3_close(db); +#ifdef _WIN32 + assert(!media_index_location("C:\\root\\a", "C:/root/", relative) && !strcmp(relative, "a")); + assert(!media_index_path("C:/", "a", path) && !strcmp(path, "C:/a")); + assert(!media_index_location("//server/share/a", "//server/share/", relative) && !strcmp(relative, "a")); +#endif + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression paths/base boundaries PASS"); +} + +struct denied { const char* index; int release; }; +static int authorizer(void* arg, int action, const char* one, const char* two, const char* db, const char* trigger) { + (void)db; (void)trigger; struct denied* denied = arg; + if (action == SQLITE_CREATE_INDEX && denied->index && !strcmp(one, denied->index)) return SQLITE_DENY; + if (action == SQLITE_SAVEPOINT && denied->release && !strcmp(one, "RELEASE") && !strcmp(two, "media_registration")) { + denied->release = 0; return SQLITE_DENY; + } + return SQLITE_OK; +} + +static void transactions(void) { + const char* indices[] = {"block_id", "media_id", "chat_id", "node_id", "content_hash"}; + for (unsigned i = 0; i < sizeof(indices) / sizeof(*indices); i++) { + sqlite3* db = NULL; assert(sqlite3_open(":memory:", &db) == SQLITE_OK); + char name[80]; snprintf(name, sizeof(name), "idx_media_files_%s", indices[i]); + struct denied denied = {.index = name}; assert(!sqlite3_set_authorizer(db, authorizer, &denied)); + assert(media_index_init(db) == -1); + assert(!sqlite3_set_authorizer(db, NULL, NULL) && !media_index_init(db)); sqlite3_close(db); + } + char path[MEDIA_PATH_MAX]; assert(!media_index_path(root, "atomic", path)); + sqlite3* db = database(); + sql(db, "CREATE TABLE marker(n); BEGIN; INSERT INTO marker VALUES(42)"); + sql(db, "CREATE TRIGGER fail_second BEFORE INSERT ON media_files WHEN NEW.chunk=1 BEGIN SELECT RAISE(ABORT,'second'); END"); + assert(commit(db, "1", path) == -1 && scalar(db, "SELECT count(*) FROM media_files") == 0); + assert(!sqlite3_get_autocommit(db) && scalar(db, "SELECT n FROM marker") == 42); + assert(media_index_register_downloaded_file(db, &result, 1, "1", path, root) == -1); + assert(!scalar(db, "SELECT count(*) FROM media_files")); + sql(db, "DROP TRIGGER fail_second"); + assert(!commit(db, "1", path) && scalar(db, "SELECT count(*) FROM media_files") == 2); + assert(!sqlite3_get_autocommit(db)); sql(db, "ROLLBACK"); + assert(!scalar(db, "SELECT count(*) FROM media_files")); + struct denied denied = {.release = 1}; assert(!sqlite3_set_authorizer(db, authorizer, &denied)); + assert(commit(db, "1", path) == -1 && !scalar(db, "SELECT count(*) FROM media_files")); + assert(sqlite3_get_autocommit(db)); assert(!sqlite3_set_authorizer(db, NULL, NULL)); + sql(db, "CREATE TRIGGER rollback_all BEFORE INSERT ON media_files BEGIN SELECT RAISE(ROLLBACK,'all'); END; BEGIN"); + assert(commit(db, "1", path) == -1 && sqlite3_get_autocommit(db)); + sql(db, "DROP TRIGGER rollback_all"); + assert(!commit(db, "1", path) && !commit(db, "1", path)); + assert(!media_index_register_downloaded_file(db, &result, 1, "1", path, root)); + assert(scalar(db, "SELECT count(*) FROM media_files WHERE node_sign!=zeroblob(64)") == 2); + assert(media_index_register_downloaded(db, result.media_id, ids, result.content_hash, "conflict", "wrong", 1, 14, 7, 0, 0) == -1); + assert(scalar(db, "SELECT count(*) FROM media_files WHERE chat_id='1'") == 2); + result.num_blocks = INT_MAX; + assert(commit(db, "1", path) == -1); result.num_blocks = 2; + sqlite3_close(db); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression schema/atomicity/savepoint/duplicates PASS"); +} + +static void deletion(void) { + sqlite3* db = database(); char path[MEDIA_PATH_MAX]; assert(!media_index_path(root, "delete", path)); + write_file(path); assert(!commit(db, "1", path)); + sql(db, "BEGIN"); assert(media_index_delete_channel(db, "1", root) == -1 && ma_file_size(path) == 14); sql(db, "ROLLBACK"); + sql(db, "CREATE TRIGGER fail_delete BEFORE DELETE ON media_files BEGIN SELECT RAISE(ABORT,'delete failure'); END"); + assert(media_index_delete_channel(db, "1", root) == -1 && ma_file_size(path) == -1); + assert(scalar(db, "SELECT count(*) FROM media_files") == 2); + sql(db, "DROP TRIGGER fail_delete"); + assert(!media_index_delete_channel(db, "1", root) && !scalar(db, "SELECT count(*) FROM media_files")); + assert(!media_index_delete_channel(db, "1", root)); + write_file(path); assert(!commit(db, "1", path)); + uint8_t other[16] = {44}; + assert(!media_index_register_downloaded(db, result.media_id, other, result.content_hash, "2", "delete", 1, 14, 7, 0, 0)); + assert(!media_index_delete_channel(db, "1", root) && ma_file_size(path) == 14); + assert(!media_index_delete_channel(db, "2", root) && ma_file_size(path) == -1); + assert(!utun_mkdir(path, 0700)); assert(!commit(db, "1", path)); + assert(media_index_delete_channel(db, "1", root) == -1 && scalar(db, "SELECT count(*) FROM media_files") == 2); + assert(!test_rmdir(path)); assert(!media_index_delete_channel(db, "1", root)); + /* Подготовка могла создать файл и упасть до первого INSERT блока. */ + write_file(path); assert(!media_index_track_preparation(db, "1", path, root)); + assert(!media_index_delete_channel(db, "1", root) && ma_file_size(path) == -1); + assert(!scalar(db, "SELECT count(*) FROM media_preparations")); + sql(db, "DROP TABLE media_files"); assert(media_index_delete_channel(db, "1", root) == -1); + sqlite3_close(db); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression deletion retry/shared/unprepared files PASS"); +} + +static void long_path(void) { +#ifndef _WIN32 + char path[MEDIA_PATH_MAX]; snprintf(path, sizeof(path), "%s", root); + for (int i = 0; i < 3; i++) { + size_t n = strlen(path); path[n++] = '/'; memset(path + n, 'a' + i, 180); path[n + 180] = 0; + assert(!utun_mkdir(path, 0700)); + } + strcat(path, "/payload"); write_file(path); sqlite3* db = database(); + assert(!commit(db, "1", path)); + sqlite3_stmt* st = NULL; assert(!sqlite3_prepare_v2(db, "SELECT location FROM media_files LIMIT 1", -1, &st, NULL)); + assert(sqlite3_step(st) == SQLITE_ROW); const char* location = (const char*)sqlite3_column_text(st, 0); + char restored[MEDIA_PATH_MAX]; + assert(strlen(location) == 550 && !media_index_path(root, location, restored) && !strcmp(restored, path)); + assert(ma_file_size(restored) == 14); sqlite3_finalize(st); + assert(!media_index_delete_channel(db, "1", root) && ma_file_size(path) == -1); sqlite3_close(db); +#endif + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression long location PASS"); +} + +static int deleted_events, failed_submissions, successful_submissions; +static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int length) { + (void)inst; + if (type == CHAT_EVT_CHANNEL_DELETED) deleted_events++; + if (type == CHAT_EVT_MSG_SUBMITTED) { + assert(length == 9); + if (data[8]) successful_submissions++; else failed_submissions++; + } +} + +static void pending_deletion(void) { + char config[1024]; snprintf(config, sizeof(config), "[global]\ndb_path=%s\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", root); + struct UASYNC* ua = uasync_create(); assert(ua); + struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst); + assert(!utun_core_start(inst) && !chat_service_start(inst)); chat_event_set_handler(inst, event); + chat_core_create_channel_auto(inst, "pending"); + sqlite3_stmt* st = NULL; sqlite3* db = inst->topo_sqlite_db; + assert(!sqlite3_prepare_v2(db, "SELECT channel_id FROM channels WHERE name='pending'", -1, &st, NULL)); + assert(sqlite3_step(st) == SQLITE_ROW); char channel[64]; snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0)); + sqlite3_finalize(st); + char source[MEDIA_PATH_MAX], destinations[2][MEDIA_PATH_MAX]; + assert(!media_index_path(root, "source", source)); write_file(source); + struct chat_msg_submit req = {.inst = inst, .media_copy = 1, .data = (uint8_t*)"payload", .data_len = 7}; + snprintf(req.channel_id, sizeof(req.channel_id), "%s", channel); + for (int i = 0; i < 2; i++) { + char relative[32]; snprintf(relative, sizeof(relative), "outgoing%d", i); + assert(!media_index_path(root, relative, destinations[i])); + snprintf(req.media_src, sizeof(req.media_src), "%s", source); + snprintf(req.media_dest, sizeof(req.media_dest), "%s", destinations[i]); + req.request_id = i + 1; chat_core_submit_media_message(inst, &req); + } + assert(CC(inst)->media_submissions && CC(inst)->media_submissions->next); /* completions не обработаны */ + chat_core_delete_channel(inst, channel); chat_core_delete_channel(inst, channel); + assert(!deleted_events && scalar(db, "SELECT count(*) FROM channels WHERE name='pending'") == 1); + req.request_id = 3; chat_core_submit_media_message(inst, &req); assert(failed_submissions == 1); + for (int i = 0; i < 2000 && !deleted_events; i++) uasync_poll(ua, 100); + assert(deleted_events == 1 && failed_submissions == 3 && !successful_submissions && !CC(inst)->media_submissions); + assert(!scalar(db, "SELECT count(*) FROM channels WHERE name='pending'")); + assert(!scalar(db, "SELECT count(*) FROM media_files") && !scalar(db, "SELECT count(*) FROM media_preparations")); + assert(ma_file_size(source) == 14 && ma_file_size(destinations[0]) == -1 && ma_file_size(destinations[1]) == -1); + chat_core_ensure_channel_ready(inst, channel); uasync_poll(ua, 0); + assert(!scalar(db, "SELECT count(*) FROM media_files")); + utun_instance_destroy(inst); uasync_destroy(ua, 0); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media regression pending deletion/two completions/recreation PASS"); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO); + assert(!test_mkdtemp(root)); ids[16] = 2; + paths(); transactions(); deletion(); long_path(); pending_deletion(); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "test_media_index_regressions PASS"); return 0; +}