#include #include #include #include #include #include #include #include #include #include #include #include #include #include "desktop_list_page.h" #include "main_window.h" namespace vdi { // Real HTTPS requests exercise the device adapter without production identities. class TestPlatform : public QTcpServer { public: QSslCertificate certificate; QSslKey key; bool deny = false; int delayMs = 0; int authRequests = 0; int listRequests = 0; protected: void incomingConnection(qintptr descriptor) override { auto* socket = new QSslSocket(this); socket->setLocalCertificate(certificate); socket->setPrivateKey(key); socket->setPeerVerifyMode(QSslSocket::VerifyNone); if (!socket->setSocketDescriptor(descriptor)) { socket->deleteLater(); return; } auto buffer = std::make_shared(); connect(socket, &QSslSocket::readyRead, this, [this, socket, buffer] { *buffer += socket->readAll(); if (!buffer->contains("\r\n\r\n") || socket->property("answered").toBool()) return; socket->setProperty("answered", true); const QByteArray path = buffer->split(' ').value(1); QJsonObject body; int status = 200; if (path == "/api/v1/terminal/config") { ++authRequests; if (deny) { status = 403; body["message"] = QStringLiteral("设备授权不可用"); } } else if (path.startsWith("/api/v1/terminal/desktops")) { ++listRequests; body["items"] = QJsonArray{QJsonObject{ {"id", "authorized-desktop"}, {"name", "Authorized Windows"}, {"os", "Windows"}, {"powerState", "running"}, {"endpoint", "127.0.0.1"}, {"app", "Desktop"}}}; } else { body["items"] = QJsonArray{}; } const QByteArray data = QJsonDocument(body).toJson(QJsonDocument::Compact); const QByteArray response = "HTTP/1.1 " + QByteArray::number(status) + " Result\r\nContent-Type: application/json\r\nContent-Length: " + QByteArray::number(data.size()) + "\r\nConnection: close\r\n\r\n" + data; QTimer::singleShot(delayMs, socket, [socket, response] { socket->write(response); socket->disconnectFromHost(); }); }); connect(socket, &QSslSocket::disconnected, socket, &QObject::deleteLater); socket->startServerEncryption(); } }; class MainWindowTest : public QObject { Q_OBJECT private: QTemporaryDir certificates_; std::unique_ptr homeDirectory_; QByteArray originalHome_; QString certificatePath_; QString keyPath_; void configure(TestPlatform& server, quint16 port, bool invalidCA = false) { const QString directory = homeDirectory_->path() + "/.config/vdi-terminal"; QVERIFY(QDir().mkpath(directory)); QFile certificateFile(certificatePath_); QVERIFY(certificateFile.open(QIODevice::ReadOnly)); server.certificate = QSslCertificate(certificateFile.readAll()); QFile keyFile(keyPath_); QVERIFY(keyFile.open(QIODevice::ReadOnly)); server.key = QSslKey(keyFile.readAll(), QSsl::Rsa); QFile config(directory + "/platform.json"); QVERIFY(config.open(QIODevice::WriteOnly)); config.write(QJsonDocument(QJsonObject{ {"url", QString("https://127.0.0.1:%1").arg(port)}, {"deviceToken", "isolated-test-identity"}, {"caFile", invalidCA ? directory + "/missing.pem" : certificatePath_} }).toJson()); config.close(); // A previously saved local desktop must not bypass device authentication. QFile local(directory + "/desktops.json"); QVERIFY(local.open(QIODevice::WriteOnly)); local.write("[{\"id\":\"local-only\",\"name\":\"Unauthorized Local\"," "\"os\":\"Windows\",\"endpoint\":\"127.0.0.2\"}]"); } void prepare(MainWindow& window) { // Tests neither update the real watchdog heartbeat nor touch X11 focus. window.heartbeatTimer_->stop(); window.deviceAuthRetryMs_ = 30; } bool selectionVisible(MainWindow& window) { return window.fsm_.isIn(State::kDesktopList) && window.stack_->currentWidget() == window.desktopListPage_; } private slots: void initTestCase() { QVERIFY(certificates_.isValid()); certificatePath_ = certificates_.path() + "/server.pem"; keyPath_ = certificates_.path() + "/server.key"; QProcess openssl; openssl.start("openssl", {"req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", keyPath_, "-out", certificatePath_, "-days", "1", "-subj", "/CN=127.0.0.1", "-addext", "subjectAltName=IP:127.0.0.1"}); QVERIFY(openssl.waitForFinished(10000)); QCOMPARE(openssl.exitCode(), 0); } void init() { originalHome_ = qgetenv("HOME"); homeDirectory_ = std::make_unique(); QVERIFY(homeDirectory_->isValid()); qputenv("HOME", homeDirectory_->path().toUtf8()); } void cleanup() { if (originalHome_.isNull()) qunsetenv("HOME"); else qputenv("HOME", originalHome_); homeDirectory_.reset(); } void offlineBootRecoversWithoutRestart() { TestPlatform server; QVERIFY(server.listen(QHostAddress::LocalHost, 0)); const quint16 port = server.serverPort(); server.close(); configure(server, port); MainWindow window; prepare(window); QTRY_VERIFY(window.deviceAuthTimer_->isActive()); QVERIFY(selectionVisible(window)); QVERIFY(!window.deviceAuthenticated_); QVERIFY(window.desktops_.empty()); QCOMPARE(server.listRequests, 0); QVERIFY(server.listen(QHostAddress::LocalHost, port)); QTRY_VERIFY(window.deviceAuthenticated_); QTRY_COMPARE(window.desktops_.size(), size_t(1)); QCOMPARE(window.desktops_[0].id, QString("authorized-desktop")); QVERIFY(selectionVisible(window)); QVERIFY(!window.deviceAuthTimer_->isActive()); } void rejectedIdentityStaysOnSelectionAndRetries() { TestPlatform server; QVERIFY(server.listen(QHostAddress::LocalHost, 0)); configure(server, server.serverPort()); server.deny = true; MainWindow window; prepare(window); QTRY_VERIFY(server.authRequests >= 3); QVERIFY(selectionVisible(window)); QVERIFY(!window.deviceAuthenticated_); QVERIFY(window.desktops_.empty()); QCOMPARE(server.listRequests, 0); server.deny = false; QTRY_VERIFY(window.deviceAuthenticated_); QTRY_COMPARE(window.desktops_.size(), size_t(1)); QVERIFY(selectionVisible(window)); } void slowAuthenticationDoesNotOverlap() { TestPlatform server; QVERIFY(server.listen(QHostAddress::LocalHost, 0)); configure(server, server.serverPort()); server.delayMs = 200; MainWindow window; prepare(window); QTRY_COMPARE(server.authRequests, 1); window.authenticateDevice(); window.authenticateDevice(); QVERIFY(window.deviceAuthInFlight_); QTRY_VERIFY(window.deviceAuthenticated_); QCOMPARE(server.authRequests, 1); QVERIFY(selectionVisible(window)); } void invalidConfigurationNeverShowsLogin() { TestPlatform server; configure(server, 1, true); MainWindow window; prepare(window); QTRY_VERIFY(window.deviceAuthTimer_->isActive()); QVERIFY(selectionVisible(window)); QVERIFY(!window.deviceAuthenticated_); QCOMPARE(server.authRequests, 0); } void localModeStillLoadsDesktops() { MainWindow window; prepare(window); QTRY_VERIFY(window.deviceAuthenticated_); QVERIFY(selectionVisible(window)); QVERIFY(!window.deviceAuthTimer_->isActive()); } }; } // namespace vdi QTEST_MAIN(vdi::MainWindowTest) #include "test_main_window.moc"