2026-10-12 13:14:08 +03:00
|
|
|
#include "testenv.h"
|
|
|
|
|
#include "fixtures.h"
|
|
|
|
|
#include "nettest.h"
|
|
|
|
|
|
|
|
|
|
#include "launcherpaths.h"
|
|
|
|
|
#include "localization.h"
|
|
|
|
|
#include "modloaderversionservice.h"
|
|
|
|
|
|
|
|
|
|
#include <QSignalSpy>
|
2026-11-01 21:07:03 +03:00
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
|
#include <QNetworkReply>
|
2026-10-12 13:14:08 +03:00
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
const QString kFabric = QStringLiteral("https://meta.fabricmc.net/v2/versions/loader/1.20.1");
|
|
|
|
|
const QString kQuilt = QStringLiteral("https://meta.quiltmc.org/v3/versions/loader/1.20.1");
|
|
|
|
|
const QString kForgeMeta = QStringLiteral("https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml");
|
|
|
|
|
const QString kForgePromos = QStringLiteral("https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json");
|
|
|
|
|
const QString kNeoMeta = QStringLiteral("https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml");
|
|
|
|
|
const QString kNeoLegacyMeta = QStringLiteral("https://maven.neoforged.net/releases/net/neoforged/forge/maven-metadata.xml");
|
|
|
|
|
|
|
|
|
|
FakeResponse xml(const QByteArray &body)
|
|
|
|
|
{
|
|
|
|
|
FakeResponse response = FakeResponse::bytes(body);
|
|
|
|
|
response.contentType = "application/xml";
|
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList loaderVersions(const QList<LoaderVersionEntry> &list)
|
|
|
|
|
{
|
|
|
|
|
QStringList out;
|
|
|
|
|
for (const LoaderVersionEntry &entry : list)
|
|
|
|
|
out << entry.loaderVersion;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
class TestModLoaderVersionService : public QObject
|
|
|
|
|
{
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
|
|
|
|
|
FakeInternet m_net;
|
|
|
|
|
|
|
|
|
|
void routeAll()
|
|
|
|
|
{
|
|
|
|
|
m_net.route(kFabric, FakeResponse::json(Fixtures::read("loaders/fabric-1.20.1.json")));
|
|
|
|
|
m_net.route(kQuilt, FakeResponse::json(Fixtures::read("loaders/quilt-1.20.1.json")));
|
|
|
|
|
m_net.route(kForgeMeta, xml(Fixtures::read("loaders/forge-maven-metadata.xml")));
|
|
|
|
|
m_net.route(kForgePromos, FakeResponse::json(Fixtures::read("loaders/forge-promotions_slim.json")));
|
|
|
|
|
m_net.route(kNeoMeta, xml(Fixtures::read("loaders/neoforge-maven-metadata.xml")));
|
|
|
|
|
m_net.route(kNeoLegacyMeta, xml(Fixtures::read("loaders/neoforge-legacy-maven-metadata.xml")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool load(ModLoaderVersionService &service, ModLoader loader, const QString &game,
|
|
|
|
|
CallResult &r, bool force = false)
|
|
|
|
|
{
|
|
|
|
|
service.ensureLoaded(loader, game, r.callback(), force);
|
|
|
|
|
return QTest::qWaitFor([&] { return r.calls > 0; }, 15000);
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-01 21:07:03 +03:00
|
|
|
bool loadNeoForgeInOrder(ModLoaderVersionService &service, CallResult &result,
|
|
|
|
|
const QString &first, const QString &second)
|
|
|
|
|
{
|
|
|
|
|
QStringList completed;
|
|
|
|
|
QObject observer;
|
|
|
|
|
auto *network = service.findChild<QNetworkAccessManager *>();
|
|
|
|
|
if (!network)
|
|
|
|
|
qFatal("ModLoaderVersionService has no network manager");
|
|
|
|
|
connect(network, &QNetworkAccessManager::finished, &observer, [&](QNetworkReply *reply) {
|
|
|
|
|
completed << reply->url().toString();
|
|
|
|
|
});
|
|
|
|
|
service.ensureLoaded(ModLoader::NeoForge, "1.20.1", result.callback());
|
|
|
|
|
if (!QTest::qWaitFor([&] { return completed.contains(first) && m_net.hits(second) == 1; }, 15000))
|
|
|
|
|
return false;
|
|
|
|
|
if (!m_net.releaseResponse(second))
|
|
|
|
|
return false;
|
|
|
|
|
return QTest::qWaitFor([&] { return result.calls == 1; }, 15000);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 13:14:08 +03:00
|
|
|
private slots:
|
|
|
|
|
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
|
|
|
|
|
|
|
|
|
|
void init()
|
|
|
|
|
{
|
|
|
|
|
TestEnv::resetLauncherRoot();
|
|
|
|
|
m_net.reset();
|
|
|
|
|
routeAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void fabric()
|
|
|
|
|
{
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
QSignalSpy changed(&service, &ModLoaderVersionService::versionsChanged);
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", r));
|
|
|
|
|
QVERIFY2(r.ok, qPrintable(r.warning));
|
|
|
|
|
|
|
|
|
|
const QList<LoaderVersionEntry> list = service.versions(ModLoader::Fabric, "1.20.1");
|
|
|
|
|
// Новые первыми: 0.16.10 > 0.16.9 (числовое сравнение, не строковое); пустые версии выброшены.
|
|
|
|
|
QCOMPARE(loaderVersions(list), QStringList({"0.17.0-beta.1", "0.16.10", "0.16.9"}));
|
|
|
|
|
QCOMPARE(list.at(1).versionId, QStringLiteral("fabric-loader-0.16.10-1.20.1"));
|
|
|
|
|
QCOMPARE(list.at(1).gameVersion, QStringLiteral("1.20.1"));
|
|
|
|
|
QVERIFY(list.at(1).stable);
|
|
|
|
|
QVERIFY(!list.at(0).stable);
|
|
|
|
|
QVERIFY(list.at(1).installerUrl.isEmpty());
|
|
|
|
|
QCOMPARE(changed.count(), 1);
|
|
|
|
|
QCOMPARE(changed.constFirst().at(0).toString(), QStringLiteral("fabric"));
|
|
|
|
|
QCOMPARE(changed.constFirst().at(1).toString(), QStringLiteral("1.20.1"));
|
|
|
|
|
|
|
|
|
|
QVERIFY(service.find(ModLoader::Fabric, "1.20.1", "0.16.9").has_value());
|
|
|
|
|
QVERIFY(!service.find(ModLoader::Fabric, "1.20.1", "9.9.9").has_value());
|
|
|
|
|
QVERIFY(service.versions(ModLoader::Fabric, "1.19.2").isEmpty());
|
|
|
|
|
QVERIFY(QFile::exists(LauncherPaths::loaderCacheFile("fabric")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void quiltStabilityHeuristic()
|
|
|
|
|
{
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Quilt, "1.20.1", r));
|
|
|
|
|
QVERIFY(r.ok);
|
|
|
|
|
const QList<LoaderVersionEntry> list = service.versions(ModLoader::Quilt, "1.20.1");
|
|
|
|
|
QCOMPARE(loaderVersions(list), QStringList({"0.26.4-beta.1", "0.26.3"}));
|
|
|
|
|
QCOMPARE(list.at(0).versionId, QStringLiteral("quilt-loader-0.26.4-beta.1-1.20.1"));
|
|
|
|
|
QVERIFY(!list.at(0).stable); // поля stable нет — решает «beta» в номере
|
|
|
|
|
QVERIFY(list.at(1).stable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void forgeMergesPromotions()
|
|
|
|
|
{
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Forge, "1.20.1", r));
|
|
|
|
|
QVERIFY2(r.ok, qPrintable(r.warning));
|
|
|
|
|
QCOMPARE(m_net.hits(kForgeMeta), 1);
|
|
|
|
|
QCOMPARE(m_net.hits(kForgePromos), 1);
|
|
|
|
|
|
|
|
|
|
const QList<LoaderVersionEntry> v1201 = service.versions(ModLoader::Forge, "1.20.1");
|
|
|
|
|
QCOMPARE(loaderVersions(v1201), QStringList({"47.4.1", "47.4.0", "47.3.0"}));
|
|
|
|
|
for (const LoaderVersionEntry &entry : v1201)
|
|
|
|
|
QCOMPARE(entry.recommended, entry.loaderVersion == QLatin1String("47.4.0"));
|
|
|
|
|
QCOMPARE(v1201.at(1).installerUrl,
|
|
|
|
|
QUrl("https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.1-47.4.0/forge-1.20.1-47.4.0-installer.jar"));
|
|
|
|
|
QVERIFY(v1201.at(1).versionId.isEmpty()); // id Forge узнаём только после установки
|
|
|
|
|
|
|
|
|
|
// Весь каталог Forge — одним запросом, по всем версиям игры.
|
|
|
|
|
const QList<LoaderVersionEntry> v1211 = service.versions(ModLoader::Forge, "1.21.1");
|
|
|
|
|
QCOMPARE(loaderVersions(v1211), QStringList({"52.0.10", "52.0.2-beta"}));
|
|
|
|
|
QVERIFY(!v1211.at(1).stable);
|
|
|
|
|
QCOMPARE(loaderVersions(service.versions(ModLoader::Forge, "1.7.10")),
|
|
|
|
|
QStringList{"10.13.4.1614-1.7.10"});
|
|
|
|
|
|
|
|
|
|
// Другая версия игры у Forge не требует нового запроса.
|
|
|
|
|
CallResult again;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Forge, "1.21.1", again));
|
|
|
|
|
QCOMPARE(m_net.hits(kForgeMeta), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void forgeWithoutPromotionsStillWorks()
|
|
|
|
|
{
|
|
|
|
|
m_net.route(kForgePromos, FakeResponse::error(500));
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Forge, "1.20.1", r));
|
|
|
|
|
QVERIFY(r.ok);
|
|
|
|
|
for (const LoaderVersionEntry &entry : service.versions(ModLoader::Forge, "1.20.1"))
|
|
|
|
|
QVERIFY(!entry.recommended);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void neoForgeBothSchemes()
|
|
|
|
|
{
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::NeoForge, "1.21.1", r));
|
|
|
|
|
QVERIFY2(r.ok, qPrintable(r.warning));
|
|
|
|
|
|
|
|
|
|
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "1.21.1")),
|
|
|
|
|
QStringList({"21.1.248", "21.1.200"}));
|
|
|
|
|
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "1.21")), QStringList{"21.0.10"});
|
|
|
|
|
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "1.20.4")),
|
|
|
|
|
QStringList({"20.4.237", "20.4.80-beta"}));
|
|
|
|
|
// Новая нумерация (год.выпуск.патч.сборка).
|
|
|
|
|
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "26.1")), QStringList{"26.1.0.5"});
|
|
|
|
|
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "26.1.1")), QStringList{"26.1.1.3"});
|
|
|
|
|
QCOMPARE(service.find(ModLoader::NeoForge, "1.21.1", "21.1.248")->installerUrl,
|
|
|
|
|
QUrl("https://maven.neoforged.net/releases/net/neoforged/neoforge/21.1.248/neoforge-21.1.248-installer.jar"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void neoForgeLegacyArtifactArrivesLast()
|
|
|
|
|
{
|
|
|
|
|
// Ранние сборки под 1.20.1 — из старого артефакта net.neoforged:forge.
|
2026-11-01 21:07:03 +03:00
|
|
|
FakeResponse legacy = xml(Fixtures::read("loaders/neoforge-legacy-maven-metadata.xml"));
|
|
|
|
|
legacy.deferred = true;
|
|
|
|
|
m_net.route(kNeoLegacyMeta, legacy);
|
2026-10-12 13:14:08 +03:00
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
2026-11-01 21:07:03 +03:00
|
|
|
QVERIFY(loadNeoForgeInOrder(service, r, kNeoMeta, kNeoLegacyMeta));
|
2026-10-12 13:14:08 +03:00
|
|
|
QVERIFY(r.ok);
|
2026-11-01 21:07:03 +03:00
|
|
|
const QList<LoaderVersionEntry> versions = service.versions(ModLoader::NeoForge, "1.20.1");
|
|
|
|
|
QCOMPARE(loaderVersions(versions), QStringList({"47.1.106", "47.1.79"}));
|
|
|
|
|
QCOMPARE(versions.at(0).installerUrl,
|
2026-10-12 13:14:08 +03:00
|
|
|
QUrl("https://maven.neoforged.net/releases/net/neoforged/forge/1.20.1-47.1.106/forge-1.20.1-47.1.106-installer.jar"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void neoForgeLegacyArtifactArrivesFirst()
|
|
|
|
|
{
|
2026-11-01 21:07:03 +03:00
|
|
|
// Основной ответ выпускается только после обработки legacy клиентом.
|
|
|
|
|
FakeResponse primary = xml(Fixtures::read("loaders/neoforge-maven-metadata.xml"));
|
|
|
|
|
primary.deferred = true;
|
|
|
|
|
m_net.route(kNeoMeta, primary);
|
2026-10-12 13:14:08 +03:00
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
2026-11-01 21:07:03 +03:00
|
|
|
QVERIFY(loadNeoForgeInOrder(service, r, kNeoLegacyMeta, kNeoMeta));
|
2026-10-12 13:14:08 +03:00
|
|
|
QVERIFY(r.ok);
|
|
|
|
|
QCOMPARE(service.versions(ModLoader::NeoForge, "1.21.1").size(), 2);
|
|
|
|
|
QCOMPARE(service.versions(ModLoader::NeoForge, "1.20.1").size(), 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void primaryFailureIsError()
|
|
|
|
|
{
|
|
|
|
|
m_net.route(kNeoMeta, FakeResponse::error(503));
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::NeoForge, "1.21.1", r));
|
|
|
|
|
QVERIFY(!r.ok);
|
|
|
|
|
QVERIFY(r.warning.contains(loaderTitle(ModLoader::NeoForge)));
|
|
|
|
|
|
|
|
|
|
m_net.route(kForgeMeta, xml("<metadata><versioning><versions></versions></versioning></metadata>"));
|
|
|
|
|
CallResult empty;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Forge, "1.20.1", empty));
|
|
|
|
|
QVERIFY(!empty.ok);
|
|
|
|
|
QCOMPARE(empty.warning, Loc::text("loader.error.versionsUnparsable").arg(loaderTitle(ModLoader::Forge)));
|
|
|
|
|
|
|
|
|
|
m_net.route(kFabric, FakeResponse::json("{\"not\": \"an array\"}"));
|
|
|
|
|
CallResult notArray;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", notArray));
|
|
|
|
|
QVERIFY(!notArray.ok);
|
|
|
|
|
QCOMPARE(notArray.warning, Loc::text("loader.error.versionsUnparsable").arg(loaderTitle(ModLoader::Fabric)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void cachePerLoaderAndOfflineFallback()
|
|
|
|
|
{
|
|
|
|
|
{
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", r));
|
|
|
|
|
}
|
|
|
|
|
QCOMPARE(m_net.hits(kFabric), 1);
|
|
|
|
|
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult cached;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", cached));
|
|
|
|
|
QVERIFY(cached.ok);
|
|
|
|
|
QCOMPARE(m_net.hits(kFabric), 1); // свежий кеш
|
|
|
|
|
QCOMPARE(service.versions(ModLoader::Fabric, "1.20.1").size(), 3);
|
|
|
|
|
|
|
|
|
|
// Сеть легла, принудительное обновление: данные остаются, ok с предупреждением.
|
|
|
|
|
m_net.route(kFabric, FakeResponse::error(502));
|
|
|
|
|
CallResult offline;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", offline, true));
|
|
|
|
|
QVERIFY(offline.ok);
|
|
|
|
|
QVERIFY(!offline.warning.isEmpty());
|
|
|
|
|
QCOMPARE(service.versions(ModLoader::Fabric, "1.20.1").size(), 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void emptyGameVersionIsNoop()
|
|
|
|
|
{
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult r;
|
|
|
|
|
QVERIFY(load(service, ModLoader::Fabric, " ", r));
|
|
|
|
|
QVERIFY(r.ok);
|
|
|
|
|
QVERIFY(m_net.requests().isEmpty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void inFlightRequestsAreShared()
|
|
|
|
|
{
|
|
|
|
|
FakeResponse slow = FakeResponse::json(Fixtures::read("loaders/fabric-1.20.1.json"));
|
|
|
|
|
slow.delayMs = 300;
|
|
|
|
|
m_net.route(kFabric, slow);
|
|
|
|
|
ModLoaderVersionService service;
|
|
|
|
|
CallResult a, b;
|
|
|
|
|
service.ensureLoaded(ModLoader::Fabric, "1.20.1", a.callback());
|
|
|
|
|
QVERIFY(service.isRefreshing(ModLoader::Fabric, "1.20.1"));
|
|
|
|
|
QVERIFY(!service.isRefreshing(ModLoader::Fabric, "1.19.2"));
|
|
|
|
|
service.ensureLoaded(ModLoader::Fabric, "1.20.1", b.callback());
|
|
|
|
|
QTRY_VERIFY_WITH_TIMEOUT(a.calls == 1 && b.calls == 1, 15000);
|
|
|
|
|
QCOMPARE(m_net.hits(kFabric), 1);
|
|
|
|
|
QVERIFY(!service.isRefreshing(ModLoader::Fabric, "1.20.1"));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
BILAUNCHER_TEST_MAIN(TestModLoaderVersionService)
|
|
|
|
|
#include "tst_modloaderversionservice.moc"
|