171 lines
9.2 KiB
C++
171 lines
9.2 KiB
C++
/* Настоящие NodesPage/SQLite: полный снимок, отказ чтения, прерывание и восстановление без частичных данных. */
|
|
#include "../src/nodespage.h"
|
|
#include "../db/db_manager.h"
|
|
#include <QtTest>
|
|
#include <cstring>
|
|
extern "C" {
|
|
#include "debug_config.h"
|
|
}
|
|
|
|
#ifdef UTUN_TEST_NODE_FINALIZE_FAILURE
|
|
static bool failFinalize;
|
|
extern "C" int __real_sqlite3_finalize(sqlite3_stmt*);
|
|
extern "C" int __wrap_sqlite3_finalize(sqlite3_stmt* statement) {
|
|
const int rc = __real_sqlite3_finalize(statement);
|
|
if (!failFinalize || !statement || rc != SQLITE_OK) return rc;
|
|
failFinalize = false;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "[INJECT] nodes statement finalize failure");
|
|
return SQLITE_IOERR;
|
|
}
|
|
#endif
|
|
|
|
struct NodesDatabase {
|
|
sqlite3* handle = nullptr;
|
|
DbManager manager;
|
|
~NodesDatabase() {
|
|
manager.setDb(nullptr);
|
|
if (!handle) return;
|
|
sqlite3_set_authorizer(handle, nullptr, nullptr);
|
|
sqlite3_progress_handler(handle, 0, nullptr, nullptr); sqlite3_trace_v2(handle, 0, nullptr, nullptr);
|
|
if (sqlite3_close(handle) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "nodes test DB close failed: statements remain");
|
|
}
|
|
bool execute(const char* sql) {
|
|
char* error = nullptr;
|
|
const int rc = sqlite3_exec(handle, sql, nullptr, nullptr, &error);
|
|
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "nodes test SQL failed rc=%d: %s", rc, error ? error : sqlite3_errmsg(handle));
|
|
sqlite3_free(error); return rc == SQLITE_OK;
|
|
}
|
|
bool open() {
|
|
const int rc = sqlite3_open(":memory:", &handle);
|
|
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "nodes test DB open failed rc=%d", rc); return false; }
|
|
manager.setDb(handle);
|
|
return execute("CREATE TABLE nodes(node_id INTEGER PRIMARY KEY,name TEXT,x25519_pubkey BLOB,ed25519_pubkey BLOB,"
|
|
"last_seen_at INTEGER,created_at INTEGER);"
|
|
"CREATE TABLE node_addresses(node_id INTEGER NOT NULL,family INTEGER NOT NULL,protocol INTEGER,address BLOB NOT NULL,"
|
|
"port INTEGER,rtt INTEGER,nat_mapping INTEGER,nat_inbound INTEGER,nat_source INTEGER);"
|
|
"INSERT INTO nodes VALUES(1,'Alpha',zeroblob(32),zeroblob(32),1,1),(2,'Beta',NULL,NULL,0,0);"
|
|
"INSERT INTO node_addresses VALUES(1,4,1,x'7F000001',1333,45,0,0,0),"
|
|
"(1,6,6,x'20010DB8000000000000000000000001',8443,0,1,0,1);");
|
|
}
|
|
};
|
|
|
|
struct InterruptState { bool afterRow = false, interrupted = false; int rows = 0; };
|
|
|
|
class NodesPageTest : public QObject {
|
|
Q_OBJECT
|
|
void checkSnapshot(NodesPage& page) {
|
|
const auto* table = page.findChild<QTableWidget*>();
|
|
const auto text = page.findChild<QPlainTextEdit*>()->toPlainText();
|
|
QCOMPARE(table->rowCount(), 2); QCOMPARE(table->columnCount(), 5);
|
|
QCOMPARE(table->item(0, 0)->text(), QString("0000000000000001"));
|
|
QCOMPARE(table->item(0, 1)->text(), QString("Alpha")); QCOMPARE(table->item(0, 4)->text(), QString("2"));
|
|
QCOMPARE(table->item(1, 1)->text(), QString("Beta")); QCOMPARE(table->item(1, 4)->text(), QString("0"));
|
|
QVERIFY2(text.contains("(Alpha)") && text.contains("(Beta)"), qPrintable(text));
|
|
QVERIFY2(text.contains("127.0.0.1:1333 UDP"), qPrintable(text));
|
|
QVERIFY2(text.contains("2001:db8:0:0:0:0:0:1:8443 TCP+REALITY No NAT; inbound untested"), qPrintable(text));
|
|
QVERIFY2(text.contains("rtt=4ms") && text.contains("rtt=-ms"), qPrintable(text));
|
|
QVERIFY(text.contains("addresses: none"));
|
|
}
|
|
void checkFailure(NodesPage& page) {
|
|
QCOMPARE(page.findChild<QTableWidget*>()->rowCount(), 0);
|
|
const auto text = page.findChild<QPlainTextEdit*>()->toPlainText();
|
|
QVERIFY2(text.contains("Query error"), qPrintable(text));
|
|
QVERIFY(!text.contains("(Alpha)")); QVERIFY(!text.contains("(Beta)"));
|
|
}
|
|
private slots:
|
|
void initTestCase() {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
|
|
debug_set_category_level(DEBUG_CATEGORY_CONFIG, DEBUG_LEVEL_DEBUG);
|
|
}
|
|
void refreshAndEmptySnapshot() {
|
|
NodesDatabase db; QVERIFY(db.open()); NodesPage page(&db.manager);
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
QVERIFY(db.execute("UPDATE nodes SET name='Updated' WHERE node_id=1; DELETE FROM node_addresses WHERE family=4;"));
|
|
page.refreshNodes();
|
|
const auto* table = page.findChild<QTableWidget*>();
|
|
const auto text = page.findChild<QPlainTextEdit*>()->toPlainText();
|
|
QCOMPARE(table->item(0, 1)->text(), QString("Updated")); QCOMPARE(table->item(0, 4)->text(), QString("1"));
|
|
QVERIFY(text.contains("(Updated)")); QVERIFY(!text.contains("(Alpha)")); QVERIFY(!text.contains("127.0.0.1"));
|
|
QVERIFY(db.execute("DELETE FROM node_addresses; DELETE FROM nodes;")); page.refreshNodes();
|
|
QCOMPARE(table->rowCount(), 0);
|
|
QCOMPARE(page.findChild<QPlainTextEdit*>()->toPlainText(), QString("No nodes found."));
|
|
QVERIFY(!sqlite3_next_stmt(db.handle, nullptr));
|
|
}
|
|
void readFailure_data() {
|
|
QTest::addColumn<QByteArray>("table");
|
|
QTest::newRow("nodes") << QByteArray("nodes"); QTest::newRow("addresses") << QByteArray("node_addresses");
|
|
}
|
|
void readFailure() {
|
|
QFETCH(QByteArray, table);
|
|
NodesDatabase db; QVERIFY(db.open()); NodesPage page(&db.manager);
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
QCOMPARE(sqlite3_set_authorizer(db.handle, [](void* context, int action, const char* name, const char*, const char*, const char*) {
|
|
if (action != SQLITE_READ || !name || strcmp(name, static_cast<const char*>(context))) return SQLITE_OK;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "[INJECT] nodes read denied table=%s", name); return SQLITE_DENY;
|
|
}, table.data()), SQLITE_OK);
|
|
page.refreshNodes(); checkFailure(page);
|
|
QVERIFY(!sqlite3_next_stmt(db.handle, nullptr));
|
|
QCOMPARE(sqlite3_set_authorizer(db.handle, nullptr, nullptr), SQLITE_OK);
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
}
|
|
void interruptedSnapshot_data() {
|
|
QTest::addColumn<bool>("afterRow");
|
|
QTest::newRow("before-first-row") << false; QTest::newRow("after-row") << true;
|
|
}
|
|
void interruptedSnapshot() {
|
|
QFETCH(bool, afterRow);
|
|
NodesDatabase db; QVERIFY(db.open()); NodesPage page(&db.manager);
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
InterruptState state{afterRow};
|
|
QCOMPARE(sqlite3_trace_v2(db.handle, SQLITE_TRACE_ROW, [](unsigned, void* context, void*, void*) {
|
|
++static_cast<InterruptState*>(context)->rows; return 0;
|
|
}, &state), SQLITE_OK);
|
|
sqlite3_progress_handler(db.handle, 1, [](void* context) {
|
|
auto* state = static_cast<InterruptState*>(context);
|
|
if (state->afterRow && state->rows < 2) return 0;
|
|
state->interrupted = true; return 1;
|
|
}, &state);
|
|
page.refreshNodes(); QVERIFY(state.interrupted); if (afterRow) QVERIFY(state.rows >= 2);
|
|
checkFailure(page); QVERIFY(!sqlite3_next_stmt(db.handle, nullptr));
|
|
sqlite3_progress_handler(db.handle, 0, nullptr, nullptr); sqlite3_trace_v2(db.handle, 0, nullptr, nullptr);
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
}
|
|
void invalidAddress_data() {
|
|
QTest::addColumn<QByteArray>("update");
|
|
QTest::newRow("unknown-family") << QByteArray("family=7");
|
|
QTest::newRow("empty-blob") << QByteArray("address=x''");
|
|
QTest::newRow("text-instead-of-blob") << QByteArray("address='addr'");
|
|
}
|
|
void invalidAddress() {
|
|
QFETCH(QByteArray, update);
|
|
NodesDatabase db; QVERIFY(db.open()); NodesPage page(&db.manager);
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
QVERIFY(db.execute(("UPDATE node_addresses SET " + update + " WHERE family=4").constData()));
|
|
page.refreshNodes(); checkFailure(page); QVERIFY(!sqlite3_next_stmt(db.handle, nullptr));
|
|
QVERIFY(db.execute("UPDATE node_addresses SET family=4,address=x'7F000001' WHERE protocol=1"));
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
}
|
|
void disconnectedDatabase() {
|
|
NodesDatabase db; QVERIFY(db.open()); NodesPage page(&db.manager);
|
|
page.refreshNodes(); checkSnapshot(page); db.manager.setDb(nullptr); page.refreshNodes();
|
|
QCOMPARE(page.findChild<QTableWidget*>()->rowCount(), 0);
|
|
QVERIFY(page.findChild<QPlainTextEdit*>()->toPlainText().contains("Database not connected"));
|
|
db.manager.setDb(db.handle); page.refreshNodes(); checkSnapshot(page);
|
|
NodesPage missing(nullptr); missing.refreshNodes();
|
|
QCOMPARE(missing.findChild<QTableWidget*>()->rowCount(), 0);
|
|
QVERIFY(missing.findChild<QPlainTextEdit*>()->toPlainText().contains("Database not connected"));
|
|
}
|
|
#ifdef UTUN_TEST_NODE_FINALIZE_FAILURE
|
|
void finalizeFailure() {
|
|
NodesDatabase db; QVERIFY(db.open()); NodesPage page(&db.manager);
|
|
page.refreshNodes(); checkSnapshot(page); failFinalize = true;
|
|
page.refreshNodes(); QVERIFY(!failFinalize); checkFailure(page);
|
|
QVERIFY(!sqlite3_next_stmt(db.handle, nullptr));
|
|
page.refreshNodes(); checkSnapshot(page);
|
|
}
|
|
#endif
|
|
};
|
|
|
|
QTEST_MAIN(NodesPageTest)
|
|
#include "test_nodes_page.moc"
|