new unit tests, hehe 67
tests / linux (push) Failing after 2m18s
tests / windows (push) Skipped
tests / macos (push) Skipped
tests / package-smoke-linux (push) Skipped
tests / package-smoke-windows (push) Skipped
tests / package-smoke-macos (push) Skipped

Signed-off-by: galeon <dertiellis@gmail.com>
This commit is contained in:
galeon committed 2026-10-12 13:14:08 +03:00
1 parent ebf8ff7869
commit 41599fa82f
67 files changed
+8775 -4

No files matched your search

+298
View File
@@ -0,0 +1,298 @@
#include "testenv.h"
#include "fixtures.h"
#include "nettest.h"
#include "authservice.h"
#include "localization.h"
#include <QJsonDocument>
#include <QSignalSpy>
namespace {
const QString kAuth = QStringLiteral("https://authserver.ely.by/auth/authenticate");
const QString kValidate = QStringLiteral("https://authserver.ely.by/auth/validate");
const QString kRefresh = QStringLiteral("https://authserver.ely.by/auth/refresh");
const QString kInjectorManifest = QStringLiteral("https://authlib-injector.yushi.moe/artifact/latest.json");
const QString kInjectorJar = QStringLiteral("https://authlib-injector.yushi.moe/artifact/53/authlib-injector-1.2.5.jar");
QJsonObject body(const FakeRequest &request)
{
return QJsonDocument::fromJson(request.body).object();
}
QJsonObject profileResponse(const QString &token, const QString &clientToken)
{
return QJsonObject{{"accessToken", token},
{"clientToken", clientToken},
{"selectedProfile", QJsonObject{{"id", "ffffffff-ffff-ffff-ffff-ffffffffffff"},
{"name", "ElyPlayer"}}}};
}
// Сервер Ely.by в миниатюре: пароль «secret», у пользователя «2fa» — ещё код 123456.
FakeResponse elyAuthenticate(const FakeRequest &request)
{
const QJsonObject in = body(request);
const QString user = in.value("username").toString();
const QString password = in.value("password").toString();
if (user == QLatin1String("2fa")) {
if (password == QLatin1String("secret:123456"))
return FakeResponse::json(profileResponse("token-2fa", in.value("clientToken").toString()));
if (password == QLatin1String("secret"))
return FakeResponse::json(QJsonObject{{"error", "ForbiddenOperationException"},
{"errorMessage", "Account protected with two factor auth."}}, 401);
}
if (password == QLatin1String("secret"))
return FakeResponse::json(profileResponse("token-" + user, in.value("clientToken").toString()));
return FakeResponse::json(QJsonObject{{"error", "ForbiddenOperationException"},
{"errorMessage", "Invalid credentials. Invalid username or password."}}, 401);
}
struct Captured
{
int calls = 0;
AuthResult result;
auto callback()
{
return [this](const AuthResult &r) { ++calls; result = r; };
}
};
} // namespace
class TestAuthService : public QObject
{
Q_OBJECT
FakeInternet m_net;
QString m_dir;
static bool wait(Captured &c) { return QTest::qWaitFor([&] { return c.calls > 0; }, 15000); }
private slots:
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
void init()
{
m_net.reset();
m_net.route(kAuth, elyAuthenticate);
m_dir = TestEnv::freshDir(QStringLiteral("injector"));
}
void passwordLogin()
{
AuthService service;
QSignalSpy progress(&service, &AuthService::progress);
Captured c;
service.loginElyBy("steve", "secret", "client-1", QString(), c.callback());
QVERIFY(wait(c));
QVERIFY2(c.result.ok, qPrintable(c.result.error));
QCOMPARE(c.result.playerName, QStringLiteral("ElyPlayer"));
QCOMPARE(c.result.uuid, QStringLiteral("ffffffffffffffffffffffffffffffff")); // без дефисов
QCOMPARE(c.result.accessToken, QStringLiteral("token-steve"));
QCOMPARE(c.result.clientToken, QStringLiteral("client-1"));
QCOMPARE(c.result.userType, QStringLiteral("ELYBY"));
QCOMPARE(progress.count(), 1);
const QJsonObject sent = body(m_net.requests().constFirst());
QCOMPARE(sent.value("username").toString(), QStringLiteral("steve"));
QCOMPARE(sent.value("clientToken").toString(), QStringLiteral("client-1"));
QCOMPARE(sent.value("requestUser").toBool(), true);
QCOMPARE(m_net.requests().constFirst().header("content-type"), QByteArray("application/json"));
}
void emptyClientTokenIsGenerated()
{
AuthService service;
Captured c;
service.loginElyBy("steve", "secret", QString(), QString(), c.callback());
QVERIFY(wait(c));
QVERIFY(c.result.ok);
QVERIFY(!c.result.clientToken.isEmpty());
QCOMPARE(body(m_net.requests().constFirst()).value("clientToken").toString(), c.result.clientToken);
}
void wrongPasswordAndEmptyPassword()
{
AuthService service;
Captured wrong;
service.loginElyBy("steve", "nope", "c", QString(), wrong.callback());
QVERIFY(wait(wrong));
QVERIFY(!wrong.result.ok);
QVERIFY(!wrong.result.twoFactorRequired);
QCOMPARE(wrong.result.error, QStringLiteral("Invalid credentials. Invalid username or password."));
Captured empty;
service.loginElyBy("steve", QString(), "c", QString(), empty.callback());
QCOMPARE(empty.calls, 1); // сразу, без сети
QCOMPARE(empty.result.error, Loc::text("auth.ely.error.noPassword"));
QCOMPARE(m_net.requests().size(), 1);
}
void twoFactorFlow()
{
AuthService service;
Captured first;
service.loginElyBy("2fa", "secret", "c", QString(), first.callback());
QVERIFY(wait(first));
QVERIFY(!first.result.ok);
QVERIFY(first.result.twoFactorRequired);
QCOMPARE(first.result.error, Loc::text("auth.ely.error.twoFactor"));
Captured second;
service.loginElyByWithTotp("2fa", "secret", "123456", "c", second.callback());
QVERIFY(wait(second));
QVERIFY2(second.result.ok, qPrintable(second.result.error));
QCOMPARE(second.result.accessToken, QStringLiteral("token-2fa"));
// Код уходит через двоеточие после пароля — так требует Ely.by.
QCOMPARE(body(m_net.requests().constLast()).value("password").toString(), QStringLiteral("secret:123456"));
}
void validTokenSkipsPassword()
{
m_net.route(kValidate, FakeResponse::json("{}"));
AuthService service;
Captured c;
service.loginElyBy("steve", "secret", "c", "stored-token", c.callback());
QVERIFY(wait(c));
QVERIFY(c.result.ok);
QCOMPARE(c.result.accessToken, QStringLiteral("stored-token"));
QCOMPARE(m_net.hits(kAuth), 0);
QCOMPARE(body(m_net.requests().constFirst()).value("accessToken").toString(), QStringLiteral("stored-token"));
}
void expiredTokenIsRefreshed()
{
m_net.route(kValidate, FakeResponse::json(QJsonObject{{"error", "ForbiddenOperationException"}}, 401));
m_net.route(kRefresh, [](const FakeRequest &request) {
const QJsonObject in = body(request);
return FakeResponse::json(profileResponse("refreshed-" + in.value("accessToken").toString(),
in.value("clientToken").toString()));
});
AuthService service;
Captured c;
service.loginElyBy("steve", "secret", "c", "old", c.callback());
QVERIFY(wait(c));
QVERIFY(c.result.ok);
QCOMPARE(c.result.accessToken, QStringLiteral("refreshed-old"));
QCOMPARE(c.result.playerName, QStringLiteral("ElyPlayer"));
QCOMPARE(m_net.hits(kAuth), 0);
}
void failedRefreshFallsBackToPassword()
{
m_net.route(kValidate, FakeResponse::json("{}", 401));
m_net.route(kRefresh, FakeResponse::json("{}", 401));
AuthService service;
Captured c;
service.loginElyBy("steve", "secret", "c", "old", c.callback());
QVERIFY(wait(c));
QVERIFY(c.result.ok);
QCOMPARE(c.result.accessToken, QStringLiteral("token-steve"));
QCOMPARE(m_net.hits(kAuth), 1);
}
void transportErrorIsReported()
{
FakeResponse drop;
drop.drop = true;
m_net.route(kAuth, drop);
AuthService service;
Captured c;
service.loginElyBy("steve", "secret", "c", QString(), c.callback());
QVERIFY(wait(c));
QVERIFY(!c.result.ok);
QVERIFY(c.result.error.startsWith(Loc::text("auth.ely.error.unreachable").section(u'%', 0, 0)));
}
void responseWithoutProfileIsError()
{
m_net.route(kAuth, FakeResponse::json(QJsonObject{{"accessToken", "t"}}));
AuthService service;
Captured c;
service.loginElyBy("steve", "secret", "c", QString(), c.callback());
QVERIFY(wait(c));
QVERIFY(!c.result.ok);
QCOMPARE(c.result.error, Loc::text("auth.ely.error.noGameProfile"));
}
void serverErrorWithoutMessageShowsCode()
{
m_net.route(kAuth, FakeResponse::json("{}", 500));
AuthService service;
Captured c;
service.loginElyBy("steve", "secret", "c", QString(), c.callback());
QVERIFY(wait(c));
QCOMPARE(c.result.error, Loc::text("auth.error.serverCode").arg(500));
}
// --- authlib-injector -------------------------------------------------------
void injectorDownloadVerifyAndCache()
{
const QByteArray jar = "PK\x03\x04 fake authlib-injector";
m_net.route(kInjectorManifest, FakeResponse::json(QJsonObject{
{"build_number", 53}, {"version", "1.2.5"},
{"download_url", kInjectorJar},
{"checksums", QJsonObject{{"sha256", Fixtures::sha256(jar)}}}}));
m_net.route(kInjectorJar, FakeResponse::bytes(jar));
AuthService service;
QString path, error;
int calls = 0;
service.ensureAuthlibInjector(m_dir, [&](const QString &p, const QString &e) { ++calls; path = p; error = e; });
QTRY_COMPARE_WITH_TIMEOUT(calls, 1, 15000);
QVERIFY2(error.isEmpty(), qPrintable(error));
QCOMPARE(path, m_dir + "/authlib-injector.jar");
QCOMPARE(Fixtures::readFile(path), jar);
// Второй раз — сразу из файла.
calls = 0;
service.ensureAuthlibInjector(m_dir, [&](const QString &p, const QString &e) { ++calls; path = p; error = e; });
QCOMPARE(calls, 1);
QCOMPARE(m_net.hits(kInjectorJar), 1);
}
void injectorChecksumMismatch()
{
m_net.route(kInjectorManifest, FakeResponse::json(QJsonObject{
{"download_url", kInjectorJar},
{"checksums", QJsonObject{{"sha256", QString(64, u'0')}}}}));
m_net.route(kInjectorJar, FakeResponse::bytes("tampered"));
AuthService service;
QString path, error;
int calls = 0;
service.ensureAuthlibInjector(m_dir, [&](const QString &p, const QString &e) { ++calls; path = p; error = e; });
QTRY_COMPARE_WITH_TIMEOUT(calls, 1, 15000);
QCOMPARE(error, Loc::text("auth.ely.error.injectorChecksum"));
QVERIFY(path.isEmpty());
QVERIFY(!QFile::exists(m_dir + "/authlib-injector.jar"));
}
void injectorManifestProblems()
{
AuthService service;
QString error;
int calls = 0;
const auto cb = [&](const QString &, const QString &e) { ++calls; error = e; };
m_net.route(kInjectorManifest, FakeResponse::json(QJsonObject{{"version", "1.2.5"}}));
service.ensureAuthlibInjector(m_dir, cb);
QTRY_COMPARE_WITH_TIMEOUT(calls, 1, 15000);
QCOMPARE(error, Loc::text("auth.ely.error.injectorNoUrl"));
m_net.route(kInjectorManifest, FakeResponse::error(503));
service.ensureAuthlibInjector(m_dir, cb);
QTRY_COMPARE_WITH_TIMEOUT(calls, 2, 15000);
QVERIFY(error.startsWith(Loc::text("auth.ely.error.injectorInfoFailed").section(u'%', 0, 0)));
m_net.route(kInjectorManifest, FakeResponse::json(QJsonObject{{"download_url", kInjectorJar}}));
m_net.route(kInjectorJar, FakeResponse::error(404));
service.ensureAuthlibInjector(m_dir, cb);
QTRY_COMPARE_WITH_TIMEOUT(calls, 3, 15000);
QVERIFY(error.startsWith(Loc::text("auth.ely.error.injectorDownloadFailed").section(u'%', 0, 0)));
}
};
BILAUNCHER_TEST_MAIN(TestAuthService)
#include "tst_authservice.moc"