/* Настоящие 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 #include extern "C" { #include "utun_instance.h" #include "chat/chat_core.h" #include "chat/chat_event.h" #include "chat/group_avatar.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 static QByteArray avatarResult; 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()); } void retryAfterPartialQueueAcceptance() { SettingsCore core; QVERIFY(core.start()); ChannelSettingsDialog dialog(core.channel, &core.db); const auto checks = dialog.findChildren(); QCOMPARE(checks.size(), 2); checks[0]->setChecked(true); checks[1]->setChecked(false); QSignalSpy accepted(&dialog, &QDialog::accepted); allocationFile = "gui_bridge_impl.cpp"; failAllocation = 2; auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; }); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QCOMPARE(failAllocation, 0); QVERIFY(accepted.isEmpty()); uasync_poll(core.ua, 0); QCOMPARE(core.db.getUiStateInt("ch_" + core.channel + "_auto_play_voice", -1), 1); QCOMPARE(chat_core_get_sound_on_message(core.inst, qPrintable(core.channel)), 1); QVERIFY(QMetaObject::invokeMethod(&dialog, "onSave", Qt::DirectConnection)); QCOMPARE(accepted.count(), 1); uasync_poll(core.ua, 0); QCOMPARE(core.db.getUiStateInt("ch_" + core.channel + "_auto_play_voice", -1), 1); QCOMPARE(chat_core_get_sound_on_message(core.inst, qPrintable(core.channel)), 0); } #endif void invalidChannel_data() { QTest::addColumn("channel"); for (const auto* value : {"", "0", "01", "+1", "1 ", "dm:1", "9223372036854775808", "18446744073709551615", "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 confirmationOutcome_data() { QTest::addColumn("change"); QTest::newRow("confirm-owned-request") << 0; QTest::newRow("cancel") << 1; QTest::newRow("removed-during-confirmation") << 2; QTest::newRow("core-stopped") << 3; #ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES QTest::newRow("queue-rejected") << 4; #endif } void confirmationOutcome() { QFETCH(int, change); SettingsCore core; QVERIFY(core.start()); ChannelSettingsDialog dialog(core.channel, &core.db); dialog.show(); QSignalSpy accepted(&dialog, &QDialog::accepted); const auto baseline = u_get_allocated_count(); #ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; }); #endif QTimer::singleShot(0, &dialog, [&] { auto* box = qobject_cast(QApplication::activeModalWidget()); QVERIFY(box); if (change == 1) { box->button(QMessageBox::Cancel)->click(); return; } if (change == 2) chat_core_delete_channel(core.inst, qPrintable(core.channel)); if (change == 3) gui_bridge_set_inst(nullptr); #ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES if (change == 4) { allocationFile = "gui_bridge_impl.cpp"; failAllocation = 1; } #endif auto* button = box->findChild("confirmDeleteChannel"); QVERIFY(button); button->click(); }); QVERIFY(QMetaObject::invokeMethod(&dialog, "onDelete", Qt::DirectConnection)); QCOMPARE(accepted.count(), int(change == 0)); gui_bridge_set_inst(nullptr); if (change != 2) QCOMPARE(u_get_allocated_count(), baseline); QCOMPARE(core.db.getChannels().size(), change == 2 ? 0 : 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); } void avatarManagerDisappearsDuringChoice() { SettingsCore core; QVERIFY(core.start()); QPointer avatars = new GroupAvatarManager(&core.db); auto release = qScopeGuard([&] { delete avatars.data(); }); ChannelSettingsDialog dialog(core.channel, &core.db); dialog.show(); auto* upload = dialog.findChild("uploadGroupAvatar"); QVERIFY(upload && upload->isEnabled()); QTimer::singleShot(0, [&] { QVERIFY(qobject_cast(QApplication::activeModalWidget())); delete avatars.data(); }); upload->click(); QVERIFY(!avatars); QVERIFY(!upload->isEnabled()); QVERIFY(!dialog.findChild("removeGroupAvatar")->isEnabled()); } void avatarPublicationIsOwned() { SettingsCore core; QVERIFY(core.start()); GroupAvatarManager avatars(&core.db); const auto baseline = u_get_allocated_count(); avatars.remove(core.channel); QVERIFY(u_get_allocated_count() > baseline); gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline); } void invalidAvatarReplyKeepsPendingRequest() { SettingsCore core; QVERIFY(core.start()); GroupAvatarManager avatars(&core.db); QSignalSpy statuses(&avatars, &GroupAvatarManager::status); avatarResult.clear(); chat_event_set_handler(core.inst, [](UTUN_INSTANCE*, int type, const uint8_t* data, int length) { if (type == CHAT_EVT_GROUP_AVATAR_RESULT) avatarResult = QByteArray((const char*)data, length); }); avatars.remove(core.channel); QCOMPARE(statuses.count(), 1); uasync_poll(core.ua, 0); QCOMPARE(avatarResult.size(), 17); QByteArray wrong = avatarResult; wrong[8] = char(uint8_t(wrong[8]) ^ 1); avatars.onResult((const uint8_t*)wrong.constData(), wrong.size()); QCOMPARE(statuses.count(), 1); wrong = avatarResult; wrong[16] = 2; avatars.onResult((const uint8_t*)wrong.constData(), wrong.size()); QCOMPARE(statuses.count(), 1); avatars.onResult((const uint8_t*)avatarResult.constData(), avatarResult.size()); QCOMPARE(statuses.count(), 2); QCOMPARE(statuses.last()[1].toString(), QString("Group avatar updated")); avatars.onResult((const uint8_t*)avatarResult.constData(), avatarResult.size()); QCOMPARE(statuses.count(), 2); } void avatarNullReplyAndBorrowedDb() { SettingsCore core; QVERIFY(core.start()); auto* db = new DbManager; db->setDb(core.inst->topo_sqlite_db); GroupAvatarManager avatars(db); delete db; avatars.onResult(nullptr, 17); avatars.onResult(nullptr, 0); QVERIFY(avatars.image(core.channel).isNull()); } void preparingSignalCanDeleteManager() { SettingsCore core; QVERIFY(core.start()); QPointer avatars = new GroupAvatarManager(&core.db); connect(avatars, &GroupAvatarManager::status, this, [&](const QString&, const QString&) { delete avatars.data(); }); avatars->upload(core.channel, QStringLiteral("/nonexistent/avatar.png")); QVERIFY(!avatars); QVERIFY(!GroupAvatarManager::instance()); QCoreApplication::processEvents(); } void oldAvatarOwnerDoesNotClearCurrentOwner() { SettingsCore core; QVERIFY(core.start()); auto* old = new GroupAvatarManager(&core.db); GroupAvatarManager current(&core.db); delete old; QCOMPARE(GroupAvatarManager::instance(), ¤t); } void avatarReadFailureCanBeRetried_data() { QTest::addColumn("prepare"); QTest::newRow("prepare-denied") << true; QTest::newRow("step-interrupted") << false; } void avatarReadFailureCanBeRetried() { QFETCH(bool, prepare); SettingsCore core; QVERIFY(core.start()); QImage image(32, 32, QImage::Format_RGB32); image.fill(Qt::green); QByteArray jpeg; QBuffer output(&jpeg); QVERIFY(output.open(QIODevice::WriteOnly)); QVERIFY(image.save(&output, "JPEG")); QCOMPARE(group_avatar_set(core.inst, core.channel.toULongLong(), (const uint8_t*)jpeg.constData(), size_t(jpeg.size())), 0); const auto row = core.db.getChannels()[0]; GroupAvatarManager avatars(&core.db); QSignalSpy changed(&avatars, &GroupAvatarManager::changed); bool denied = false; if (prepare) QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db, [](void* context, int action, const char*, const char* column, const char*, const char*) { if (action != SQLITE_READ || !column || strcmp(column, "avatar_data")) return SQLITE_OK; *static_cast(context) = true; return SQLITE_DENY; }, &denied), SQLITE_OK); else sqlite3_progress_handler(core.inst->topo_sqlite_db, 1, [](void* context) { *static_cast(context) = true; return 1; }, &denied); auto restore = qScopeGuard([&] { sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr); sqlite3_progress_handler(core.inst->topo_sqlite_db, 0, nullptr, nullptr); }); QVERIFY(avatars.image(core.channel, row.avatarVersion, row.avatarHash).isNull()); QVERIFY(denied); QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr), SQLITE_OK); sqlite3_progress_handler(core.inst->topo_sqlite_db, 0, nullptr, nullptr); avatars.image(core.channel, row.avatarVersion, row.avatarHash); QTRY_VERIFY_WITH_TIMEOUT(!avatars.currentImage(core.channel).isNull(), 1000); QCOMPARE(changed.count(), 1); } void avatarBlobTypesAndLimits_data() { QTest::addColumn("value"); QTest::addColumn("valid"); QTest::addColumn("bytes"); QTest::newRow("empty-blob") << QByteArray("zeroblob(0)") << true << 0; QTest::newRow("maximum-blob") << QByteArray("zeroblob(16384)") << true << 16384; QTest::newRow("oversized-blob") << QByteArray("zeroblob(16385)") << false << 0; QTest::newRow("null") << QByteArray("NULL") << false << 0; QTest::newRow("text") << QByteArray("'jpeg'") << false << 0; QTest::newRow("integer") << QByteArray("1") << false << 0; } void avatarBlobTypesAndLimits() { QFETCH(QByteArray, value); QFETCH(bool, valid); QFETCH(int, bytes); SettingsCore core; QVERIFY(core.start()); const QByteArray sql = "UPDATE channels SET avatar_data=" + value; QCOMPARE(sqlite3_exec(core.inst->topo_sqlite_db, sql.constData(), nullptr, nullptr, nullptr), SQLITE_OK); bool ok = !valid; const QByteArray result = core.db.getChannelAvatar(core.channel, &ok); QCOMPARE(ok, valid); QCOMPARE(result.size(), bytes); QVERIFY(core.db.getChannelAvatar("missing", &ok).isEmpty()); QVERIFY(!ok); } #ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES void avatarAllocationFailure_data() { QTest::addColumn("source"); QTest::newRow("request") << QByteArray("groupavatarmanager.cpp"); QTest::newRow("bridge") << QByteArray("gui_bridge_impl.cpp"); } void avatarAllocationFailure() { QFETCH(QByteArray, source); SettingsCore core; QVERIFY(core.start()); GroupAvatarManager avatars(&core.db); QSignalSpy statuses(&avatars, &GroupAvatarManager::status); const auto baseline = u_get_allocated_count(); allocationFile = source.constData(); failAllocation = 1; auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; }); avatars.remove(core.channel); QCOMPARE(failAllocation, 0); QCOMPARE(statuses.count(), 1); QCOMPARE(statuses[0][1].toString(), QString("Cannot update group avatar")); gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline); } #endif }; QTEST_MAIN(ChannelSettingsTest) #include "test_channel_settings.moc"