196 lines
11 KiB
C++
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"
|