#include "testenv.h" #include "fixtures.h" #include "nettest.h" #include "launcherpaths.h" #include "localization.h" #include "modloaderversionservice.h" #include 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 &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); } 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 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 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 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 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. FakeResponse legacySlow = xml(Fixtures::read("loaders/neoforge-legacy-maven-metadata.xml")); legacySlow.delayMs = 400; m_net.route(kNeoLegacyMeta, legacySlow); ModLoaderVersionService service; CallResult r; QVERIFY(load(service, ModLoader::NeoForge, "1.20.1", r)); QVERIFY(r.ok); const QList legacy = service.versions(ModLoader::NeoForge, "1.20.1"); QCOMPARE(loaderVersions(legacy), QStringList({"47.1.106", "47.1.79"})); QCOMPARE(legacy.at(0).installerUrl, 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() { // Тот же каталог, но маленький legacy-ответ приходит раньше основного. FakeResponse primarySlow = xml(Fixtures::read("loaders/neoforge-maven-metadata.xml")); primarySlow.delayMs = 400; m_net.route(kNeoMeta, primarySlow); ModLoaderVersionService service; CallResult r; QVERIFY(load(service, ModLoader::NeoForge, "1.20.1", r)); QVERIFY(r.ok); QCOMPARE(service.versions(ModLoader::NeoForge, "1.21.1").size(), 2); // В startRefresh() основной ответ делает state->merged = parsed и затирает // уже слитые записи legacy — версии NeoForge для 1.20.1 пропадают. QEXPECT_FAIL("", "гонка: основной maven-metadata NeoForge затирает уже пришедший legacy", Continue); 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("")); 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"