237 lines
10 KiB
C++
237 lines
10 KiB
C++
#include "testenv.h"
|
||
#include "fixtures.h"
|
||
|
||
#include "buildarchiveworker.h"
|
||
#include "localization.h"
|
||
|
||
#include <QDir>
|
||
#include <QSignalSpy>
|
||
|
||
class TestBuildArchiveWorker : public QObject
|
||
{
|
||
Q_OBJECT
|
||
|
||
QString m_game;
|
||
QString m_work;
|
||
|
||
void populateGame()
|
||
{
|
||
Fixtures::write(m_game + "/options.txt", "fov:90");
|
||
Fixtures::write(m_game + "/mods/a.jar", "mod-a");
|
||
Fixtures::write(m_game + "/mods/sub/b.jar", "mod-b");
|
||
Fixtures::write(m_game + "/config/x/y.toml", "y=1");
|
||
Fixtures::write(m_game + "/saves/World/level.dat", "level"); // общее — исключается
|
||
Fixtures::write(m_game + "/versions/1.20.1/1.20.1.json", "{}"); // общее — исключается
|
||
}
|
||
|
||
// Последний вызов finished(): (ok, error)
|
||
static QPair<bool, QString> result(const QSignalSpy &spy)
|
||
{
|
||
if (spy.isEmpty())
|
||
return {false, QStringLiteral("finished не пришёл")};
|
||
return {spy.constLast().at(0).toBool(), spy.constLast().at(1).toString()};
|
||
}
|
||
|
||
private slots:
|
||
void init()
|
||
{
|
||
m_game = TestEnv::freshDir(QStringLiteral("game"));
|
||
m_work = TestEnv::freshDir(QStringLiteral("archives"));
|
||
}
|
||
|
||
void archiveClearRestoreRoundTrip()
|
||
{
|
||
populateGame();
|
||
const QStringList shared{"saves", "versions"};
|
||
const QString zip = m_work + "/build.zip";
|
||
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
QSignalSpy progress(&worker, &BuildArchiveWorker::progress);
|
||
|
||
worker.archive(m_game, zip, shared);
|
||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||
QCOMPARE(Fixtures::zipEntries(zip),
|
||
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar", "options.txt"}));
|
||
// progress: (0,total) в начале и (total,total) в конце.
|
||
QCOMPARE(progress.constFirst().at(0).toInt(), 0);
|
||
QCOMPARE(progress.constLast().at(0).toInt(), 4);
|
||
QCOMPARE(progress.constLast().at(1).toInt(), 4);
|
||
|
||
worker.clear(m_game, shared);
|
||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||
QCOMPARE(Fixtures::listTree(m_game),
|
||
QStringList({"saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
|
||
|
||
worker.restore(zip, m_game);
|
||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||
QCOMPARE(Fixtures::listTree(m_game),
|
||
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar",
|
||
"options.txt", "saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
|
||
QCOMPARE(Fixtures::readFile(m_game + "/mods/sub/b.jar"), QByteArray("mod-b"));
|
||
}
|
||
|
||
void dotFilesSurviveRoundTrip()
|
||
{
|
||
// В .minecraft живут .fabric/, .mixin.out/, на macOS ещё .DS_Store.
|
||
Fixtures::write(m_game + "/.fabric/remappedJars/x.jar", "cache");
|
||
Fixtures::write(m_game + "/mods/a.jar", "a");
|
||
const QString zip = m_work + "/dots.zip";
|
||
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
worker.archive(m_game, zip, {});
|
||
QVERIFY(result(finished).first);
|
||
worker.clear(m_game, {});
|
||
worker.restore(zip, m_game);
|
||
QVERIFY(result(finished).first);
|
||
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
|
||
|
||
// QZipReader при чтении срезает ведущие «.» и «/» в именах записей, поэтому
|
||
// после переключения сборок .fabric/ возвращается как fabric/.
|
||
QEXPECT_FAIL("", "QZipReader срезает ведущую точку: .fabric/ восстанавливается как fabric/", Continue);
|
||
QVERIFY(QFile::exists(m_game + "/.fabric/remappedJars/x.jar"));
|
||
}
|
||
|
||
void archiveMissingGameDir()
|
||
{
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
const QString missing = m_game + "/nope";
|
||
worker.archive(missing, m_work + "/x.zip", {});
|
||
QCOMPARE(result(finished),
|
||
qMakePair(false, Loc::text("switch.error.gameDirMissing").arg(QDir::toNativeSeparators(missing))));
|
||
}
|
||
|
||
void archiveCancelRemovesTempZip()
|
||
{
|
||
populateGame();
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
worker.requestCancel();
|
||
worker.archive(m_game, m_work + "/cancel.zip", {});
|
||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.archivingCancelled")));
|
||
QVERIFY(!QFile::exists(m_work + "/cancel.zip"));
|
||
|
||
worker.clearCancel();
|
||
worker.archive(m_game, m_work + "/ok.zip", {});
|
||
QVERIFY(result(finished).first);
|
||
}
|
||
|
||
void clearMissingDirIsOk()
|
||
{
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
worker.clear(m_game + "/nope", {});
|
||
QVERIFY(result(finished).first);
|
||
}
|
||
|
||
void restoreRejectsUnreadableAndUnsafeArchives()
|
||
{
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
|
||
const QString missing = m_work + "/missing.zip";
|
||
worker.restore(missing, m_game);
|
||
QCOMPARE(result(finished),
|
||
qMakePair(false, Loc::text("switch.error.archiveOpenFailed").arg(QDir::toNativeSeparators(missing))));
|
||
|
||
// Zip-slip: запись с «..» не должна вылезти из папки игры.
|
||
const QString evil = m_work + "/evil.zip";
|
||
// Ведущие «../» QZipReader срезает сам, а «..» в середине пути — нет.
|
||
QVERIFY(Fixtures::makeZip(evil, {{"ok.txt", "ok"}, {"mods/../../escaped.txt", "pwned"}}));
|
||
QVERIFY(Fixtures::zipEntries(evil).contains("mods/../../escaped.txt"));
|
||
worker.restore(evil, m_game);
|
||
QCOMPARE(result(finished),
|
||
qMakePair(false, Loc::text("switch.error.archiveBadPath").arg("mods/../../escaped.txt")));
|
||
QVERIFY(!QFile::exists(QFileInfo(m_game).absolutePath() + "/escaped.txt"));
|
||
}
|
||
|
||
void restoreCancel()
|
||
{
|
||
const QString zip = m_work + "/r.zip";
|
||
QVERIFY(Fixtures::makeZip(zip, {{"a.txt", "a"}}));
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
worker.requestCancel();
|
||
worker.restore(zip, m_game);
|
||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.extractCancelled")));
|
||
}
|
||
|
||
void applyPackRemovesThenWrites()
|
||
{
|
||
populateGame();
|
||
Fixtures::write(m_game + "/mods/old/legacy.jar", "old");
|
||
Fixtures::write(m_game + "/mods/keep.jar", "keep");
|
||
|
||
const QString pack = m_work + "/pack.zip";
|
||
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"},
|
||
{"config/x/y.toml", "y=2"},
|
||
{"options.txt", "fov:70"}}));
|
||
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
QSignalSpy entries(&worker, &BuildArchiveWorker::packEntries);
|
||
worker.applyPack(pack, m_game,
|
||
{"mods/old/legacy.jar", "mods/missing.jar",
|
||
"../outside.txt", "versions/1.20.1/1.20.1.json"},
|
||
{"versions", "saves"});
|
||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||
|
||
QVERIFY(!QFile::exists(m_game + "/mods/old/legacy.jar"));
|
||
QVERIFY(!QDir(m_game + "/mods/old").exists()); // опустевшая папка убрана
|
||
QVERIFY(QFile::exists(m_game + "/mods/keep.jar"));
|
||
// Удаление в запретной зоне молча пропускается.
|
||
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/1.20.1.json"));
|
||
QCOMPARE(Fixtures::readFile(m_game + "/mods/new.jar"), QByteArray("new"));
|
||
QCOMPARE(Fixtures::readFile(m_game + "/config/x/y.toml"), QByteArray("y=2"));
|
||
QCOMPARE(Fixtures::readFile(m_game + "/options.txt"), QByteArray("fov:70"));
|
||
|
||
QCOMPARE(entries.count(), 1);
|
||
QStringList applied = entries.constFirst().constFirst().toStringList();
|
||
applied.sort();
|
||
QCOMPARE(applied, QStringList({"config/x/y.toml", "mods/new.jar", "options.txt"}));
|
||
}
|
||
|
||
void applyPackRefusesLauncherFolders()
|
||
{
|
||
populateGame();
|
||
const QString pack = m_work + "/bad.zip";
|
||
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"}, {"versions/hack/hack.json", "{}"}}));
|
||
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
worker.applyPack(pack, m_game, {"mods/a.jar"}, {"versions"});
|
||
QCOMPARE(result(finished),
|
||
qMakePair(false, Loc::text("switch.error.packTouchesLauncher").arg("versions/hack/hack.json")));
|
||
// Проверка до любых изменений: ничего не удалено и не записано.
|
||
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
|
||
QVERIFY(!QFile::exists(m_game + "/mods/new.jar"));
|
||
}
|
||
|
||
void applyPackRefusesUnsafePaths()
|
||
{
|
||
const QString pack = m_work + "/slip.zip";
|
||
QVERIFY(Fixtures::makeZip(pack, {{"mods/../../../etc/x", "x"}}));
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
worker.applyPack(pack, m_game, {}, {});
|
||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.error.packBadPath").arg("mods/../../../etc/x")));
|
||
}
|
||
|
||
void applyPackCancel()
|
||
{
|
||
const QString pack = m_work + "/c.zip";
|
||
QVERIFY(Fixtures::makeZip(pack, {{"a.txt", "a"}}));
|
||
BuildArchiveWorker worker;
|
||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||
worker.requestCancel();
|
||
worker.applyPack(pack, m_game, {}, {});
|
||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.installCancelled")));
|
||
QVERIFY(!QFile::exists(m_game + "/a.txt"));
|
||
}
|
||
};
|
||
|
||
BILAUNCHER_TEST_MAIN(TestBuildArchiveWorker)
|
||
#include "tst_buildarchiveworker.moc"
|