1 parent
ebf8ff7869
commit
41599fa82f
67 files changed
+8775
-4
No files matched your search
@@ -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"
|
||||
Reference in new issue
Block a user