From 484d6f2fa4869a0fcc43750b908700688cd3fa80 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 8 Oct 2026 00:31:13 +0200 Subject: [PATCH] Test signed replies, media completion and old-history lookup --- tools/chatgui/CMakeLists.txt | 6 + tools/chatgui/tests/test_channel_replies.cpp | 123 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 tools/chatgui/tests/test_channel_replies.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 4bed0cfc..440a265e 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -344,6 +344,12 @@ target_link_libraries(test_file_drop PRIVATE ${QT_LIBS} ${QT_TEST_LIB} rlottie:: add_test(NAME test_file_drop COMMAND test_file_drop) set_tests_properties(test_file_drop PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) +add_executable(test_channel_replies tests/test_channel_replies.cpp db/db_manager.cpp) +target_include_directories(test_channel_replies PRIVATE ${UTUN_INCLUDE_DIRS}) +target_link_libraries(test_channel_replies PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS}) +add_test(NAME test_channel_replies COMMAND test_channel_replies) +set_tests_properties(test_channel_replies PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) + add_executable(test_member_files_browser tests/test_member_files_browser.cpp src/member_files/browser.cpp db/db_manager.cpp) target_link_libraries(test_member_files_browser PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread) add_test(NAME test_member_files_browser COMMAND test_member_files_browser) diff --git a/tools/chatgui/tests/test_channel_replies.cpp b/tools/chatgui/tests/test_channel_replies.cpp new file mode 100644 index 00000000..a7f08244 --- /dev/null +++ b/tools/chatgui/tests/test_channel_replies.cpp @@ -0,0 +1,123 @@ +/* Настоящее ядро и SQLite: подписанные ответы, сохранение, медиа и окно старой истории. */ +#include "../db/db_manager.h" +#include +#include +extern "C" { +#include "utun_instance.h" +#include "chat/chat_core.h" +#include "chat/chat_event.h" +#include "chat/db_sync.h" +#include "media_async/attachment_send.h" +#include "mem.h" +#include "debug_config.h" +} + +static QHash results; +static void submitted(UTUN_INSTANCE*, int type, const uint8_t* data, int len) { + if (type != CHAT_EVT_MSG_SUBMITTED || len != 9) return; + quint64 id = 0; for (unsigned i = 0; i < 8; ++i) id |= quint64(data[i]) << (8 * i); + results.insert(id, data[8] != 0); +} + +class ChannelRepliesTest : public QObject { + Q_OBJECT + QTemporaryDir directory; + UASYNC* ua = nullptr; + UTUN_INSTANCE* inst = nullptr; + DbManager db; + QString channel; + QByteArray original; + + QByteArray send(const QByteArray& text, const QByteArray& reply = {}) { + chat_msg_submit request{}; request.inst = inst; + snprintf(request.channel_id, sizeof(request.channel_id), "%s", qPrintable(channel)); + strcpy(request.content_type, "text/plain"); request.data = (uint8_t*)text.constData(); request.data_len = text.size(); + if (reply.size() == 32) memcpy(request.reply_to, reply.constData(), 32); + if (chat_core_submit_message(inst, &request) != 0) return {}; + return QByteArray(reinterpret_cast(request.message_id), 32); + } + +private slots: + void initTestCase() { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + QVERIFY(directory.isValid()); ua = uasync_create(); QVERIFY(ua); + const QByteArray config = "[global]\ndb_path=" + directory.path().toUtf8() + + "\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"; + inst = utun_instance_create_from_str(ua, config.constData()); QVERIFY(inst); + QCOMPARE(utun_core_start(inst), 0); QCOMPARE(chat_service_start(inst), 0); + chat_event_set_handler(inst, submitted); + chat_core_create_channel_auto(inst, "Reply tests"); + db.setDb(inst->topo_sqlite_db); db.setMyNodeId(inst->node_id); + const auto channels = db.getChannels(); QCOMPARE(channels.size(), 1); channel = channels.first().channelId; + original = send("Original with Unicode: Привет \"world\"\nnext line"); QCOMPARE(original.size(), 32); + } + + void signedReferenceAndRestart() { + const auto reply = send("Answer", original); QCOMPARE(reply.size(), 32); + QCOMPARE(db.getMessageByKey(channel, reply).replyTo, original); + const auto sql = QString("SELECT timestamp,data,author_signature FROM msg_%1 WHERE message_id=?").arg(channel).toUtf8(); + sqlite3_stmt* statement = nullptr; + QCOMPARE(sqlite3_prepare_v2(inst->topo_sqlite_db, sql.constData(), -1, &statement, nullptr), SQLITE_OK); + sqlite3_bind_blob(statement, 1, reply.constData(), reply.size(), SQLITE_STATIC); + QCOMPARE(sqlite3_step(statement), SQLITE_ROW); + uint64_t ts = sqlite3_column_int64(statement, 0); + QByteArray body(reinterpret_cast(&ts), 8); body += DbManager::colBlob(statement, 1); + QByteArray signature = DbManager::colBlob(statement, 2); sqlite3_finalize(statement); + QCOMPARE(sc_ed25519_verify(inst->my_ed25519_pubkey, (uint8_t*)body.data(), body.size(), (uint8_t*)signature.data()), SC_OK); + const int reference = body.indexOf(original.toHex()); QVERIFY(reference >= 0); body[reference] = body[reference] == 'a' ? 'b' : 'a'; + QVERIFY(sc_ed25519_verify(inst->my_ed25519_pubkey, (uint8_t*)body.data(), body.size(), (uint8_t*)signature.data()) != SC_OK); + chat_service_stop(inst); QCOMPARE(chat_service_start(inst), 0); + QCOMPARE(db.getMessageByKey(channel, reply).replyTo, original); + sqlite3* reopened = nullptr; + QCOMPARE(sqlite3_open_v2(qPrintable(directory.filePath("chats.db")), &reopened, SQLITE_OPEN_READONLY, nullptr), SQLITE_OK); + DbManager reader; reader.setDb(reopened); QCOMPARE(reader.getMessageByKey(channel, reply).replyTo, original); + reader.setDb(nullptr); QCOMPARE(sqlite3_close(reopened), SQLITE_OK); + } + + void unavailableAndForeignReferencesFail() { + QVERIFY(send("missing", QByteArray(32, 'z')).isEmpty()); + const QString saved = channel; chat_core_create_channel_auto(inst, "Other channel"); + for (const auto& row : db.getChannels()) if (row.channelId != saved) channel = row.channelId; + QVERIFY(channel != saved); QVERIFY(send("wrong channel", original).isEmpty()); channel = saved; + auto* request = (chat_msg_submit*)u_calloc(1, sizeof(chat_msg_submit)); QVERIFY(request); + request->inst = inst; request->request_id = 101; strcpy(request->channel_id, "0"); strcpy(request->content_type, "text/plain"); + chat_core_submit_trampoline(request); QVERIFY(results.contains(101)); QVERIFY(!results.value(101)); + } + + void mediaKeepsReferenceThroughWorker() { + auto* request = (attachment_send_req*)u_calloc(1, sizeof(attachment_send_req)); QVERIFY(request); + request->inst = inst; request->request_id = 102; request->info.kind = ATTACHMENT_VOICE; request->preset = 1; + snprintf(request->target, sizeof(request->target), "%s", qPrintable(channel)); memcpy(request->reply_to, original.constData(), 32); + request->pcm = (int16_t*)u_calloc(48000, sizeof(int16_t)); QVERIFY(request->pcm); request->pcm_count = 48000; + attachment_send_trampoline(request); + QElapsedTimer timer; timer.start(); while (!results.contains(102) && timer.elapsed() < 10000) uasync_poll(ua, 20); + QVERIFY(results.contains(102)); QVERIFY(results.value(102)); + const auto messages = db.getMessages(channel, 1); QCOMPARE(messages.size(), 1); + QCOMPARE(messages.first().contentType, QString("audio/opus")); QCOMPARE(messages.first().replyTo, original); + // Регистрация файла завершена до успешного подтверждения. + QVERIFY(messages.first().localAttrs.contains("\"st\":\"fl\"")); + } + + void historyWindowAndDelayedOriginal() { + for (int i = 0; i < 420; ++i) QCOMPARE(send(QByteArray::number(i)).size(), 32); + const auto latest = db.getMessages(channel, 200); QCOMPARE(latest.size(), 200); + for (const auto& row : latest) QVERIFY(row.messageId != original); + const auto window = db.getMessages(channel, 200, original); QCOMPARE(window.size(), 200); + bool found = false; for (const auto& row : window) found |= row.messageId == original; QVERIFY(found); + const auto answer = send("Answer to old message", original); QCOMPARE(answer.size(), 32); + const auto sql = QString("CREATE TEMP TABLE saved_original AS SELECT * FROM msg_%1 WHERE message_id=x'%2';" + "DELETE FROM msg_%1 WHERE message_id=x'%2'").arg(channel, QString::fromLatin1(original.toHex())).toUtf8(); + QCOMPARE(sqlite3_exec(inst->topo_sqlite_db, sql.constData(), nullptr, nullptr, nullptr), SQLITE_OK); + QCOMPARE(db.getMessageByKey(channel, answer).replyTo, original); QVERIFY(db.getMessageByKey(channel, original).messageId.isEmpty()); + const auto restore = QString("INSERT INTO msg_%1 SELECT * FROM saved_original").arg(channel).toUtf8(); + QCOMPARE(sqlite3_exec(inst->topo_sqlite_db, restore.constData(), nullptr, nullptr, nullptr), SQLITE_OK); + QCOMPARE(db.getMessageByKey(channel, original).messageId, original); + } + + void cleanupTestCase() { + db.setDb(nullptr); if (inst) utun_instance_destroy(inst); if (ua) uasync_destroy(ua, 0); + } +}; + +QTEST_MAIN(ChannelRepliesTest) +#include "test_channel_replies.moc"