mirror of
https://github.com/spalencsar/bearwave.git
synced 2026-07-06 22:24:17 +02:00
Validate manual stream URLs (http/https only), restrict notification cover downloads to HTTPS, and add transfer timeouts for metadata and cover fetches. Document playback, tray, MPRIS, and QML refactor changes in CHANGELOG and AppStream metadata.
332 lines
12 KiB
C++
332 lines
12 KiB
C++
// Copyright (c) 2026 Sebastian Palencsar
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include <QtTest>
|
|
#include <QApplication>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QTemporaryDir>
|
|
#include <QCryptographicHash>
|
|
#include <QEventLoop>
|
|
#include <QTimer>
|
|
|
|
#include "radiobackend.h"
|
|
|
|
class RadioBackendPlaybackTest : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void initTestCase();
|
|
void init();
|
|
void history_next_navigates_older_entries();
|
|
void resume_disables_list_navigation();
|
|
void station_list_next_uses_filtered_index();
|
|
void history_next_does_not_jump_to_station_list();
|
|
void cached_load_keeps_error_clear_after_network_failure();
|
|
void list_reload_resyncs_index_for_playing_station();
|
|
void list_next_disabled_when_playing_station_not_in_new_list();
|
|
void manual_station_rejects_unsafe_url_scheme();
|
|
void manual_station_accepts_http_and_https();
|
|
};
|
|
|
|
namespace {
|
|
QTemporaryDir *s_tempHome = nullptr;
|
|
|
|
QString cachePathForEndpoint(const QString &endpoint)
|
|
{
|
|
const QString cacheDir = QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache");
|
|
const QString hash = QString(QCryptographicHash::hash(endpoint.toUtf8(), QCryptographicHash::Md5).toHex());
|
|
return cacheDir + QStringLiteral("/") + hash + QStringLiteral(".json");
|
|
}
|
|
|
|
void writeStationCache(const QString &endpoint, const QString &name,
|
|
const QString &url = QString(), const QString &uuid = QString())
|
|
{
|
|
QDir().mkpath(QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache"));
|
|
|
|
QJsonObject station;
|
|
station.insert(QStringLiteral("name"), name);
|
|
station.insert(QStringLiteral("url_resolved"),
|
|
url.isEmpty() ? QStringLiteral("http://example.com/") + name : url);
|
|
station.insert(QStringLiteral("country"), QStringLiteral("Germany"));
|
|
station.insert(QStringLiteral("stationuuid"), uuid.isEmpty() ? name : uuid);
|
|
|
|
QJsonArray array;
|
|
array.append(station);
|
|
|
|
QFile file(cachePathForEndpoint(endpoint));
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
return;
|
|
}
|
|
file.write(QJsonDocument(array).toJson());
|
|
file.close();
|
|
}
|
|
|
|
void writeStationListCache(const QString &endpoint, const QJsonArray &stations)
|
|
{
|
|
QDir().mkpath(QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache"));
|
|
QFile file(cachePathForEndpoint(endpoint));
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
return;
|
|
}
|
|
file.write(QJsonDocument(stations).toJson());
|
|
file.close();
|
|
}
|
|
|
|
int visibleIndexForUrl(RadioBackend &backend, const QString &url)
|
|
{
|
|
const QList<QObject*> stations = backend.stations();
|
|
for (int i = 0; i < stations.size(); ++i) {
|
|
QObject *station = stations[i];
|
|
const QString resolved = station->property("urlResolved").toString();
|
|
if (resolved == url) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::initTestCase()
|
|
{
|
|
s_tempHome = new QTemporaryDir();
|
|
QVERIFY(s_tempHome->isValid());
|
|
qputenv("HOME", s_tempHome->path().toUtf8());
|
|
|
|
const QString configDir = QDir::homePath() + QStringLiteral("/.config/bearwave");
|
|
QDir().mkpath(configDir);
|
|
|
|
QJsonObject state;
|
|
state.insert(QStringLiteral("lastStationName"), QStringLiteral("Station A"));
|
|
state.insert(QStringLiteral("lastStationUrl"), QStringLiteral("http://example.com/a"));
|
|
state.insert(QStringLiteral("volume"), 0.5);
|
|
|
|
QJsonArray recent;
|
|
recent.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Station A")},
|
|
{QStringLiteral("uuid"), QStringLiteral("uuid-a")},
|
|
{QStringLiteral("urlResolved"), QStringLiteral("http://example.com/a")}
|
|
});
|
|
recent.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Station B")},
|
|
{QStringLiteral("uuid"), QStringLiteral("uuid-b")},
|
|
{QStringLiteral("urlResolved"), QStringLiteral("http://example.com/b")}
|
|
});
|
|
recent.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Station C")},
|
|
{QStringLiteral("uuid"), QStringLiteral("uuid-c")},
|
|
{QStringLiteral("urlResolved"), QStringLiteral("http://example.com/c")}
|
|
});
|
|
state.insert(QStringLiteral("recentStations"), recent);
|
|
|
|
QFile file(configDir + QStringLiteral("/state.json"));
|
|
QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate));
|
|
file.write(QJsonDocument(state).toJson());
|
|
file.close();
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::init()
|
|
{
|
|
initTestCase();
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::history_next_navigates_older_entries()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
QVERIFY(backend.playRecentByUuid(QStringLiteral("uuid-b"), QString()));
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/b"));
|
|
QVERIFY(backend.hasNextStation());
|
|
QVERIFY(!backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
QVERIFY(backend.hasPreviousStation());
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::resume_disables_list_navigation()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
backend.resumeLastStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
QVERIFY(!backend.hasNextStation());
|
|
QVERIFY(!backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::station_list_next_uses_filtered_index()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
backend.addManualStation(QStringLiteral("Station One"), QStringLiteral("http://example.com/one"), QStringLiteral("DE"));
|
|
backend.addManualStation(QStringLiteral("Station Two"), QStringLiteral("http://example.com/two"), QStringLiteral("DE"));
|
|
|
|
backend.playStation(0);
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/two"));
|
|
QVERIFY(backend.hasNextStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/one"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::history_next_does_not_jump_to_station_list()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
// Loaded station list present, but playback started from history (not playStation()).
|
|
backend.addManualStation(QStringLiteral("Station X"), QStringLiteral("http://example.com/x"), QStringLiteral("DE"));
|
|
backend.addManualStation(QStringLiteral("Station Y"), QStringLiteral("http://example.com/y"), QStringLiteral("DE"));
|
|
|
|
QVERIFY(backend.playRecentByUuid(QStringLiteral("uuid-c"), QString()));
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/c"));
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/a"));
|
|
QVERIFY(backend.currentStationUrl() != QStringLiteral("http://example.com/x"));
|
|
QVERIFY(backend.currentStationUrl() != QStringLiteral("http://example.com/y"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::cached_load_keeps_error_clear_after_network_failure()
|
|
{
|
|
writeStationCache(QStringLiteral("/stations/topvote/100"), QStringLiteral("Cached Top Station"));
|
|
|
|
RadioBackend backend;
|
|
QVERIFY(QFile::exists(QDir::homePath() + QStringLiteral("/.cache/bearwave/api_cache")));
|
|
backend.loadTopStations();
|
|
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() > 0, 2000);
|
|
QCOMPARE(backend.lastError(), QString());
|
|
|
|
QEventLoop loop;
|
|
QTimer::singleShot(11000, &loop, &QEventLoop::quit);
|
|
loop.exec();
|
|
|
|
QCOMPARE(backend.lastError(), QString());
|
|
QVERIFY(backend.stations().size() > 0);
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::list_reload_resyncs_index_for_playing_station()
|
|
{
|
|
const QString topEndpoint = QStringLiteral("/stations/topvote/100");
|
|
const QString deEndpoint = QStringLiteral("/stations/bycountrycodeexact/DE?limit=50&order=votes&reverse=true");
|
|
const QString sharedUrl = QStringLiteral("http://example.com/shared");
|
|
|
|
QJsonArray topStations;
|
|
topStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Alpha Top")},
|
|
{QStringLiteral("url_resolved"), QStringLiteral("http://example.com/alpha")},
|
|
{QStringLiteral("country"), QStringLiteral("Global")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-alpha")}
|
|
});
|
|
topStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Shared Station")},
|
|
{QStringLiteral("url_resolved"), sharedUrl},
|
|
{QStringLiteral("country"), QStringLiteral("Global")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-shared")}
|
|
});
|
|
topStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Zulu Top")},
|
|
{QStringLiteral("url_resolved"), QStringLiteral("http://example.com/zulu")},
|
|
{QStringLiteral("country"), QStringLiteral("Global")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-zulu")}
|
|
});
|
|
writeStationListCache(topEndpoint, topStations);
|
|
|
|
QJsonArray deStations;
|
|
deStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("DE Only")},
|
|
{QStringLiteral("url_resolved"), QStringLiteral("http://example.com/deonly")},
|
|
{QStringLiteral("country"), QStringLiteral("Germany")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-deonly")}
|
|
});
|
|
deStations.append(QJsonObject{
|
|
{QStringLiteral("name"), QStringLiteral("Shared Station")},
|
|
{QStringLiteral("url_resolved"), sharedUrl},
|
|
{QStringLiteral("country"), QStringLiteral("Germany")},
|
|
{QStringLiteral("stationuuid"), QStringLiteral("uuid-shared")}
|
|
});
|
|
writeStationListCache(deEndpoint, deStations);
|
|
|
|
RadioBackend backend;
|
|
backend.loadTopStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 3, 2000);
|
|
|
|
const int sharedTopIndex = visibleIndexForUrl(backend, sharedUrl);
|
|
QVERIFY(sharedTopIndex >= 0);
|
|
backend.playStation(sharedTopIndex);
|
|
QCOMPARE(backend.currentStationUrl(), sharedUrl);
|
|
QVERIFY(backend.hasNextStation());
|
|
|
|
backend.loadGermanStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 2, 2000);
|
|
QCOMPARE(backend.currentStationUrl(), sharedUrl);
|
|
QVERIFY(!backend.hasNextStation());
|
|
QVERIFY(backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), sharedUrl);
|
|
|
|
backend.playPreviousStation();
|
|
QCOMPARE(backend.currentStationUrl(), QStringLiteral("http://example.com/deonly"));
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::list_next_disabled_when_playing_station_not_in_new_list()
|
|
{
|
|
const QString topEndpoint = QStringLiteral("/stations/topvote/100");
|
|
const QString deEndpoint = QStringLiteral("/stations/bycountrycodeexact/DE?limit=50&order=votes&reverse=true");
|
|
const QString topUrl = QStringLiteral("http://example.com/toponly");
|
|
|
|
writeStationCache(topEndpoint, QStringLiteral("Top Only"), topUrl, QStringLiteral("uuid-top"));
|
|
writeStationCache(deEndpoint, QStringLiteral("DE Only"), QStringLiteral("http://example.com/deonly"),
|
|
QStringLiteral("uuid-de"));
|
|
|
|
RadioBackend backend;
|
|
backend.loadTopStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 1, 2000);
|
|
backend.playStation(0);
|
|
QCOMPARE(backend.currentStationUrl(), topUrl);
|
|
QVERIFY(!backend.hasNextStation());
|
|
|
|
backend.loadGermanStations();
|
|
QTRY_VERIFY_WITH_TIMEOUT(backend.stations().size() == 1, 2000);
|
|
QCOMPARE(backend.currentStationUrl(), topUrl);
|
|
QVERIFY(!backend.hasNextStation());
|
|
QVERIFY(!backend.hasPreviousStation());
|
|
|
|
backend.playNextStation();
|
|
QCOMPARE(backend.currentStationUrl(), topUrl);
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::manual_station_rejects_unsafe_url_scheme()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
backend.addManualStation(QStringLiteral("Unsafe"), QStringLiteral("file:///etc/passwd"), QStringLiteral("DE"));
|
|
QCOMPARE(backend.stations().size(), 0);
|
|
QVERIFY(!backend.lastError().isEmpty());
|
|
|
|
backend.addManualStation(QStringLiteral("FTP"), QStringLiteral("ftp://example.com/stream"), QStringLiteral("DE"));
|
|
QCOMPARE(backend.stations().size(), 0);
|
|
}
|
|
|
|
void RadioBackendPlaybackTest::manual_station_accepts_http_and_https()
|
|
{
|
|
RadioBackend backend;
|
|
|
|
backend.addManualStation(QStringLiteral("HTTP"), QStringLiteral("http://example.com/stream"), QStringLiteral("DE"));
|
|
QCOMPARE(backend.stations().size(), 1);
|
|
QCOMPARE(backend.lastError(), QString());
|
|
|
|
backend.addManualStation(QStringLiteral("HTTPS"), QStringLiteral("https://example.com/stream"), QStringLiteral("DE"));
|
|
QCOMPARE(backend.stations().size(), 2);
|
|
}
|
|
|
|
QTEST_MAIN(RadioBackendPlaybackTest)
|
|
#include "radiobackend_playback_test.moc" |