mirror of
https://github.com/jellyfin/jellyfin-desktop.git
synced 2026-10-26 16:45:24 +00:00
Merge pull request #1011 from andrewrabert/fix_base_url
Fix base URL determination
This commit is contained in:
9 files changed
+576
-108
No files matched your search
@@ -0,0 +1,30 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies from debian/control
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes devscripts equivs
|
||||
sudo mk-build-deps -i -r -t "apt-get --yes" debian/control
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd build
|
||||
ctest --output-on-failure
|
||||
@@ -77,6 +77,9 @@ endif(Qt5_POSITION_INDEPENDENT_CODE)
|
||||
add_subdirectory(external)
|
||||
add_subdirectory(src)
|
||||
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
|
||||
include(CPackConfiguration)
|
||||
|
||||
add_custom_target(install_app_bundle COMMAND ${CMAKE_COMMAND} -P cmake_install.cmake DEPENDS JellyfinMediaPlayer)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Contributing to Jellyfin Media Player
|
||||
|
||||
## Running Tests
|
||||
|
||||
Jellyfin Media Player uses Qt Test for unit testing.
|
||||
|
||||
### Building Tests
|
||||
|
||||
Tests are built automatically when you build the project:
|
||||
|
||||
```sh
|
||||
cmake -B build
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
Run all tests using CTest:
|
||||
|
||||
```sh
|
||||
cd build
|
||||
ctest
|
||||
```
|
||||
|
||||
Or run individual test executables directly:
|
||||
|
||||
```sh
|
||||
cd build
|
||||
./tests/test_systemcomponent
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
Tests are located in the `tests/` directory. To add a new test:
|
||||
|
||||
1. Create a test file in `tests/` (e.g., `test_mycomponent.cpp`)
|
||||
2. Use `QTEST_APPLESS_MAIN` for headless unit tests
|
||||
3. Add the test to `tests/CMakeLists.txt`
|
||||
|
||||
Example test structure:
|
||||
|
||||
```cpp
|
||||
#include <QtTest/QtTest>
|
||||
#include "../src/mycomponent/MyComponent.h"
|
||||
|
||||
class TestMyComponent : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void testMyFunction_data();
|
||||
void testMyFunction();
|
||||
};
|
||||
|
||||
void TestMyComponent::testMyFunction_data()
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<QString>("expected");
|
||||
|
||||
QTest::newRow("test case 1") << "input1" << "output1";
|
||||
QTest::newRow("test case 2") << "input2" << "output2";
|
||||
}
|
||||
|
||||
void TestMyComponent::testMyFunction()
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QFETCH(QString, expected);
|
||||
|
||||
QString result = MyComponent::myFunction(input);
|
||||
QCOMPARE(result, expected);
|
||||
}
|
||||
|
||||
QTEST_APPLESS_MAIN(TestMyComponent)
|
||||
#include "test_mycomponent.moc"
|
||||
```
|
||||
|
||||
For more information on Qt Test, see the [Qt Test documentation](https://doc.qt.io/qt-6/qtest-overview.html).
|
||||
@@ -1,9 +1,10 @@
|
||||
window.jmpCheckServerConnectivity = (() => {
|
||||
let checkInProgress = false;
|
||||
let activeController = null;
|
||||
|
||||
return async function(url) {
|
||||
if (checkInProgress) {
|
||||
throw new Error('Connectivity check already in progress');
|
||||
const checkFunc = async function(url) {
|
||||
// Abort any in-progress check
|
||||
if (activeController) {
|
||||
activeController.abort();
|
||||
}
|
||||
|
||||
// Wait for API
|
||||
@@ -16,24 +17,48 @@ window.jmpCheckServerConnectivity = (() => {
|
||||
throw new Error('WebChannel not available');
|
||||
}
|
||||
|
||||
checkInProgress = true;
|
||||
// Create abort controller for this check
|
||||
const controller = new AbortController();
|
||||
activeController = controller;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handler = (resultUrl, success) => {
|
||||
if (resultUrl === url) {
|
||||
// Handle abort
|
||||
controller.signal.addEventListener('abort', () => {
|
||||
if (handler) {
|
||||
window.api.system.serverConnectivityResult.disconnect(handler);
|
||||
checkInProgress = false;
|
||||
}
|
||||
reject(new Error('Connection cancelled'));
|
||||
});
|
||||
|
||||
let handler = (resultUrl, success, resolvedUrl) => {
|
||||
if (resultUrl === url && !controller.signal.aborted) {
|
||||
window.api.system.serverConnectivityResult.disconnect(handler);
|
||||
handler = null;
|
||||
if (activeController === controller) {
|
||||
activeController = null;
|
||||
}
|
||||
if (success) {
|
||||
resolve();
|
||||
resolve(resolvedUrl);
|
||||
} else {
|
||||
reject(new Error('Connection failed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.api.system.serverConnectivityResult.connect(handler);
|
||||
window.api.system.checkServerConnectivity(url);
|
||||
});
|
||||
};
|
||||
|
||||
// Expose abort function for cancellation
|
||||
checkFunc.abort = () => {
|
||||
if (activeController) {
|
||||
activeController.abort();
|
||||
activeController = null;
|
||||
}
|
||||
};
|
||||
|
||||
return checkFunc;
|
||||
})();
|
||||
|
||||
window.jmpFetchPage = (() => {
|
||||
|
||||
+20
-23
@@ -3,14 +3,18 @@ async function tryConnect(server) {
|
||||
if (!server.startsWith("http")) {
|
||||
server = "http://" + server;
|
||||
}
|
||||
serverBaseURL = server.replace(/\/+$/, "");
|
||||
|
||||
console.log("Checking connectivity to:", server);
|
||||
|
||||
await window.jmpCheckServerConnectivity(server);
|
||||
const resolvedUrl = await window.jmpCheckServerConnectivity(server);
|
||||
console.log("Server connectivity check passed");
|
||||
console.log("Resolved URL:", resolvedUrl);
|
||||
|
||||
// Save original URL but navigate to fully-resolved redirect
|
||||
window.jmpInfo.settings.main.userWebClient = server;
|
||||
window.location = server;
|
||||
|
||||
// Navigation will clean up handlers, but do it explicitly
|
||||
window.location = resolvedUrl;
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -54,16 +58,8 @@ const startConnecting = async () => {
|
||||
button.style.visibility = 'hidden';
|
||||
document.addEventListener('keydown', cancelOnEscape);
|
||||
|
||||
let connected = false;
|
||||
|
||||
while (!connected && isConnecting) {
|
||||
connected = await tryConnect(server);
|
||||
|
||||
if (!connected && isConnecting) {
|
||||
// Wait 5 seconds before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
// C++ handles retries, just wait for result
|
||||
const connected = await tryConnect(server);
|
||||
|
||||
if (!connected) {
|
||||
isConnecting = false;
|
||||
@@ -82,8 +78,17 @@ const startConnecting = async () => {
|
||||
const cancelConnection = () => {
|
||||
if (!isConnecting) return;
|
||||
|
||||
console.log("Cancelling connection");
|
||||
isConnecting = false;
|
||||
|
||||
// Cancel C++ connectivity check and abort JS promise
|
||||
if (window.api && window.api.system) {
|
||||
window.api.system.cancelServerConnectivity();
|
||||
}
|
||||
if (window.jmpCheckServerConnectivity.abort) {
|
||||
window.jmpCheckServerConnectivity.abort();
|
||||
}
|
||||
|
||||
const address = document.getElementById('address');
|
||||
const title = document.getElementById('title');
|
||||
const spinner = document.getElementById('spinner');
|
||||
@@ -160,16 +165,8 @@ document.addEventListener('keydown', (e) => {
|
||||
button.style.visibility = 'hidden';
|
||||
document.addEventListener('keydown', cancelOnEscape);
|
||||
|
||||
let connected = false;
|
||||
|
||||
while (!connected && isConnecting) {
|
||||
connected = await tryConnect(savedServer);
|
||||
|
||||
if (!connected && isConnecting) {
|
||||
// Wait 5 seconds before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
// C++ handles retries, just wait for result
|
||||
const connected = await tryConnect(savedServer);
|
||||
|
||||
if (!connected) {
|
||||
// User cancelled or error - show UI
|
||||
|
||||
+192
-75
@@ -17,6 +17,7 @@
|
||||
#include <QSslError>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include <functional>
|
||||
|
||||
#include "input/InputComponent.h"
|
||||
#include "SystemComponent.h"
|
||||
@@ -53,12 +54,19 @@ QMap<SystemComponent::PlatformArch, QString> g_platformArchNames = {
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
SystemComponent::SystemComponent(QObject* parent) : ComponentBase(parent), m_platformType(platformTypeUnknown), m_platformArch(platformArchUnknown), m_doLogMessages(false), m_cursorVisible(true), m_scale(1), m_connectivityCheckReply(nullptr)
|
||||
SystemComponent::SystemComponent(QObject* parent) : ComponentBase(parent), m_platformType(platformTypeUnknown), m_platformArch(platformArchUnknown), m_doLogMessages(false), m_cursorVisible(true), m_scale(1), m_connectivityCheckReply(nullptr), m_resolveUrlReply(nullptr)
|
||||
{
|
||||
m_mouseOutTimer = new QTimer(this);
|
||||
m_mouseOutTimer->setSingleShot(true);
|
||||
connect(m_mouseOutTimer, &QTimer::timeout, [&] () { setCursorVisibility(false); });
|
||||
|
||||
m_connectivityRetryTimer = new QTimer(this);
|
||||
m_connectivityRetryTimer->setSingleShot(true);
|
||||
connect(m_connectivityRetryTimer, &QTimer::timeout, [this]() {
|
||||
qInfo() << "Retrying connectivity check for" << m_pendingConnectivityUrl;
|
||||
checkServerConnectivity(m_pendingConnectivityUrl);
|
||||
});
|
||||
|
||||
m_networkManager = new QNetworkAccessManager(this);
|
||||
|
||||
// define OS Type
|
||||
@@ -206,104 +214,213 @@ void SystemComponent::jsLog(int level, QString text)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
void SystemComponent::checkServerConnectivity(QString url)
|
||||
QString SystemComponent::extractBaseUrl(const QString& url)
|
||||
{
|
||||
// Ensure URL ends without trailing slash
|
||||
QString baseUrl = url;
|
||||
while (baseUrl.endsWith("/")) {
|
||||
baseUrl.chop(1);
|
||||
QUrl parsedUrl(url);
|
||||
QString path = parsedUrl.path();
|
||||
|
||||
// Find last occurrence of "/web" in path only (case-insensitive)
|
||||
int webIndex = path.toLower().lastIndexOf("/web");
|
||||
|
||||
if (webIndex >= 0) {
|
||||
// Truncate path at /web
|
||||
path = path.left(webIndex);
|
||||
|
||||
// Build URL without fragment/query
|
||||
QString result = parsedUrl.scheme() + "://" + parsedUrl.host();
|
||||
int port = parsedUrl.port();
|
||||
if (port != -1 &&
|
||||
!((parsedUrl.scheme() == "https" && port == 443) ||
|
||||
(parsedUrl.scheme() == "http" && port == 80))) {
|
||||
result += ":" + QString::number(port);
|
||||
}
|
||||
result += path;
|
||||
return result;
|
||||
}
|
||||
|
||||
QString checkUrl = baseUrl + "/System/Info/Public";
|
||||
// Fallback to origin (scheme://host:port, excluding default ports and userinfo)
|
||||
QString origin = parsedUrl.scheme() + "://" + parsedUrl.host();
|
||||
int port = parsedUrl.port();
|
||||
// Only add port if non-default (443 for https, 80 for http)
|
||||
if (port != -1 &&
|
||||
!((parsedUrl.scheme() == "https" && port == 443) ||
|
||||
(parsedUrl.scheme() == "http" && port == 80))) {
|
||||
origin += ":" + QString::number(port);
|
||||
}
|
||||
return origin;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QSslConfiguration SystemComponent::getSSLConfiguration()
|
||||
{
|
||||
QSslConfiguration sslConfig;
|
||||
if (SettingsComponent::Get().ignoreSSLErrors()) {
|
||||
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||
} else if (SettingsComponent::Get().autodetectCertBundle()) {
|
||||
QString certPath = SettingsComponent::Get().detectCertBundlePath();
|
||||
if (!certPath.isEmpty()) {
|
||||
QList<QSslCertificate> certs = QSslCertificate::fromPath(certPath);
|
||||
if (!certs.isEmpty()) {
|
||||
sslConfig.setCaCertificates(certs);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sslConfig;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
void SystemComponent::setReplyTimeout(QNetworkReply* reply, int ms)
|
||||
{
|
||||
QTimer::singleShot(ms, this, [reply]() {
|
||||
if (!reply->isFinished()) {
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
void SystemComponent::resolveUrl(const QString& url, std::function<void(const QString&)> callback)
|
||||
{
|
||||
// Abort any pending resolve
|
||||
if (m_resolveUrlReply) {
|
||||
m_resolveUrlReply->abort();
|
||||
m_resolveUrlReply->deleteLater();
|
||||
m_resolveUrlReply = nullptr;
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent());
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setSslConfiguration(getSSLConfiguration());
|
||||
|
||||
m_resolveUrlReply = m_networkManager->head(request);
|
||||
|
||||
QNetworkReply* reply = m_resolveUrlReply;
|
||||
setReplyTimeout(reply, NETWORK_REQUEST_TIMEOUT_MS);
|
||||
|
||||
if (SettingsComponent::Get().ignoreSSLErrors()) {
|
||||
connect(reply, QOverload<const QList<QSslError>&>::of(&QNetworkReply::sslErrors),
|
||||
reply, QOverload<>::of(&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, callback]() {
|
||||
if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||
reply->deleteLater();
|
||||
if (m_resolveUrlReply == reply) {
|
||||
m_resolveUrlReply = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "resolveUrl: error:" << reply->errorString();
|
||||
}
|
||||
|
||||
callback(reply->url().toString());
|
||||
reply->deleteLater();
|
||||
if (m_resolveUrlReply == reply) {
|
||||
m_resolveUrlReply = nullptr;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
void SystemComponent::checkServerConnectivity(QString url)
|
||||
{
|
||||
// Stop any pending retry
|
||||
if (m_connectivityRetryTimer->isActive()) {
|
||||
m_connectivityRetryTimer->stop();
|
||||
}
|
||||
|
||||
// Abort any pending connectivity check
|
||||
if (m_connectivityCheckReply) {
|
||||
m_connectivityCheckReply->abort();
|
||||
m_connectivityCheckReply->deleteLater();
|
||||
m_connectivityCheckReply = nullptr;
|
||||
}
|
||||
|
||||
QNetworkRequest request(checkUrl);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent());
|
||||
request.setRawHeader("Cache-Control", "no-cache");
|
||||
// Store for retry
|
||||
m_pendingConnectivityUrl = url;
|
||||
|
||||
// Configure SSL settings
|
||||
QSslConfiguration sslConfig = request.sslConfiguration();
|
||||
resolveUrl(url, [this, url](const QString& fullResolvedUrl) {
|
||||
QString baseUrl = extractBaseUrl(fullResolvedUrl);
|
||||
|
||||
if (SettingsComponent::Get().ignoreSSLErrors()) {
|
||||
qDebug() << "checkServerConnectivity: ignoring SSL errors";
|
||||
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||
} else {
|
||||
if (SettingsComponent::Get().autodetectCertBundle()) {
|
||||
QString certPath = SettingsComponent::Get().detectCertBundlePath();
|
||||
if (!certPath.isEmpty()) {
|
||||
QList<QSslCertificate> certs = QSslCertificate::fromPath(certPath);
|
||||
if (!certs.isEmpty()) {
|
||||
sslConfig.setCaCertificates(certs);
|
||||
qDebug() << "checkServerConnectivity: loaded CA bundle from" << certPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
QString checkUrl = baseUrl + "/System/Info/Public";
|
||||
|
||||
request.setSslConfiguration(sslConfig);
|
||||
QNetworkRequest request(checkUrl);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent());
|
||||
request.setRawHeader("Cache-Control", "no-cache");
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setSslConfiguration(getSSLConfiguration());
|
||||
|
||||
m_connectivityCheckReply = m_networkManager->get(request);
|
||||
m_connectivityCheckReply = m_networkManager->get(request);
|
||||
|
||||
// Handle SSL errors if ignoreSSLErrors is enabled
|
||||
if (SettingsComponent::Get().ignoreSSLErrors()) {
|
||||
QNetworkReply* reply = m_connectivityCheckReply;
|
||||
connect(reply, QOverload<const QList<QSslError>&>::of(&QNetworkReply::sslErrors),
|
||||
this, [reply](const QList<QSslError>& errors) {
|
||||
qDebug() << "checkServerConnectivity: ignoring SSL errors:" << errors;
|
||||
reply->ignoreSslErrors();
|
||||
});
|
||||
}
|
||||
setReplyTimeout(reply, NETWORK_REQUEST_TIMEOUT_MS);
|
||||
|
||||
if (SettingsComponent::Get().ignoreSSLErrors()) {
|
||||
connect(reply, QOverload<const QList<QSslError>&>::of(&QNetworkReply::sslErrors),
|
||||
reply, QOverload<>::of(&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url, fullResolvedUrl]() {
|
||||
if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||
reply->deleteLater();
|
||||
if (m_connectivityCheckReply == reply) {
|
||||
m_connectivityCheckReply = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isObject() && doc.object().contains("Id") && !doc.object()["Id"].toString().isEmpty()) {
|
||||
success = true;
|
||||
} else {
|
||||
qWarning() << "checkServerConnectivity: invalid response";
|
||||
}
|
||||
} else {
|
||||
qWarning() << "checkServerConnectivity: error:" << reply->errorString();
|
||||
}
|
||||
|
||||
if (success) {
|
||||
m_connectivityRetryTimer->stop();
|
||||
m_pendingConnectivityUrl.clear();
|
||||
emit serverConnectivityResult(url, success, fullResolvedUrl);
|
||||
} else {
|
||||
m_connectivityRetryTimer->start(CONNECTIVITY_RETRY_INTERVAL_MS);
|
||||
}
|
||||
|
||||
QNetworkReply* reply = m_connectivityCheckReply;
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
|
||||
// Check if this reply was aborted
|
||||
if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||
qDebug() << "checkServerConnectivity: check was aborted";
|
||||
reply->deleteLater();
|
||||
if (m_connectivityCheckReply == reply) {
|
||||
m_connectivityCheckReply = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
// Parse JSON and verify it has an Id field
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isObject()) {
|
||||
QJsonObject obj = doc.object();
|
||||
if (obj.contains("Id") && !obj["Id"].toString().isEmpty()) {
|
||||
success = true;
|
||||
qInfo() << "checkServerConnectivity: success" << url << "status:" << statusCode;
|
||||
qInfo() << "checkServerConnectivity: response:" << QString::fromUtf8(data);
|
||||
} else {
|
||||
qWarning() << "checkServerConnectivity: response missing Id field" << url;
|
||||
}
|
||||
} else {
|
||||
qWarning() << "checkServerConnectivity: response not valid JSON" << url;
|
||||
}
|
||||
} else {
|
||||
qWarning() << "checkServerConnectivity: error" << url << "error:" << reply->errorString();
|
||||
}
|
||||
|
||||
emit serverConnectivityResult(url, success);
|
||||
reply->deleteLater();
|
||||
if (m_connectivityCheckReply == reply) {
|
||||
m_connectivityCheckReply = nullptr;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
void SystemComponent::cancelServerConnectivity()
|
||||
{
|
||||
if (m_connectivityRetryTimer->isActive()) {
|
||||
m_connectivityRetryTimer->stop();
|
||||
}
|
||||
|
||||
if (m_resolveUrlReply) {
|
||||
m_resolveUrlReply->abort();
|
||||
m_resolveUrlReply->deleteLater();
|
||||
m_resolveUrlReply = nullptr;
|
||||
}
|
||||
|
||||
if (m_connectivityCheckReply) {
|
||||
m_connectivityCheckReply->abort();
|
||||
m_connectivityCheckReply->deleteLater();
|
||||
m_connectivityCheckReply = nullptr;
|
||||
}
|
||||
|
||||
m_pendingConnectivityUrl.clear();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
void SystemComponent::setCursorVisibility(bool visible)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
// System modifiers
|
||||
#define SYSTEM_MODIFIER_OPENELEC "OpenELEC"
|
||||
|
||||
// Network timeouts (milliseconds)
|
||||
constexpr int NETWORK_REQUEST_TIMEOUT_MS = 30000;
|
||||
constexpr int CONNECTIVITY_RETRY_INTERVAL_MS = 5000;
|
||||
|
||||
class SystemComponent : public ComponentBase
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -36,7 +40,11 @@ public:
|
||||
Q_INVOKABLE void jsLog(int level, QString text);
|
||||
|
||||
Q_INVOKABLE void checkServerConnectivity(QString url);
|
||||
Q_SIGNAL void serverConnectivityResult(QString url, bool success);
|
||||
Q_INVOKABLE void cancelServerConnectivity();
|
||||
Q_SIGNAL void serverConnectivityResult(QString url, bool success, QString resolvedUrl);
|
||||
|
||||
QString extractBaseUrl(const QString& url);
|
||||
void resolveUrl(const QString& url, std::function<void(const QString&)> callback);
|
||||
|
||||
Q_INVOKABLE void setCursorVisibility(bool visible);
|
||||
|
||||
@@ -117,6 +125,9 @@ private:
|
||||
bool platformIsMac() const { return m_platformType == platformTypeOsx; }
|
||||
bool platformIsLinux() const { return m_platformType == platformTypeLinux; }
|
||||
|
||||
QSslConfiguration getSSLConfiguration();
|
||||
void setReplyTimeout(QNetworkReply* reply, int ms);
|
||||
|
||||
QTimer* m_mouseOutTimer;
|
||||
QNetworkAccessManager* m_networkManager;
|
||||
PlatformType m_platformType;
|
||||
@@ -127,6 +138,9 @@ private:
|
||||
bool m_cursorVisible;
|
||||
qreal m_scale;
|
||||
QNetworkReply* m_connectivityCheckReply;
|
||||
QNetworkReply* m_resolveUrlReply;
|
||||
QTimer* m_connectivityRetryTimer;
|
||||
QString m_pendingConnectivityUrl;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
cmake_minimum_required(VERSION 3.5.0)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
# Find Qt5 Test module
|
||||
find_package(Qt5 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Test Core Network)
|
||||
|
||||
# Get all source files that are part of the main application
|
||||
get_property(ALL_SRCS GLOBAL PROPERTY SRCS_LIST)
|
||||
|
||||
# Create test executable - link against all application sources
|
||||
add_executable(test_systemcomponent
|
||||
test_systemcomponent.cpp
|
||||
${ALL_SRCS}
|
||||
)
|
||||
|
||||
# Link Qt5 libraries and other dependencies
|
||||
target_link_libraries(test_systemcomponent
|
||||
shared
|
||||
Qt5::Test
|
||||
Qt5::Core
|
||||
Qt5::Network
|
||||
${QT5_LIBRARIES}
|
||||
${MPV_LIBRARY}
|
||||
${OPENGL_LIBS}
|
||||
${OS_LIBS}
|
||||
${EXTRA_LIBS}
|
||||
${X11_LIBRARIES}
|
||||
${X11_Xrandr_LIB}
|
||||
${ICU_LIBRARIES}
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
${RPI_LIBS}
|
||||
${MINIZIP_LIBS}
|
||||
)
|
||||
|
||||
# Include directories - match main application
|
||||
target_include_directories(test_systemcomponent PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/src/core
|
||||
${CMAKE_SOURCE_DIR}/src/shared
|
||||
${CMAKE_SOURCE_DIR}/external
|
||||
${CMAKE_SOURCE_DIR}/external/HIDRemote
|
||||
${CMAKE_SOURCE_DIR}/external/plistparser
|
||||
${CMAKE_SOURCE_DIR}/external/letsmove
|
||||
${CMAKE_SOURCE_DIR}/src/player
|
||||
)
|
||||
|
||||
# Add compile definitions
|
||||
target_compile_definitions(test_systemcomponent PRIVATE
|
||||
PREFIX="${CMAKE_INSTALL_PREFIX}"
|
||||
)
|
||||
|
||||
# Add test to CTest
|
||||
add_test(NAME test_systemcomponent COMMAND test_systemcomponent)
|
||||
@@ -0,0 +1,151 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "../src/system/SystemComponent.h"
|
||||
|
||||
class TestSystemComponent : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void testExtractBaseUrl_data();
|
||||
void testExtractBaseUrl();
|
||||
};
|
||||
|
||||
void TestSystemComponent::testExtractBaseUrl_data()
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<QString>("expected");
|
||||
|
||||
// Standard cases - URLs with /web
|
||||
QTest::newRow("https with /web at root")
|
||||
<< "https://server.com/web/"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with /web and custom port")
|
||||
<< "https://server.com:8096/web/"
|
||||
<< "https://server.com:8096";
|
||||
|
||||
QTest::newRow("http with /web and port 80")
|
||||
<< "http://server.com:80/web/"
|
||||
<< "http://server.com";
|
||||
|
||||
QTest::newRow("https with /web and port 443")
|
||||
<< "https://server.com:443/web/"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with /web at subpath")
|
||||
<< "https://server.com/jellyfin/web/"
|
||||
<< "https://server.com/jellyfin";
|
||||
|
||||
QTest::newRow("https with /web, no trailing slash")
|
||||
<< "https://server.com/jellyfin/web"
|
||||
<< "https://server.com/jellyfin";
|
||||
|
||||
QTest::newRow("https with nested path before /web")
|
||||
<< "https://server.com/path/to/jellyfin/web/"
|
||||
<< "https://server.com/path/to/jellyfin";
|
||||
|
||||
// Case insensitivity
|
||||
QTest::newRow("https with /WEB uppercase")
|
||||
<< "https://server.com/WEB/"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with /Web mixed case")
|
||||
<< "https://server.com/jellyfin/Web/"
|
||||
<< "https://server.com/jellyfin";
|
||||
|
||||
// Fallback cases - URLs without /web
|
||||
QTest::newRow("https root without /web")
|
||||
<< "https://server.com/"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with port, no /web")
|
||||
<< "https://server.com:8096/"
|
||||
<< "https://server.com:8096";
|
||||
|
||||
QTest::newRow("http with port 80, no /web")
|
||||
<< "http://server.com:80/"
|
||||
<< "http://server.com";
|
||||
|
||||
QTest::newRow("https with port 443, no /web")
|
||||
<< "https://server.com:443/"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with path but no /web")
|
||||
<< "https://server.com/some/path"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("http root without /web")
|
||||
<< "http://server.com"
|
||||
<< "http://server.com";
|
||||
|
||||
// Edge cases - query strings and fragments
|
||||
QTest::newRow("https with /web and query string")
|
||||
<< "https://server.com/web/?foo=bar"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with /web and fragment")
|
||||
<< "https://server.com/web/#section"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with /web, query and fragment")
|
||||
<< "https://server.com/jellyfin/web/?foo=bar#section"
|
||||
<< "https://server.com/jellyfin";
|
||||
|
||||
// Edge cases - partial matches
|
||||
QTest::newRow("https with /website (not /web)")
|
||||
<< "https://server.com/website/"
|
||||
<< "https://server.com";
|
||||
|
||||
QTest::newRow("https with /webdav (not /web)")
|
||||
<< "https://server.com/webdav/"
|
||||
<< "https://server.com";
|
||||
|
||||
// Edge cases - multiple /web occurrences (should use last)
|
||||
QTest::newRow("https with multiple /web - uses last")
|
||||
<< "https://server.com/web/something/web/"
|
||||
<< "https://server.com/web/something";
|
||||
|
||||
// Edge cases - localhost and IP addresses
|
||||
QTest::newRow("localhost with port")
|
||||
<< "http://localhost:8096/web/"
|
||||
<< "http://localhost:8096";
|
||||
|
||||
QTest::newRow("IPv4 address")
|
||||
<< "http://192.168.1.100:8096/web/"
|
||||
<< "http://192.168.1.100:8096";
|
||||
|
||||
QTest::newRow("IPv6 address")
|
||||
<< "http://[::1]:8096/web/"
|
||||
<< "http://::1:8096";
|
||||
|
||||
// Edge cases - malformed/empty
|
||||
QTest::newRow("empty string")
|
||||
<< ""
|
||||
<< "://";
|
||||
|
||||
QTest::newRow("no scheme")
|
||||
<< "server.com/web/"
|
||||
<< "://server.com";
|
||||
|
||||
QTest::newRow("only scheme")
|
||||
<< "https://"
|
||||
<< "https://";
|
||||
|
||||
QTest::newRow("no host")
|
||||
<< "https:///web/"
|
||||
<< "https://";
|
||||
}
|
||||
|
||||
void TestSystemComponent::testExtractBaseUrl()
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QFETCH(QString, expected);
|
||||
|
||||
SystemComponent* component = &SystemComponent::Get();
|
||||
QString result = component->extractBaseUrl(input);
|
||||
|
||||
QCOMPARE(result, expected);
|
||||
}
|
||||
|
||||
QTEST_APPLESS_MAIN(TestSystemComponent)
|
||||
#include "test_systemcomponent.moc"
|
||||
Reference in new issue
Block a user