/* Настоящие Qt-диалоги/SQLite/bridge: отказы очереди и владение вложенными окнами. */ #include "../src/channelsettingsdialog.h" #include "../src/avatars/groupavatarmanager.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" #include #include #include #include #include #include #include extern "C" { #include "utun_instance.h" #include "chat/chat_core.h" #include "mem.h" #include "debug_config.h" } #ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES static int failAllocation; static const char* allocationFile; static bool rejectAllocation(const char* where) { if (!failAllocation || !strstr(where, allocationFile) || --failAllocation) return false; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "[INJECT] channel settings allocation failure at %s", where); return true; } extern "C" void* __real_u_malloc_impl(uint32_t, const char*); extern "C" void* __wrap_u_malloc_impl(uint32_t bytes, const char* where) { return rejectAllocation(where) ? nullptr : __real_u_malloc_impl(bytes, where); } extern "C" void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); extern "C" void* __wrap_u_calloc_impl(uint32_t count, uint32_t bytes, const char* where) { return rejectAllocation(where) ? nullptr : __real_u_calloc_impl(count, bytes, where); } #endif struct SettingsCore { UASYNC* ua = uasync_create(); UTUN_INSTANCE* inst = nullptr; DbManager db; QString channel; ~SettingsCore() { gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); if (ua) uasync_mark_stopped(ua); db.setDb(nullptr); if (inst) utun_instance_destroy(inst); if (ua) uasync_destroy(ua, 0); } bool start(bool attach = true) { if (!ua) return false; const char* config = "[global]\n" "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" "[chatserver]\ngroup_autoconnect=0\nmember_files_enabled=0\nstorage_autoload=0\n"; inst = utun_instance_create_from_str(ua, config); if (!inst || utun_core_start(inst) || chat_service_start(inst)) return false; chat_core_create_channel_auto(inst, "Settings"); db.setDb(inst->topo_sqlite_db); db.setMyNodeId(inst->node_id); if (db.getChannels().size() != 1) return false; channel = db.getChannels()[0].channelId; if (attach) { gui_bridge_set_uasync(ua); gui_bridge_set_inst(inst); } return true; } }; class ChannelSettingsTest : public QObject { Q_OBJECT private slots: void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); gui_bridge_init(); QApplication::setAttribute(Qt::AA_DontUseNativeDialogs); } void pendingSaveIsOwned() { SettingsCore core; QVERIFY(core.start()); ChannelSettingsDialog dialog(core.channel, &core.db); const auto baseline = u_get_allocated_count(); QSignalSpy accepted(&dialog, &QDialog::accepted); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QCOMPARE(accepted.count(), 1); gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline); } void savesBothValues() { SettingsCore core; QVERIFY(core.start()); ChannelSettingsDialog dialog(core.channel, &core.db); const auto checks = dialog.findChildren(); QCOMPARE(checks.size(), 2); QVERIFY(!checks[0]->isChecked()); QVERIFY(checks[1]->isChecked()); checks[0]->setChecked(true); checks[1]->setChecked(false); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); uasync_poll(core.ua, 0); QCOMPARE(core.db.getUiStateInt("ch_" + core.channel + "_auto_play_voice", -1), 1); QCOMPARE(core.db.getUiStateInt("ch_" + core.channel + "_sound_on_message", -1), 0); } void stoppedCoreKeepsDialogOpen() { SettingsCore core; QVERIFY(core.start(false)); ChannelSettingsDialog dialog(core.channel, &core.db); QSignalSpy accepted(&dialog, &QDialog::accepted); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QVERIFY(accepted.isEmpty()); auto* status = dialog.findChild("channelSettingsStatus"); QVERIFY(status && !status->text().isEmpty()); } #ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES void allocationFailure_data() { QTest::addColumn("source"); QTest::addColumn("position"); for (const auto* source : {"channelsettingsdialog.cpp", "gui_bridge_impl.cpp"}) for (int position : {1, 2}) QTest::newRow(qPrintable(QString("%1-%2").arg(source).arg(position))) << QByteArray(source) << position; } void allocationFailure() { QFETCH(QByteArray, source); QFETCH(int, position); SettingsCore core; QVERIFY(core.start()); ChannelSettingsDialog dialog(core.channel, &core.db); QSignalSpy accepted(&dialog, &QDialog::accepted); const auto baseline = u_get_allocated_count(); allocationFile = source.constData(); failAllocation = position; auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; }); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QCOMPARE(failAllocation, 0); QVERIFY(accepted.isEmpty()); gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline); auto* status = dialog.findChild("channelSettingsStatus"); QVERIFY(status && !status->text().isEmpty()); } #endif void invalidChannel_data() { QTest::addColumn("channel"); for (const auto* value : {"", "0", "01", "+1", "1 ", "dm:1", "18446744073709551616"}) QTest::newRow(value) << QString::fromLatin1(value); QTest::newRow("embedded-nul") << QString::fromLatin1("1\0tail", 6); } void invalidChannel() { QFETCH(QString, channel); SettingsCore core; QVERIFY(core.start()); ChannelSettingsDialog dialog(channel, &core.db); QSignalSpy accepted(&dialog, &QDialog::accepted); QVERIFY(!dialog.findChild("deleteChannel")->isEnabled()); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QVERIFY(accepted.isEmpty()); } void borrowedDbDestruction() { SettingsCore core; QVERIFY(core.start()); auto* db = new DbManager; db->setDb(core.inst->topo_sqlite_db); ChannelSettingsDialog dialog(core.channel, db); QSignalSpy rejected(&dialog, &QDialog::rejected); delete db; QCOMPARE(rejected.count(), 1); QSignalSpy accepted(&dialog, &QDialog::accepted); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QVERIFY(accepted.isEmpty()); } void confirmationSurvivesOwnerDeletion() { SettingsCore core; QVERIFY(core.start()); QPointer dialog = new ChannelSettingsDialog(core.channel, &core.db); dialog->show(); QTimer::singleShot(0, [&] { QVERIFY(qobject_cast(QApplication::activeModalWidget())); delete dialog.data(); }); QVERIFY(QMetaObject::invokeMethod(dialog.data(), "onDelete", Qt::DirectConnection)); QVERIFY(!dialog); QCOMPARE(core.db.getChannels().size(), 1); } void avatarChooserSurvivesOwnerDeletion() { SettingsCore core; QVERIFY(core.start()); GroupAvatarManager avatars(&core.db); QPointer dialog = new ChannelSettingsDialog(core.channel, &core.db); dialog->show(); auto* upload = dialog->findChild("uploadGroupAvatar"); QVERIFY(upload && upload->isEnabled()); QTimer::singleShot(0, [&] { QVERIFY(qobject_cast(QApplication::activeModalWidget())); delete dialog.data(); }); upload->click(); QVERIFY(!dialog); } }; QTEST_MAIN(ChannelSettingsTest) #include "test_channel_settings.moc"