272 lines
11 KiB
C++
272 lines
11 KiB
C++
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QLabel>
|
|
#include <QProcess>
|
|
#include <QSslCertificate>
|
|
#include <QSslKey>
|
|
#include <QSslSocket>
|
|
#include <QStackedWidget>
|
|
#include <QTcpServer>
|
|
#include <QTemporaryDir>
|
|
#include <QtTest>
|
|
|
|
#include "desktop_list_page.h"
|
|
#include "main_window.h"
|
|
#include "platform_client.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;
|
|
bool sharedGPU = false;
|
|
bool prepareFailed = false;
|
|
int preparePolls = 0;
|
|
int ticketRequests = 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<QByteArray>();
|
|
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", sharedGPU ? "stopped" : "running"},
|
|
{"powerOnConnect", sharedGPU},
|
|
{"endpoint", "127.0.0.1"}, {"app", "Desktop"}}};
|
|
} else if (path == "/api/v1/terminal/desktop-prepare") {
|
|
status = 202;
|
|
body["taskId"] = "prepare-task";
|
|
body["status"] = "queued";
|
|
} else if (path == "/api/v1/terminal/desktop-prepare/prepare-task") {
|
|
++preparePolls;
|
|
body["status"] = prepareFailed ? "failed" : (preparePolls > 1 ? "success" : "running");
|
|
body["message"] = "prepare test result";
|
|
} else if (path == "/api/v1/terminal/connect-ticket") {
|
|
++ticketRequests;
|
|
status = 201;
|
|
body["ticket"] = "prepared-ticket";
|
|
} else if (path == "/api/v1/terminal/connect-ticket/consume") {
|
|
body = QJsonObject{{"desktopId", "authorized-desktop"}, {"sessionId", "prepared-session"},
|
|
{"endpoint", "127.0.0.1"}, {"width", 2560}, {"height", 1440}, {"fps", 45}};
|
|
} 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<QTemporaryDir> 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<QTemporaryDir>();
|
|
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());
|
|
}
|
|
void sharedDesktopWaitsBeforeIssuingTicket() {
|
|
for (bool fail : {false, true}) {
|
|
TestPlatform server;
|
|
QVERIFY(server.listen(QHostAddress::LocalHost, 0));
|
|
configure(server, server.serverPort());
|
|
server.sharedGPU = true;
|
|
server.prepareFailed = fail;
|
|
PlatformClient client;
|
|
bool authenticated = false;
|
|
client.authenticate([&](const AuthResult& result) { authenticated = result.ok; });
|
|
QTRY_VERIFY(authenticated);
|
|
bool listed = false;
|
|
client.list([&](std::vector<DesktopInfo> desktops, const QString& error) {
|
|
QVERIFY(error.isEmpty());
|
|
QCOMPARE(desktops.size(), size_t(1));
|
|
QVERIFY(desktops[0].powerOnConnect && desktops[0].poweredOff);
|
|
QCOMPARE(desktops[0].state, DesktopState::kAvailable);
|
|
listed = true;
|
|
});
|
|
QTRY_VERIFY(listed);
|
|
bool completed = false;
|
|
client.ticket("authorized-desktop", [&](const ConnectionTicket& ticket, const QString& error) {
|
|
QCOMPARE(error.isEmpty(), !fail);
|
|
if (!fail) QCOMPARE(ticket.sessionId, QString("prepared-session"));
|
|
completed = true;
|
|
});
|
|
QTRY_VERIFY(server.preparePolls > 0);
|
|
QCOMPARE(server.ticketRequests, 0);
|
|
QTRY_VERIFY(completed);
|
|
QCOMPARE(server.ticketRequests, fail ? 0 : 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace vdi
|
|
|
|
QTEST_MAIN(vdi::MainWindowTest)
|
|
#include "test_main_window.moc"
|