Merge pull request #1011 from andrewrabert/fix_base_url

Fix base URL determination
This commit is contained in:
Izzie Walton authored and GitHub committed 2025-11-11 22:20:33 -05:00
commit f848420123
9 files changed
+576 -108

No files matched your search

+30
View File
@@ -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
+3
View File
@@ -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)
+77
View File
@@ -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).
+34 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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)
{
+15 -1
View File
@@ -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;
};
+54
View File
@@ -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)
+151
View File
@@ -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"