#include "testenv.h" #include "fixtures.h" #include "nettest.h" #include "launcherpaths.h" #include "localization.h" #include "seasonalbuildservice.h" #include #include namespace { const QUrl kBase(QStringLiteral("https://launcher-builds.galeonworx.com")); const QString kIndex = QStringLiteral("https://launcher-builds.galeonworx.com/index.json"); } class TestSeasonalBuildService : public QObject { Q_OBJECT FakeInternet m_net; private slots: void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); } void init() { TestEnv::resetLauncherRoot(); m_net.reset(); } void withoutBaseUrlFails() { SeasonalBuildService service; CallResult r; service.ensureLoaded(r.callback()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 5000); QVERIFY(!r.ok); QCOMPARE(r.warning, Loc::text("seasonal.error.noServerUrl")); QCOMPARE(service.lastError(), Loc::text("seasonal.error.noServerUrl")); QVERIFY(m_net.requests().isEmpty()); } void parsesValidatesAndSorts() { m_net.route(kIndex, FakeResponse::json(Fixtures::read("seasonal/index.json"))); SeasonalBuildService service; service.setBaseUrl(kBase); QCOMPARE(service.baseUrl(), kBase); CallResult r; service.ensureLoaded(r.callback()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); QVERIFY2(r.ok, qPrintable(r.warning)); QStringList ids; for (const SeasonalBuildEntry &entry : service.builds()) ids << entry.id; // Открытые сезоны первыми (новые выше), закрытые — после; всё невалидное отброшено. QCOMPARE(ids, QStringList({"season-5", "vanilla-plus", "season-4"})); const auto s5 = service.find("season-5"); QVERIFY(s5.has_value()); QVERIFY(s5->isValid()); QCOMPARE(s5->name, QStringLiteral("Сезон 5: Пустоши")); QCOMPARE(s5->revision, 3); QCOMPARE(s5->minecraftVersion, QStringLiteral("1.21.1")); QCOMPARE(s5->loader, QStringLiteral("fabric")); QCOMPARE(s5->loaderVersion, QStringLiteral("0.16.10")); QCOMPARE(s5->javaMajor, 21); QCOMPARE(s5->modCount, 120); QCOMPARE(s5->description, QStringLiteral("Пустоши")); // обрезаны пробелы QCOMPARE(s5->seasonStart, QDate(2025, 7, 1)); QVERIFY(!s5->seasonEnd.isValid()); QCOMPARE(s5->archiveSize, qint64(123456)); QCOMPARE(s5->archiveSha256, QString("abcdef" + QString(58, u'0'))); // к нижнему регистру QCOMPARE(s5->archiveUrl, QUrl("https://launcher-builds.galeonworx.com/packs/s5.zip")); QCOMPARE(service.find("season-4")->seasonEnd, QDate(2025, 6, 1)); QVERIFY(service.find("vanilla-plus")->loader.isEmpty()); QVERIFY(QFile::exists(LauncherPaths::seasonalCatalogFile())); } void baseUrlPathIsKept() { m_net.route("https://launcher-builds.galeonworx.com/beta/index.json", FakeResponse::json(QJsonObject{{"formatVersion", 1}, {"builds", QJsonArray{}}})); SeasonalBuildService service; service.setBaseUrl(QUrl("https://launcher-builds.galeonworx.com/beta")); CallResult r; service.ensureLoaded(r.callback()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); QCOMPARE(m_net.hits("https://launcher-builds.galeonworx.com/beta/index.json"), 1); QVERIFY(!r.ok); QCOMPARE(r.warning, Loc::text("seasonal.error.catalogEmpty")); } void formatVersionChecks_data() { QTest::addColumn("format"); QTest::addColumn("error"); QTest::newRow("too new") << 2 << Loc::text("seasonal.error.formatTooNew").arg(2); QTest::newRow("unknown") << 0 << Loc::text("seasonal.error.formatUnknown").arg(0); } void formatVersionChecks() { QFETCH(int, format); QFETCH(QString, error); QJsonObject catalog = Fixtures::readJson("seasonal/index.json"); catalog["formatVersion"] = format; m_net.route(kIndex, FakeResponse::json(catalog)); SeasonalBuildService service; service.setBaseUrl(kBase); CallResult r; service.ensureLoaded(r.callback()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); QVERIFY(!r.ok); QCOMPARE(r.warning, error); QCOMPARE(service.lastError(), error); } void cacheIsBoundToBaseUrl() { m_net.route(kIndex, FakeResponse::json(Fixtures::read("seasonal/index.json"))); { SeasonalBuildService service; service.setBaseUrl(kBase); CallResult r; service.ensureLoaded(r.callback()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); } // Тот же адрес — свежий кеш, без сети. { SeasonalBuildService service; service.setBaseUrl(kBase); CallResult r; service.ensureLoaded(r.callback()); QVERIFY(service.hasData()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 5000); QCOMPARE(m_net.hits(kIndex), 1); } // Другой адрес — чужой кеш не подхватывается. SeasonalBuildService other; other.setBaseUrl(QUrl("https://mirror.example")); m_net.route("https://mirror.example/index.json", FakeResponse::error(503)); CallResult r; other.ensureLoaded(r.callback()); QVERIFY(!other.hasData()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); QVERIFY(!r.ok); QVERIFY(r.warning.startsWith(Loc::text("seasonal.error.catalogFetchFailed").section(u'%', 0, 0))); } void setBaseUrlResetsData() { m_net.route(kIndex, FakeResponse::json(Fixtures::read("seasonal/index.json"))); SeasonalBuildService service; service.setBaseUrl(kBase); CallResult r; service.ensureLoaded(r.callback()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); QVERIFY(service.hasData()); QSignalSpy changed(&service, &SeasonalBuildService::buildsChanged); service.setBaseUrl(kBase); // тот же адрес — ничего не происходит QCOMPARE(changed.count(), 0); service.setBaseUrl(QUrl("https://mirror.example")); QCOMPARE(changed.count(), 1); QVERIFY(!service.hasData()); QVERIFY(!service.find("season-5").has_value()); } void offlineWithCacheKeepsBuilds() { m_net.route(kIndex, FakeResponse::json(Fixtures::read("seasonal/index.json"))); { SeasonalBuildService service; service.setBaseUrl(kBase); CallResult r; service.ensureLoaded(r.callback()); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); } m_net.route(kIndex, FakeResponse::error(502)); SeasonalBuildService service; service.setBaseUrl(kBase); CallResult r; service.ensureLoaded(r.callback(), /*forceRefresh=*/true); QTRY_COMPARE_WITH_TIMEOUT(r.calls, 1, 15000); QVERIFY(r.ok); QVERIFY(!r.warning.isEmpty()); QCOMPARE(service.builds().size(), 3); QCOMPARE(service.lastError(), r.warning); } }; BILAUNCHER_TEST_MAIN(TestSeasonalBuildService) #include "tst_seasonalbuildservice.moc"