/* Настоящие MainWindow/uasync/SQLite: подтверждение, rollback, скрытие PM и старые поколения GUI. */ #include "../src/mainwindow.h" #include "../src/channellist.h" #include "../src/channeldelegate.h" #include "../src/messagelist.h" #include "../src/messagedelegate.h" #include "../src/chatview.h" #include "../src/inputbar.h" #include "../db/db_manager.h" #include "../transport/utun_node.h" #include "../transport/gui_bridge.h" #include #include #include #include #include #include #include #include #include #include extern "C" { #include "utun_instance.h" #include "dm/dm_core.h" #include "dm/dm_history.h" #include "debug_config.h" } static MessageList* historyView; static QList snapshots; /* Все изменения ядра/БД идут в его потоке; GUI не нужен для завершения этой задачи. */ static int inCore(std::function operation) { auto* task = new std::packaged_task([operation] { return operation(gui_bridge_get_inst()); }); auto result = task->get_future(); gui_bridge_post_uasync_fn([](void* arg) { std::unique_ptr> work(static_cast*>(arg)); (*work)(); }, task); if (result.wait_for(std::chrono::seconds(5)) != std::future_status::ready) qFatal("DM test: core task timed out"); return result.get(); } static void answerConfirmation(bool remove) { QTimer::singleShot(0, [remove] { auto* box = qobject_cast(QApplication::activeModalWidget()); if (!box) qFatal("DM test: confirmation missing"); for (auto* button : box->buttons()) if (button->text() == (remove ? "Delete" : "Cancel")) { button->click(); return; } qFatal("DM test: confirmation button missing"); }); } static void dismissError() { auto* box = qobject_cast(QApplication::activeModalWidget()); QVERIFY(box); QCOMPARE(box->text(), QString("Could not delete the conversation. Please try again.")); box->accept(); } class DmDeletionTest : public QObject { Q_OBJECT private slots: void initTestCase() { debug_config_init(); gui_bridge_init(); } void deleteThroughWindowAndCore() { QTemporaryDir directory; QVERIFY(directory.isValid()); directory.setAutoRemove(false); auto diagnostics = qScopeGuard([&directory] { directory.setAutoRemove(!QTest::currentTestFailed()); if (QTest::currentTestFailed()) DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "DM test diagnostics retained: %s", qPrintable(directory.path())); }); const QString configPath = directory.filePath("node.cfg"); QFile config(configPath); QVERIFY(config.open(QIODevice::WriteOnly)); const QByteArray text = "[global]\n" "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" "tun_enabled=no\n[server:local]\naddr=127.0.0.1:0\ntype=private\n" "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\nlocal_invites_enabled=0\nlocal_broadcasts_enabled=0\n" "[ntp]\nenabled=no\n"; QCOMPARE(config.write(text), text.size()); config.close(); DbManager db; MainWindow window(nullptr, &db, configPath, directory.filePath("db"), directory.filePath("dm.log"), "warn", "dm=debug,general=info"); window.show(); QVERIFY2(window.utunNode()->isRunning(), qPrintable(window.utunNode()->startupError())); QTRY_VERIFY_WITH_TIMEOUT(db.isOpen(), 3000); auto* channels = window.findChild(); QVERIFY(channels); auto* history = window.findChild(); QVERIFY(history); auto* view = history->findChild(); QVERIFY(view); auto* input = history->findChild(); QVERIFY(input); auto* editor = input->findChild(); QVERIFY(editor); historyView = history; gui_bridge_set_dm_messages_cb([](const uint8_t* data, int len) { snapshots.append(QByteArray(reinterpret_cast(data), len)); historyView->onDmMessages(data, len); }); SC_MYKEYS peer{}; uint8_t ed[32]; QCOMPARE(sc_generate_keypair(&peer), SC_OK); QCOMPARE(sc_derive_ed25519_pubkey(peer.private_key, ed), SC_OK); const quint64 peerId = sc_derive_node_id_from_pubkey(peer.public_key); const auto startPeer = [peer, ed, peerId](UTUN_INSTANCE* inst) { return dm_start(inst, peerId, peer.public_key, ed, "Alice", nullptr); }; QCOMPARE(inCore(startPeer), 0); QTRY_COMPARE_WITH_TIMEOUT(db.getDmConversations().size(), 1, 3000); const QString conv = db.getDmConversations().first().convId; QCOMPARE(db.getDmConversations().first().generation, quint64(1)); const auto send = [conv](UTUN_INSTANCE* inst) { return dm_send(inst, qPrintable(conv), "text", reinterpret_cast("one"), 3); }; QCOMPARE(inCore(send), 0); QTRY_COMPARE_WITH_TIMEOUT(view->model()->rowCount(), 1, 3000); QVERIFY(input->isEnabled()); QVERIFY(!snapshots.isEmpty()); const QByteArray oldHistory = snapshots.last(); editor->setPlainText("keep draft on cancellation"); answerConfirmation(false); window.onDmDeleteRequested(conv, 1, "Alice"); QVERIFY(input->isEnabled()); QCOMPARE(editor->toPlainText(), QString("keep draft on cancellation")); QCOMPARE(db.getDmConversations().size(), 1); QCOMPARE(inCore([](UTUN_INSTANCE* inst) { return sqlite3_exec(inst->topo_sqlite_db, "CREATE TEMP TRIGGER refuse_delete BEFORE DELETE ON dm_messages" " BEGIN SELECT RAISE(ABORT,'injected UI deletion failure'); END", nullptr, nullptr, nullptr); }), SQLITE_OK); answerConfirmation(true); window.onDmDeleteRequested(conv, 1, "Alice"); QVERIFY(!input->isEnabled()); QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() != nullptr, 3000); dismissError(); QVERIFY(input->isEnabled()); QCOMPARE(db.getDmConversations().first().generation, quint64(1)); QCOMPARE(editor->toPlainText(), QString("keep draft on cancellation")); QCOMPARE(inCore([](UTUN_INSTANCE* inst) { return sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER refuse_delete", nullptr, nullptr, nullptr); }), SQLITE_OK); // ПКМ запускает тот же диалог; после подтверждения отправки заблокированы до результата. auto* list = channels->findChild(); QVERIFY(list); QModelIndex dmIndex; for (int row = 0; row < list->model()->rowCount(); ++row) { auto index = list->model()->index(row, 0); if (index.data(ChannelChannelIdRole).toString() == "dm:" + conv) dmIndex = index; } QVERIFY(dmIndex.isValid()); QCOMPARE(dmIndex.data(ChannelDmGenerationRole).toULongLong(), quint64(1)); QSignalSpy requested(channels, &ChannelList::dmDeleteRequested); QTimer::singleShot(0, [] { auto* menu = qobject_cast(QApplication::activePopupWidget()); if (!menu) qFatal("DM test: PM context menu missing"); auto* action = menu->findChild("deleteDmAction"); if (!action || !action->isEnabled()) qFatal("DM test: delete action unavailable"); menu->setActiveAction(action); answerConfirmation(true); QTest::keyClick(menu, Qt::Key_Return); }); QMetaObject::invokeMethod(list, "customContextMenuRequested", Qt::DirectConnection, Q_ARG(QPoint, list->visualRect(dmIndex).center())); QCOMPARE(requested.size(), 1); QVERIFY(!input->isEnabled()); QTRY_VERIFY_WITH_TIMEOUT(db.getDmConversations().isEmpty(), 3000); QTRY_COMPARE_WITH_TIMEOUT(view->model()->rowCount(), 0, 3000); QVERIFY(editor->toPlainText().isEmpty()); QVERIFY(!input->isEnabled()); history->onDmMessages(reinterpret_cast(oldHistory.constData()), oldHistory.size()); QCOMPARE(view->model()->rowCount(), 0); QCOMPARE(inCore(startPeer), 0); QCOMPARE(inCore(send), 0); QTRY_COMPARE_WITH_TIMEOUT(db.getDmConversations().size(), 1, 3000); QCOMPARE(db.getDmConversations().first().generation, quint64(2)); QTRY_COMPARE_WITH_TIMEOUT(view->model()->rowCount(), 1, 3000); QVERIFY(input->isEnabled()); editor->setPlainText("new generation draft"); history->onDmMessages(reinterpret_cast(oldHistory.constData()), oldHistory.size()); QCOMPARE(view->model()->rowCount(), 1); QCOMPARE(view->model()->index(0, 0).data(MsgIdRole).toLongLong(), qint64(2)); answerConfirmation(true); window.onDmDeleteRequested(conv, 1, "Alice"); QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() != nullptr, 3000); dismissError(); QCOMPARE(db.getDmConversations().first().generation, quint64(2)); QCOMPARE(editor->toPlainText(), QString("new generation draft")); QVERIFY(input->isEnabled()); // Удаление другой беседы сохраняет выбранный PM и его черновик. SC_MYKEYS other{}; uint8_t otherEd[32]; QCOMPARE(sc_generate_keypair(&other), SC_OK); QCOMPARE(sc_derive_ed25519_pubkey(other.private_key, otherEd), SC_OK); const quint64 otherId = sc_derive_node_id_from_pubkey(other.public_key); QCOMPARE(inCore([other, otherEd, otherId](UTUN_INSTANCE* inst) { return dm_start(inst, otherId, other.public_key, otherEd, "Bob", nullptr); }), 0); QTRY_COMPARE_WITH_TIMEOUT(db.getDmConversations().size(), 2, 3000); QCoreApplication::processEvents(); channels->selectChannel("dm:" + conv); QTRY_VERIFY(input->isEnabled()); editor->setPlainText("selected PM stays"); QString otherConv; for (const auto& dm : db.getDmConversations()) if (dm.convId != conv) otherConv = dm.convId; QVERIFY(!otherConv.isEmpty()); answerConfirmation(true); window.onDmDeleteRequested(otherConv, 1, "Bob"); QVERIFY(input->isEnabled()); QTRY_COMPARE_WITH_TIMEOUT(db.getDmConversations().size(), 1, 3000); QCoreApplication::processEvents(); QCOMPARE(editor->toPlainText(), QString("selected PM stays")); QCOMPARE(list->currentIndex().data(ChannelChannelIdRole).toString(), "dm:" + conv); QVERIFY(input->isEnabled()); DEBUG_INFO(DEBUG_CATEGORY_DM, "DM desktop: cancel, SQL rollback, context menu, generations and unselected deletion PASS"); } }; QTEST_MAIN(DmDeletionTest) #include "test_dm_deletion.moc"