You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

231 lines
16 KiB

/* Реальный браузер 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 <QtTest>
#include <QComboBox>
#include <QTreeWidget>
#include <QPushButton>
#include <QLabel>
#include <QLocale>
#include <QInputDialog>
#include <QMessageBox>
#include <QTemporaryDir>
#include <QFile>
#include <QMimeData>
#include <QDragEnterEvent>
#include <QDropEvent>
extern "C" {
#include "member_files/member_files.h"
#include "mem.h"
}
static mf_gui_request captured;
static mf_gui_request quotaCaptured;
static QList<mf_transfer_request> transfers;
static QList<quint64> cancelled;
static bool ready = true;
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) transfers.append(*(mf_transfer_request*)arg);
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<QMessageBox*>(QApplication::activeModalWidget())) box->button((QMessageBox::StandardButton)result)->click();
else if (auto* input = qobject_cast<QInputDialog*>(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<QPushButton*>()) if (button->text() == "Cancel transfer") return button;
return nullptr;
}
class BrowserTest : public QObject {
Q_OBJECT
private slots:
void init() { transfers.clear(); cancelled.clear(); ready = true; }
void footerDropsCopyAndCaptureGroup() {
DbManager db; db.setMyNodeId(22); ChannelList list(&db); list.resize(350, 400); list.show();
auto* view = list.findChild<QListView*>(); auto* model = qobject_cast<QStandardItemModel*>(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<QWidget*>("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<QPushButton*>(); QVERIFY(!targets.isEmpty());
QList<QWidget*> 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<QLabel*>("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<QLabel*>("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<QLabel*>("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<QComboBox*>()) 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<QComboBox*>(); 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<QLabel*>("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<QTreeWidget*>(); QCOMPARE(table->topLevelItemCount(), 1);
table->setCurrentItem(table->topLevelItem(0));
for (auto* button : browser.findChildren<QPushButton*>()) {
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<qulonglong>(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<QPushButton*>()) 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"