/* Реальный браузер Qt с захватом bridge-команд: выбор подписанного storage через * NAT, права чужого UID и отбрасывание старого LIST после смены каталога. */ #include "../src/member_files/browser.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" #include "../src/channellist.h" #include "../src/channeldelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "member_files/member_files.h" #include "mem.h" #include "debug_config.h" } static mf_gui_request captured; static mf_gui_request quotaCaptured; static QList transfers; static QList cancelled; static bool ready = true; static std::function transferStarted; extern "C" int gui_bridge_is_uasync_ready(void) { return ready; } extern "C" UTUN_INSTANCE* gui_bridge_get_inst(void) { return nullptr; } extern "C" void gui_bridge_post_uasync_fn(void (*function)(void*), void* arg) { if (function == member_files_gui_request) { auto* request = (mf_gui_request*)arg; if (request->message.command == MF_QUOTA) quotaCaptured = *request; else captured = *request; } if (function == member_files_gui_transfer) { auto request = *(mf_transfer_request*)arg; transfers.append(request); if (transferStarted) transferStarted(request); } if (function == member_files_gui_cancel) cancelled.append(((mf_cancel_request*)arg)->id); u_free(arg); } static void answerDialog(int result) { QTimer::singleShot(0, [result] { if (auto* box = qobject_cast(QApplication::activeModalWidget())) box->button((QMessageBox::StandardButton)result)->click(); else if (auto* input = qobject_cast(QApplication::activeModalWidget())) input->done(result); }); } static void respond(MemberFileBrowser& browser, const mf_gui_request& request, uint16_t status, uint8_t type = MF_FILE) { mf_message r = request.message; r.kind = MF_RESPONSE; r.status = status; r.type = type; memset(r.version, 7, sizeof(r.version)); uint8_t bytes[16 + MF_WIRE_MAX]; mf_put64(bytes, request.group); mf_put64(bytes + 8, request.server); int size = mf_encode(&r, bytes + 16, MF_WIRE_MAX); QVERIFY(size > 0); browser.onResponse(bytes, size + 16); } static void complete(MemberFileBrowser& browser, const mf_transfer_request& request, uint16_t status = MF_OK) { uint8_t bytes[51]{}; mf_put64(bytes, request.id); mf_put64(bytes + 8, request.group); mf_put64(bytes + 16, request.server); mf_put64(bytes + 24, request.owner); bytes[48] = status; bytes[49] = status >> 8; bytes[50] = 2; browser.onTransfer(bytes, sizeof(bytes)); } static QPushButton* cancelButton(MemberFileBrowser& browser) { for (auto* button : browser.findChildren()) if (button->text() == "Cancel transfer") return button; return nullptr; } class BrowserTest : public QObject { Q_OBJECT private slots: void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_INFO); } void init() { transfers.clear(); cancelled.clear(); ready = true; transferStarted = {}; } void exportToSystemClipboardAndSingleDrag() { QTemporaryDir cache; QVERIFY(cache.isValid()); QByteArray oldCache = qgetenv("XDG_CACHE_HOME"); qputenv("XDG_CACHE_HOME", cache.path().toUtf8()); auto restoreCache = qScopeGuard([oldCache] { qputenv("XDG_CACHE_HOME", oldCache); QApplication::clipboard()->clear(); }); sqlite3* sql = nullptr; QCOMPARE(sqlite3_open(":memory:", &sql), SQLITE_OK); QCOMPARE(sqlite3_exec(sql, "CREATE TABLE accounts(node_id INTEGER,display_name TEXT);" "INSERT INTO accounts VALUES(11,'Other'),(22,'Me'),(33,'Server');" "CREATE TABLE peers_42(node_id INTEGER,adm_tags TEXT,adm_tags_sig BLOB,deleted INTEGER,source INTEGER);" "INSERT INTO peers_42 VALUES(11,'{}',NULL,0,0),(22,'{}',NULL,0,0)," "(33,'{\"storage\":\"yes\"}',zeroblob(64),0,0);", nullptr, nullptr, nullptr), SQLITE_OK); DbManager db; db.setDb(sql); db.setMyNodeId(22); QString exported; { MemberFileBrowser browser(&db); browser.setChannel("42", 11); browser.show(); auto* tree = browser.findChild(); auto* item = new QTreeWidgetItem(tree, {"remote.txt"}); item->setData(0, Qt::UserRole, "remote.txt"); item->setData(0, Qt::UserRole + 1, MF_FILE); item->setData(0, Qt::UserRole + 2, QByteArray(16, '\7')); tree->setCurrentItem(item); auto* copy = browser.findChild("memberFilesCopy"); QVERIFY(copy); QVERIFY(copy->isEnabled()); QApplication::clipboard()->setText("unchanged"); copy->trigger(); QTRY_COMPARE(transfers.size(), 1); auto request = transfers.last(); QCOMPARE(request.upload, 0); QCOMPARE(request.owner, 11ULL); QCOMPARE(request.group, 42ULL); QCOMPARE(request.server, 33ULL); QCOMPARE(QString::fromUtf8(request.remote), QString("remote.txt")); QCOMPARE(QByteArray((const char*)request.version, 16), QByteArray(16, '\7')); QCOMPARE(QApplication::clipboard()->text(), QString("unchanged")); exported = QString::fromUtf8(request.local); { QFile file(exported); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write("downloaded bytes"), 16LL); } complete(browser, request); const auto* mime = QApplication::clipboard()->mimeData(); QVERIFY(mime->hasUrls()); QCOMPARE(mime->urls(), QList{QUrl::fromLocalFile(exported)}); QVERIFY(mime->data("x-special/gnome-copied-files").startsWith("copy\n")); copy->trigger(); QCOMPARE(transfers.size(), 1); // Готовая версия не скачивается повторно. item->setData(0, Qt::UserRole + 2, QByteArray(16, '\10')); QApplication::clipboard()->setText("keep on error"); copy->trigger(); QTRY_COMPARE(transfers.size(), 2); complete(browser, transfers.last(), MF_IO_ERROR); QCOMPARE(QApplication::clipboard()->text(), QString("keep on error")); copy->trigger(); QTRY_COMPARE(transfers.size(), 3); auto cancelledRequest = transfers.last(); cancelButton(browser)->click(); complete(browser, cancelledRequest, MF_CANCELLED); QCOMPARE(QApplication::clipboard()->text(), QString("keep on error")); bool committed = false, received = false; transferStarted = [&browser, &committed](mf_transfer_request transfer) { QTimer::singleShot(50, &browser, [&browser, &committed, transfer] { QFile file(QString::fromUtf8(transfer.local)); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write("drag bytes"), 10LL); file.close(); committed = true; complete(browser, transfer); }); }; QTimer receiver; receiver.setInterval(5); connect(&receiver, &QTimer::timeout, &browser, [&] { auto* drag = QCoreApplication::instance()->findChild(); if (!drag || !drag->mimeData()) return; receiver.stop(); auto* data = drag->mimeData(); QVERIFY(data->hasUrls()); QVERIFY(!committed); // Запрос системного менеджера приходит до скачивания и возвращается лишь после commit. auto urls = data->urls(); QVERIFY(committed); QCOMPARE(urls.size(), 1); QFile file(urls.first().toLocalFile()); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), QByteArray("drag bytes")); received = true; QDrag::cancel(); }); receiver.start(); QTimer::singleShot(3000, &browser, [] { QDrag::cancel(); }); tree->setFocus(); QCoreApplication::processEvents(); QPoint point = tree->visualItemRect(item).center(); QTest::mousePress(tree->viewport(), Qt::LeftButton, Qt::NoModifier, point); QMouseEvent move(QEvent::MouseMove, QPointF(point + QPoint(40, 0)), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(tree->viewport(), &move); QTest::mouseRelease(tree->viewport(), Qt::LeftButton, Qt::NoModifier, point + QPoint(40, 0)); QVERIFY(received); QCOMPARE(transfers.size(), 4); transferStarted = {}; } QVERIFY(QFile::exists(exported)); // Закрытие Group files не ломает локальный URI. db.setDb(nullptr); QCOMPARE(sqlite3_close(sql), SQLITE_OK); } void browserDropsIntoCurrentDirectory() { sqlite3* sql = nullptr; QCOMPARE(sqlite3_open(":memory:", &sql), SQLITE_OK); QCOMPARE(sqlite3_exec(sql, "CREATE TABLE accounts(node_id INTEGER,display_name TEXT);" "INSERT INTO accounts VALUES(11,'Other'),(22,'Me'),(33,'Server');" "CREATE TABLE peers_42(node_id INTEGER,adm_tags TEXT,adm_tags_sig BLOB,deleted INTEGER,source INTEGER);" "INSERT INTO peers_42 VALUES(11,'{}',NULL,0,0),(22,'{}',NULL,0,0)," "(33,'{\"storage\":\"yes\"}',zeroblob(64),0,0);", nullptr, nullptr, nullptr), SQLITE_OK); DbManager db; db.setDb(sql); db.setMyNodeId(22); QTemporaryDir dir; QVERIFY(dir.isValid()); QString path = dir.filePath("dropped.txt"); { QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write("test"), 4LL); } QMimeData mime; mime.setUrls({QUrl::fromLocalFile(path)}); { MemberFileBrowser browser(&db); browser.show(); auto* tree = browser.findChild(); auto* location = browser.findChild(); QVERIFY(location); auto* cancel = cancelButton(browser); QVERIFY(cancel); QList targets{&browser, tree->viewport(), location}; for (auto* button : browser.findChildren()) if (button->text().startsWith("Upload")) targets.append(button); for (auto* target : targets) { browser.setChannel("42", 22); location->setText("documents"); location->returnPressed(); QDragEnterEvent enter(QPoint(3, 3), Qt::CopyAction | Qt::MoveAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(target, &enter); DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "test browser drop: target=%s acceptDrops=%d accepted=%d", target->metaObject()->className(), target->acceptDrops(), enter.isAccepted()); QVERIFY(enter.isAccepted()); QCOMPARE(enter.dropAction(), Qt::CopyAction); QVERIFY(browser.findChild("memberFilesDropHint")->isVisible()); QDropEvent drop(QPointF(3, 3), Qt::CopyAction | Qt::MoveAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(target, &drop); QVERIFY(drop.isAccepted()); QCOMPARE(drop.dropAction(), Qt::CopyAction); QVERIFY(!browser.findChild("memberFilesDropHint")->isVisible()); browser.setChannel("43", 11); // Назначение уже зафиксировано до отложенного запуска очереди. QTRY_COMPARE(captured.message.command, uint8_t(MF_STAT)); QCOMPARE(captured.group, 42ULL); QCOMPARE(captured.server, 33ULL); QCOMPARE(captured.message.owner, 22ULL); QCOMPARE(QString::fromUtf8(captured.message.path), QString("documents/dropped.txt")); int count = transfers.size(); respond(browser, captured, MF_NOT_FOUND); QCOMPARE(transfers.size(), count + 1); QCOMPARE(QString::fromUtf8(transfers.last().remote), QString("documents/dropped.txt")); complete(browser, transfers.last()); } browser.setChannel("42", 11); QDragEnterEvent other(QPoint(3, 3), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(tree->viewport(), &other); QVERIFY(!other.isAccepted()); browser.setChannel("42", 22); QDragEnterEvent enter(QPoint(3, 3), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(tree->viewport(), &enter); QVERIFY(enter.isAccepted()); QDropEvent drop(QPointF(3, 3), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(tree->viewport(), &drop); QVERIFY(drop.isAccepted()); quint64 previous = captured.message.request_id; cancel->click(); QCoreApplication::processEvents(); QCOMPARE(captured.message.request_id, previous); QVERIFY(!cancel->isEnabled()); ready = false; QDragEnterEvent stopped(QPoint(3, 3), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(tree->viewport(), &stopped); QVERIFY(!stopped.isAccepted()); ready = true; } db.setDb(nullptr); QCOMPARE(sqlite3_close(sql), SQLITE_OK); } void footerDropsCopyAndCaptureGroup() { DbManager db; db.setMyNodeId(22); ChannelList list(&db); list.resize(350, 400); list.show(); auto* view = list.findChild(); auto* model = qobject_cast(view->model()); QVERIFY(model); for (const auto& id : {QString("42"), QString("43"), QString("dm:abc")}) { auto* item = new QStandardItem; item->setData(id, ChannelChannelIdRole); model->appendRow(item); } auto* footer = list.findChild("channelFooter"); QVERIFY(footer); QSignalSpy drops(&list, &ChannelList::ownFilesDropped); QMimeData mime; mime.setUrls({QUrl::fromLocalFile("/tmp/a.txt"), QUrl::fromLocalFile("/tmp/b.txt")}); list.selectChannel("42"); auto targets = footer->findChildren(); QVERIFY(!targets.isEmpty()); QList widgets{footer}; for (auto* button : targets) widgets.append(button); for (auto* target : widgets) { list.selectChannel("42"); QDragEnterEvent enter(QPoint(3, 3), Qt::CopyAction | Qt::MoveAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(target, &enter); QVERIFY(enter.isAccepted()); QCOMPARE(enter.dropAction(), Qt::CopyAction); QVERIFY(list.findChild("ownFilesDropHint")->isVisible()); QDropEvent drop(QPointF(3, 3), Qt::CopyAction | Qt::MoveAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(target, &drop); QVERIFY(drop.isAccepted()); QCOMPARE(drop.dropAction(), Qt::CopyAction); list.selectChannel("43"); QCoreApplication::processEvents(); QCOMPARE(drops.last()[0].toString(), QString("42")); QCOMPARE(drops.last()[1].toStringList(), QStringList({"/tmp/a.txt", "/tmp/b.txt"})); QVERIFY(!list.findChild("ownFilesDropHint")->isVisible()); } list.selectChannel("dm:abc"); QDragEnterEvent dm(QPoint(3, 3), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(footer, &dm); QVERIFY(!dm.isAccepted()); list.selectChannel("42"); ready = false; QDragEnterEvent stopped(QPoint(3, 3), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(footer, &stopped); QVERIFY(!stopped.isAccepted()); ready = true; mime.setUrls({QUrl("https://example.com/file")}); QDragEnterEvent remote(QPoint(3, 3), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(footer, &remote); QVERIFY(!remote.isAccepted()); } void uploadQueueConflictsAndCancellation() { sqlite3* sql = nullptr; QCOMPARE(sqlite3_open(":memory:", &sql), SQLITE_OK); QCOMPARE(sqlite3_exec(sql, "CREATE TABLE accounts(node_id INTEGER,display_name TEXT);" "INSERT INTO accounts VALUES(22,'Me'),(33,'Server');" "CREATE TABLE peers_42(node_id INTEGER,adm_tags TEXT,adm_tags_sig BLOB,deleted INTEGER,source INTEGER);" "INSERT INTO peers_42 VALUES(22,'{}',NULL,0,0),(33,'{\"storage\":\"yes\"}',zeroblob(64),0,0);", nullptr, nullptr, nullptr), SQLITE_OK); DbManager db; db.setDb(sql); db.setMyNodeId(22); QTemporaryDir dir; QVERIFY(dir.isValid()); QStringList paths; for (const auto& name : {"a.txt", "b.txt", "c.txt"}) { QString path = dir.filePath(name); QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write("test"), 4LL); paths.append(path); } { MemberFileBrowser browser(&db); auto* cancel = cancelButton(browser); QVERIFY(cancel); auto* status = browser.findChild("memberFilesUploadStatus"); QVERIFY(status); answerDialog(QDialog::Accepted); browser.uploadOwnFiles("42", paths); QTRY_COMPARE(captured.message.command, uint8_t(MF_STAT)); QCOMPARE(captured.message.owner, 22ULL); QCOMPARE(captured.server, 33ULL); QVERIFY(transfers.isEmpty()); quint64 firstStat = captured.message.request_id; browser.setChannel("43", 11); QCOMPARE(captured.message.request_id, firstStat); for (auto* selector : browser.findChildren()) QVERIFY(!selector->isEnabled()); respond(browser, captured, MF_NOT_FOUND); QCOMPARE(transfers.size(), 1); QCOMPARE(QString::fromUtf8(transfers[0].remote), QString("a.txt")); QCOMPARE(transfers[0].owner, 22ULL); QCOMPARE(QString::fromUtf8(transfers[0].local), paths[0]); QCOMPARE(transfers[0].group, 42ULL); QCOMPARE(QByteArray((const char*)transfers[0].version, 16), QByteArray(16, '\0')); complete(browser, transfers[0]); QCOMPARE(QString::fromUtf8(captured.message.path), QString("b.txt")); answerDialog(QMessageBox::No); respond(browser, captured, MF_OK); QCOMPARE(transfers.size(), 1); QCOMPARE(QString::fromUtf8(captured.message.path), QString("c.txt")); answerDialog(QMessageBox::Yes); respond(browser, captured, MF_OK); QCOMPARE(transfers.size(), 2); QCOMPARE(QByteArray((const char*)transfers[1].version, 16), QByteArray(16, '\7')); complete(browser, transfers[1]); QVERIFY(status->text().contains("2 uploaded, 1 skipped")); QVERIFY(!cancel->isEnabled()); for (const auto& path : paths) QVERIFY(QFile::exists(path)); answerDialog(QDialog::Accepted); browser.uploadOwnFiles("42", paths); QTRY_COMPARE(captured.message.command, uint8_t(MF_STAT)); mf_gui_request stale = captured; cancel->click(); QVERIFY(cancelled.contains(stale.message.request_id)); respond(browser, stale, MF_NOT_FOUND); QCOMPARE(transfers.size(), 2); QVERIFY(!cancel->isEnabled()); answerDialog(QDialog::Accepted); browser.uploadOwnFiles("42", paths); QTRY_COMPARE(captured.message.command, uint8_t(MF_STAT)); respond(browser, captured, MF_NOT_FOUND); QCOMPARE(transfers.size(), 3); auto active = transfers.last(); cancel->click(); QVERIFY(cancelled.contains(active.id)); QVERIFY(cancel->isEnabled()); complete(browser, active, MF_CANCELLED); QVERIFY(!cancel->isEnabled()); QCOMPARE(transfers.size(), 3); QVERIFY(status->text().contains("cancelled")); answerDialog(QDialog::Accepted); browser.uploadOwnFiles("42", paths); QTRY_COMPARE(captured.message.command, uint8_t(MF_STAT)); respond(browser, captured, MF_NOT_FOUND); QCOMPARE(transfers.size(), 4); complete(browser, transfers.last(), MF_NO_SPACE); QVERIFY(status->text().contains("Upload stopped: a.txt")); QVERIFY(!cancel->isEnabled()); QCOMPARE(transfers.size(), 4); answerDialog(QDialog::Accepted); browser.uploadOwnFiles("42", {dir.path()}); QTRY_VERIFY(status->text().contains("not folders or links")); QVERIFY(!cancel->isEnabled()); QCOMPARE(transfers.size(), 4); QCOMPARE(sqlite3_exec(sql, "DELETE FROM peers_42 WHERE node_id=33", nullptr, nullptr, nullptr), SQLITE_OK); answerDialog(QMessageBox::Ok); browser.uploadOwnFiles("42", paths); QVERIFY(!cancel->isEnabled()); QCOMPARE(transfers.size(), 4); } db.setDb(nullptr); QCOMPARE(sqlite3_close(sql), SQLITE_OK); } void rolePermissionsAndLateResponse() { sqlite3* sql = nullptr; QCOMPARE(sqlite3_open(":memory:", &sql), SQLITE_OK); QCOMPARE(sqlite3_exec(sql, "CREATE TABLE accounts(node_id INTEGER,display_name TEXT);" "INSERT INTO accounts VALUES(11,'Author'),(22,'Reader'),(33,'Server');" "CREATE TABLE peers_42(node_id INTEGER,storage INTEGER,adm_tags TEXT,adm_tags_sig BLOB,deleted INTEGER,source INTEGER);" "INSERT INTO peers_42 VALUES(11,0,'{}',NULL,0,0),(22,0,'{}',NULL,0,0)," "(33,0,'{\"storage\":\"yes\"}',zeroblob(64),0,0);", nullptr, nullptr, nullptr), SQLITE_OK); DbManager db; db.setDb(sql); db.setMyNodeId(22); { MemberFileBrowser browser(&db); browser.setChannel("42", 11); auto selectors = browser.findChildren(); QCOMPARE(selectors.size(), 2); QCOMPARE(selectors[0]->count(), 1); QCOMPARE(selectors[0]->currentData().toULongLong(), 33ULL); QCOMPARE(captured.message.owner, 11ULL); QCOMPARE(captured.message.command, (uint8_t)MF_LIST); QCOMPARE(quotaCaptured.message.owner, 11ULL); QCOMPARE(quotaCaptured.message.command, (uint8_t)MF_QUOTA); auto* quota = browser.findChild("memberFilesQuota"); QVERIFY(quota); mf_gui_request previousQuota = quotaCaptured; mf_message stats = quotaCaptured.message; stats.kind = MF_RESPONSE; stats.data_len = MF_QUOTA_BYTES; mf_put64(stats.data, 2 * 1024 * 1024); mf_put64(stats.data + 8, 8 * 1024 * 1024); mf_put64(stats.data + 16, 1024 * 1024); mf_put64(stats.data + 24, 42); mf_put64(stats.data + 32, 7); uint8_t quotaPacket[16 + MF_WIRE_MAX]; mf_put64(quotaPacket, 42); mf_put64(quotaPacket + 8, 33); int quotaLength = mf_encode(&stats, quotaPacket + 16, MF_WIRE_MAX); browser.onResponse(quotaPacket, quotaLength + 16); QCOMPARE(quota->text(), QString("Storage: %1 used / %2 total · Files: 42 · Directories: 7 · Reserved for uploads: %3") .arg(QLocale().formattedDataSize(2 * 1024 * 1024, 2), QLocale().formattedDataSize(8 * 1024 * 1024, 2), QLocale().formattedDataSize(1024 * 1024, 2))); QVERIFY(quota->toolTip().contains("2097152")); mf_gui_request previous = captured; mf_message r = previous.message; r.kind = MF_RESPONSE; r.type = MF_DIRECTORY; const char name[] = "readme.txt"; unsigned n = sizeof(name) - 1; r.data[0] = (uint8_t)n; r.data[2] = MF_FILE; mf_put64(r.data + 3, 123); memcpy(r.data + MF_ENTRY_HEADER, name, n); r.data_len = MF_ENTRY_HEADER + n; uint8_t packet[16 + MF_WIRE_MAX]; mf_put64(packet, 42); mf_put64(packet + 8, 33); int length = mf_encode(&r, packet + 16, MF_WIRE_MAX); browser.onResponse(packet, length + 16); auto* table = browser.findChild(); QCOMPARE(table->topLevelItemCount(), 1); table->setCurrentItem(table->topLevelItem(0)); for (auto* button : browser.findChildren()) { if (button->text().startsWith("Upload") || button->text().startsWith("Rename") || button->text() == "Delete") QVERIFY(!button->isEnabled()); if (button->text().startsWith("Download")) QVERIFY(button->isEnabled()); } int own = selectors[1]->findData(QVariant::fromValue(22)); QVERIFY(own >= 0); selectors[1]->setCurrentIndex(own); QCOMPARE(captured.message.owner, 22ULL); QCOMPARE(table->topLevelItemCount(), 0); QCOMPARE(quotaCaptured.message.owner, 22ULL); QVERIFY(quotaCaptured.message.request_id != previousQuota.message.request_id); browser.onResponse(quotaPacket, quotaLength + 16); QCOMPARE(quota->text(), QString("Loading quota…")); stats = quotaCaptured.message; stats.kind = MF_RESPONSE; stats.data_len = MF_QUOTA_BYTES; mf_put64(stats.data, 3 * 1024 * 1024); mf_put64(stats.data + 8, 8 * 1024 * 1024); mf_put64(stats.data + 24, 12); mf_put64(stats.data + 32, 3); quotaLength = mf_encode(&stats, quotaPacket + 16, MF_WIRE_MAX); browser.onResponse(quotaPacket, quotaLength + 16); QString ownQuota = QString("Storage: %1 used / %2 total · Files: 12 · Directories: 3") .arg(QLocale().formattedDataSize(3 * 1024 * 1024, 2), QLocale().formattedDataSize(8 * 1024 * 1024, 2)); QCOMPARE(quota->text(), ownQuota); browser.onResponse(packet, length + 16); QCOMPARE(table->topLevelItemCount(), 0); for (auto* button : browser.findChildren()) if (button->text().startsWith("Upload")) QVERIFY(button->isEnabled()); mf_message busy = captured.message; busy.kind = MF_RESPONSE; busy.status = MF_BUSY; length = mf_encode(&busy, packet + 16, MF_WIRE_MAX); browser.onResponse(packet, length + 16); QTRY_VERIFY_WITH_TIMEOUT(captured.message.request_id != busy.request_id, 1500); QCOMPARE(captured.message.owner, 22ULL); selectors[1]->setCurrentIndex(0); QCOMPARE(quota->text(), QString("Select a member to see storage quota")); browser.onResponse(quotaPacket, quotaLength + 16); QCOMPARE(quota->text(), QString("Select a member to see storage quota")); } db.setDb(nullptr); QCOMPARE(sqlite3_close(sql), SQLITE_OK); } }; QTEST_MAIN(BrowserTest) #include "test_member_files_browser.moc"