/* Настоящие NodesPage/SQLite: полный снимок, отказ чтения, прерывание и восстановление без частичных данных. */ #include "../src/nodespage.h" #include "../db/db_manager.h" #include #include 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(); const auto text = page.findChild()->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()->rowCount(), 0); const auto text = page.findChild()->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(); const auto text = page.findChild()->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()->toPlainText(), QString("No nodes found.")); QVERIFY(!sqlite3_next_stmt(db.handle, nullptr)); } void readFailure_data() { QTest::addColumn("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(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("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(context)->rows; return 0; }, &state), SQLITE_OK); sqlite3_progress_handler(db.handle, 1, [](void* context) { auto* state = static_cast(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("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()->rowCount(), 0); QVERIFY(page.findChild()->toPlainText().contains("Database not connected")); db.manager.setDb(db.handle); page.refreshNodes(); checkSnapshot(page); NodesPage missing(nullptr); missing.refreshNodes(); QCOMPARE(missing.findChild()->rowCount(), 0); QVERIFY(missing.findChild()->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"