1 parent
ebf8ff7869
commit
41599fa82f
67 files changed
+8775
-4
No files matched your search
@@ -0,0 +1,262 @@
|
||||
#include "testenv.h"
|
||||
#include "nettest.h"
|
||||
|
||||
#include "authservice.h"
|
||||
#include "localization.h"
|
||||
#include "msaauthservice.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QSignalSpy>
|
||||
|
||||
namespace {
|
||||
|
||||
const QString kToken = QStringLiteral("https://login.live.com/oauth20_token.srf");
|
||||
const QString kXbl = QStringLiteral("https://user.auth.xboxlive.com/user/authenticate");
|
||||
const QString kXsts = QStringLiteral("https://xsts.auth.xboxlive.com/xsts/authorize");
|
||||
const QString kMcLogin = QStringLiteral("https://api.minecraftservices.com/authentication/login_with_xbox");
|
||||
const QString kEntitlements = QStringLiteral("https://api.minecraftservices.com/entitlements/mcstore");
|
||||
const QString kProfile = QStringLiteral("https://api.minecraftservices.com/minecraft/profile");
|
||||
|
||||
QJsonObject jsonBody(const FakeRequest &request)
|
||||
{
|
||||
return QJsonDocument::fromJson(request.body).object();
|
||||
}
|
||||
|
||||
struct Captured
|
||||
{
|
||||
int calls = 0;
|
||||
AuthResult result;
|
||||
auto callback()
|
||||
{
|
||||
return [this](const AuthResult &r) { ++calls; result = r; };
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestMsaAuthService : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
FakeInternet m_net;
|
||||
|
||||
// Полностью рабочая цепочка Microsoft → Xbox → XSTS → Minecraft.
|
||||
void routeHappyPath()
|
||||
{
|
||||
m_net.route(kToken, [](const FakeRequest &request) {
|
||||
const QUrlQuery form = request.formItems();
|
||||
const QString grant = form.queryItemValue("grant_type");
|
||||
if (grant == QLatin1String("authorization_code") && form.queryItemValue("code") == QLatin1String("good-code"))
|
||||
return FakeResponse::json(QJsonObject{{"access_token", "ms-access"}, {"refresh_token", "ms-refresh-2"}});
|
||||
if (grant == QLatin1String("refresh_token") && form.queryItemValue("refresh_token") == QLatin1String("ms-refresh-1"))
|
||||
return FakeResponse::json(QJsonObject{{"access_token", "ms-access"}, {"refresh_token", "ms-refresh-3"}});
|
||||
return FakeResponse::json(QJsonObject{{"error", "invalid_grant"},
|
||||
{"error_description", "The provided value for the 'code' parameter is not valid."}}, 400);
|
||||
});
|
||||
m_net.route(kXbl, [](const FakeRequest &request) {
|
||||
const QJsonObject props = jsonBody(request).value("Properties").toObject();
|
||||
if (props.value("RpsTicket").toString() != QLatin1String("ms-access"))
|
||||
return FakeResponse::error(400);
|
||||
return FakeResponse::json(QJsonObject{{"Token", "xbl-token"}});
|
||||
});
|
||||
m_net.route(kXsts, [](const FakeRequest &request) {
|
||||
const QJsonObject props = jsonBody(request).value("Properties").toObject();
|
||||
if (props.value("UserTokens").toArray().first().toString() != QLatin1String("xbl-token"))
|
||||
return FakeResponse::error(400);
|
||||
const QJsonObject claims{{"xui", QJsonArray{QJsonObject{{"uhs", "user-hash"}, {"xid", "2535400000000000"}}}}};
|
||||
return FakeResponse::json(QJsonObject{{"Token", "xsts-token"}, {"DisplayClaims", claims}});
|
||||
});
|
||||
m_net.route(kMcLogin, [](const FakeRequest &request) {
|
||||
if (jsonBody(request).value("identityToken").toString() != QLatin1String("XBL3.0 x=user-hash;xsts-token"))
|
||||
return FakeResponse::error(401);
|
||||
return FakeResponse::json(QJsonObject{{"access_token", "mc-token"}, {"expires_in", 3600}});
|
||||
});
|
||||
m_net.route(kEntitlements, [](const FakeRequest &request) {
|
||||
if (request.header("authorization") != "Bearer mc-token")
|
||||
return FakeResponse::error(401);
|
||||
return FakeResponse::json(QJsonObject{{"items", QJsonArray{QJsonObject{{"name", "game_minecraft"}}}}});
|
||||
});
|
||||
m_net.route(kProfile, [](const FakeRequest &request) {
|
||||
if (request.header("authorization") != "Bearer mc-token")
|
||||
return FakeResponse::error(401);
|
||||
return FakeResponse::json(QJsonObject{{"id", "069a79f4-44e9-4726-a5be-fca90e38aaf5"}, {"name", "Notch"}});
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
routeHappyPath();
|
||||
}
|
||||
|
||||
void fullChainWithCode()
|
||||
{
|
||||
MsaAuthService service;
|
||||
QSignalSpy progress(&service, &MsaAuthService::progress);
|
||||
Captured c;
|
||||
const QDateTime before = QDateTime::currentDateTimeUtc();
|
||||
service.loginWithCode("good-code", c.callback());
|
||||
QVERIFY(wait(c));
|
||||
QVERIFY2(c.result.ok, qPrintable(c.result.error));
|
||||
QCOMPARE(c.result.playerName, QStringLiteral("Notch"));
|
||||
QCOMPARE(c.result.uuid, QStringLiteral("069a79f444e94726a5befca90e38aaf5"));
|
||||
QCOMPARE(c.result.accessToken, QStringLiteral("mc-token"));
|
||||
QCOMPARE(c.result.refreshToken, QStringLiteral("ms-refresh-2"));
|
||||
QCOMPARE(c.result.xuid, QStringLiteral("2535400000000000"));
|
||||
QCOMPARE(c.result.userType, QStringLiteral("msa"));
|
||||
QCOMPARE(c.result.clientToken, MsaAuthService::clientId());
|
||||
QVERIFY(!c.result.licenseMissing);
|
||||
const qint64 lifetime = before.secsTo(c.result.expiresAt);
|
||||
QVERIFY(lifetime > 3500 && lifetime <= 3600);
|
||||
QVERIFY(progress.count() >= 5);
|
||||
|
||||
// Порядок шагов и формат запроса токена.
|
||||
QCOMPARE(m_net.requestedUrls(), QStringList({kToken, kXbl, kXsts, kMcLogin, kEntitlements, kProfile}));
|
||||
const FakeRequest tokenRequest = m_net.requests().constFirst();
|
||||
QCOMPARE(tokenRequest.header("content-type"), QByteArray("application/x-www-form-urlencoded"));
|
||||
QCOMPARE(tokenRequest.formItems().queryItemValue("client_id"), MsaAuthService::clientId());
|
||||
QCOMPARE(jsonBody(m_net.requests().at(1)).value("RelyingParty").toString(), QStringLiteral("http://auth.xboxlive.com"));
|
||||
QCOMPARE(jsonBody(m_net.requests().at(2)).value("RelyingParty").toString(), QStringLiteral("rp://api.minecraftservices.com/"));
|
||||
QCOMPARE(jsonBody(m_net.requests().at(2)).value("Properties").toObject().value("SandboxId").toString(),
|
||||
QStringLiteral("RETAIL"));
|
||||
}
|
||||
|
||||
void refreshTokenLogin()
|
||||
{
|
||||
MsaAuthService service;
|
||||
Captured c;
|
||||
service.loginWithRefreshToken("ms-refresh-1", c.callback());
|
||||
QVERIFY(wait(c));
|
||||
QVERIFY2(c.result.ok, qPrintable(c.result.error));
|
||||
QCOMPARE(c.result.refreshToken, QStringLiteral("ms-refresh-3")); // новый refresh-токен сохраняется
|
||||
QCOMPARE(m_net.requests().constFirst().formItems().queryItemValue("grant_type"), QStringLiteral("refresh_token"));
|
||||
}
|
||||
|
||||
void emptyInputsFailImmediately()
|
||||
{
|
||||
MsaAuthService service;
|
||||
Captured noCode;
|
||||
service.loginWithCode(QString(), noCode.callback());
|
||||
QCOMPARE(noCode.calls, 1);
|
||||
QCOMPARE(noCode.result.error, Loc::text("auth.msa.error.noAuthCode"));
|
||||
Captured noRefresh;
|
||||
service.loginWithRefreshToken(QString(), noRefresh.callback());
|
||||
QCOMPARE(noRefresh.calls, 1);
|
||||
QCOMPARE(noRefresh.result.error, Loc::text("auth.msa.error.profileNotLinked"));
|
||||
QVERIFY(m_net.requests().isEmpty());
|
||||
}
|
||||
|
||||
void badCodeShowsMicrosoftDescription()
|
||||
{
|
||||
MsaAuthService service;
|
||||
Captured c;
|
||||
service.loginWithCode("bad-code", c.callback());
|
||||
QVERIFY(wait(c));
|
||||
QVERIFY(!c.result.ok);
|
||||
QCOMPARE(c.result.error, QStringLiteral("The provided value for the 'code' parameter is not valid."));
|
||||
QCOMPARE(m_net.requests().size(), 1);
|
||||
}
|
||||
|
||||
void xstsErrors_data()
|
||||
{
|
||||
QTest::addColumn<double>("xerr");
|
||||
QTest::addColumn<QString>("message");
|
||||
QTest::addColumn<QString>("expected");
|
||||
QTest::newRow("no xbox profile") << 2148916233.0 << "" << Loc::text("auth.msa.error.noXboxProfile");
|
||||
QTest::newRow("country") << 2148916235.0 << "" << Loc::text("auth.msa.error.countryBlocked");
|
||||
QTest::newRow("age 36") << 2148916236.0 << "" << Loc::text("auth.msa.error.ageVerification");
|
||||
QTest::newRow("age 37") << 2148916237.0 << "" << Loc::text("auth.msa.error.ageVerification");
|
||||
QTest::newRow("child") << 2148916238.0 << "" << Loc::text("auth.msa.error.childAccount");
|
||||
QTest::newRow("unknown with message") << 1.0 << "Something odd" << "Something odd";
|
||||
QTest::newRow("unknown without message") << 7.0 << "" << Loc::text("auth.msa.error.xboxRejected").arg(7);
|
||||
}
|
||||
|
||||
void xstsErrors()
|
||||
{
|
||||
QFETCH(double, xerr);
|
||||
QFETCH(QString, message);
|
||||
QFETCH(QString, expected);
|
||||
m_net.route(kXsts, FakeResponse::json(QJsonObject{{"XErr", xerr}, {"Message", message}}, 401));
|
||||
MsaAuthService service;
|
||||
Captured c;
|
||||
service.loginWithCode("good-code", c.callback());
|
||||
QVERIFY(wait(c));
|
||||
QVERIFY(!c.result.ok);
|
||||
QCOMPARE(c.result.error, expected);
|
||||
}
|
||||
|
||||
void stepFailures_data()
|
||||
{
|
||||
QTest::addColumn<QString>("url");
|
||||
QTest::addColumn<int>("status");
|
||||
QTest::addColumn<QByteArray>("body");
|
||||
QTest::addColumn<QString>("expected");
|
||||
QTest::newRow("token without access_token") << kToken << 200 << QByteArray("{}") << Loc::text("auth.msa.error.noAccessToken");
|
||||
QTest::newRow("xbl rejected") << kXbl << 400 << QByteArray("{}") << Loc::text("auth.msa.error.xboxTokenRejected").arg(400);
|
||||
QTest::newRow("xbl no token") << kXbl << 200 << QByteArray("{}") << Loc::text("auth.msa.error.xboxNoToken");
|
||||
QTest::newRow("xsts 500") << kXsts << 500 << QByteArray("{}") << Loc::text("auth.msa.error.xstsRejected").arg(500);
|
||||
QTest::newRow("xsts no user") << kXsts << 200 << QByteArray("{\"Token\":\"t\"}") << Loc::text("auth.msa.error.xstsNoUser");
|
||||
QTest::newRow("services rejected") << kMcLogin << 429 << QByteArray("{}") << Loc::text("auth.msa.error.servicesRejected").arg(429);
|
||||
QTest::newRow("services no token") << kMcLogin << 200 << QByteArray("{}") << Loc::text("auth.msa.error.servicesNoToken");
|
||||
QTest::newRow("profile 500") << kProfile << 500 << QByteArray("{}") << Loc::text("auth.msa.error.profileFetchFailed").arg(500);
|
||||
QTest::newRow("profile empty") << kProfile << 200 << QByteArray("{}") << Loc::text("auth.msa.error.servicesNoProfile");
|
||||
}
|
||||
|
||||
void stepFailures()
|
||||
{
|
||||
QFETCH(QString, url);
|
||||
QFETCH(int, status);
|
||||
QFETCH(QByteArray, body);
|
||||
QFETCH(QString, expected);
|
||||
m_net.route(url, FakeResponse::json(body, status));
|
||||
MsaAuthService service;
|
||||
Captured c;
|
||||
service.loginWithCode("good-code", c.callback());
|
||||
QVERIFY(wait(c));
|
||||
QVERIFY(!c.result.ok);
|
||||
QCOMPARE(c.result.error, expected);
|
||||
}
|
||||
|
||||
void licenseChecks()
|
||||
{
|
||||
// Профиля нет, покупки тоже нет — нет лицензии.
|
||||
m_net.route(kEntitlements, FakeResponse::json(QJsonObject{{"items", QJsonArray{}}}));
|
||||
m_net.route(kProfile, FakeResponse::json("{}", 404));
|
||||
MsaAuthService service;
|
||||
Captured noLicense;
|
||||
service.loginWithCode("good-code", noLicense.callback());
|
||||
QVERIFY(wait(noLicense));
|
||||
QVERIFY(!noLicense.result.ok);
|
||||
QVERIFY(noLicense.result.licenseMissing);
|
||||
QCOMPARE(noLicense.result.error, Loc::text("auth.msa.error.noLicence"));
|
||||
|
||||
// Покупка есть, но профиль (ник) ещё не создан.
|
||||
m_net.route(kEntitlements, FakeResponse::json(QJsonObject{{"items", QJsonArray{QJsonObject{{"name", "x"}}}}}));
|
||||
Captured noProfile;
|
||||
service.loginWithCode("good-code", noProfile.callback());
|
||||
QVERIFY(wait(noProfile));
|
||||
QVERIFY(noProfile.result.licenseMissing);
|
||||
QCOMPARE(noProfile.result.error, Loc::text("auth.msa.error.noProfileYet"));
|
||||
}
|
||||
|
||||
void transportErrorAtMicrosoft()
|
||||
{
|
||||
FakeResponse drop;
|
||||
drop.drop = true;
|
||||
m_net.route(kToken, drop);
|
||||
MsaAuthService service;
|
||||
Captured c;
|
||||
service.loginWithCode("good-code", c.callback());
|
||||
QVERIFY(wait(c));
|
||||
QVERIFY(c.result.error.startsWith(Loc::text("auth.msa.error.microsoftUnreachable").section(u'%', 0, 0)));
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestMsaAuthService)
|
||||
#include "tst_msaauthservice.moc"
|
||||
Reference in new issue
Block a user