Files
utun2/tools/chatgui/tests/test_dm_deletion.cpp
T

196 lines
11 KiB
C++

/* Настоящие 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 <QtTest>
#include <QTemporaryDir>
#include <QFile>
#include <QTextEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QMenu>
#include <QScopeGuard>
#include <future>
#include <memory>
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<QByteArray> snapshots;
/* Все изменения ядра/БД идут в его потоке; GUI не нужен для завершения этой задачи. */
static int inCore(std::function<int(UTUN_INSTANCE*)> operation) {
auto* task = new std::packaged_task<int()>([operation] { return operation(gui_bridge_get_inst()); });
auto result = task->get_future();
gui_bridge_post_uasync_fn([](void* arg) {
std::unique_ptr<std::packaged_task<int()>> work(static_cast<std::packaged_task<int()>*>(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<QMessageBox*>(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<QMessageBox*>(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<ChannelList*>(); QVERIFY(channels);
auto* history = window.findChild<MessageList*>(); QVERIFY(history);
auto* view = history->findChild<ChatView*>(); QVERIFY(view);
auto* input = history->findChild<InputBar*>(); QVERIFY(input);
auto* editor = input->findChild<QTextEdit*>(); QVERIFY(editor);
historyView = history;
gui_bridge_set_dm_messages_cb([](const uint8_t* data, int len) {
snapshots.append(QByteArray(reinterpret_cast<const char*>(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<const uint8_t*>("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<QListView*>(); 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<QMenu*>(QApplication::activePopupWidget());
if (!menu) qFatal("DM test: PM context menu missing");
auto* action = menu->findChild<QAction*>("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<const uint8_t*>(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<const uint8_t*>(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"