mirror of
https://github.com/elyby/mojang-api.git
synced 2024-12-23 00:29:47 +05:30
Init
This commit is contained in:
commit
7211fbc190
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/vendor/
|
||||
/build/
|
||||
.phpunit.result.cache
|
||||
.php_cs.cache
|
5
.php_cs.dist
Normal file
5
.php_cs.dist
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
$finder = \PhpCsFixer\Finder::create()
|
||||
->in(__DIR__);
|
||||
return \Ely\CS\Config::create()
|
||||
->setFinder($finder);
|
34
.travis.yml
Normal file
34
.travis.yml
Normal file
@ -0,0 +1,34 @@
|
||||
language: php
|
||||
php:
|
||||
- 7.1
|
||||
- 7.2
|
||||
- 7.3
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- vendor
|
||||
- $HOME/.composer
|
||||
|
||||
env:
|
||||
global:
|
||||
- DEFAULT_COMPOSER_FLAGS="--optimize-autoloader --no-progress"
|
||||
- COMPOSER_NO_INTERACTION=1
|
||||
|
||||
before_script:
|
||||
- composer global show hirak/prestissimo -q || travis_retry composer global require $DEFAULT_COMPOSER_FLAGS hirak/prestissimo
|
||||
- travis_retry composer install
|
||||
- travis_retry phpenv rehash
|
||||
|
||||
stages:
|
||||
- Static Code Analysis
|
||||
- Test
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: Static Code Analysis
|
||||
php: 7.3
|
||||
script:
|
||||
- vendor/bin/php-cs-fixer fix -v --dry-run
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Ely.by <team@ely.by>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
62
README.md
Normal file
62
README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Mojang API
|
||||
|
||||
This package provides easy access to the Minecraft related API of Mojang.
|
||||
The library is built on the top of the [Guzzle HTTP client](https://github.com/guzzle/guzzle),
|
||||
has custom errors handler and automatic retry in case of problems with Mojang.
|
||||
|
||||
> Please note that this is not a complete implementation of all available APIs.
|
||||
If you don't find the method you need, [open Issue](https://github.com/elyby/mojang-api/issues/new)
|
||||
or [submit a PR](https://github.com/elyby/mojang-api/compare) with the implementation.
|
||||
|
||||
[![Latest Version on Packagist][ico-version]][link-packagist]
|
||||
[![Total Downloads][ico-downloads]][link-downloads]
|
||||
[![Software License][ico-license]](LICENSE.md)
|
||||
[![Build Status][ico-build-status]][link-build-status]
|
||||
|
||||
## Installation
|
||||
|
||||
To install, use composer:
|
||||
|
||||
```bash
|
||||
composer require ely/mojang-api
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To get the configured `Api` object right away, just use the static `create()` method:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$api = \Ely\Mojang\Api::create();
|
||||
$response = $api->usernameToUUID('erickskrauch');
|
||||
echo $response->getId();
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
$ ./vendor/bin/phpunit
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
|
||||
|
||||
## Credits
|
||||
|
||||
This package was designed and developed within the [Ely.by](http://ely.by) project team. We also thank all the
|
||||
[contributors](link-contributors) for their help.
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
|
||||
|
||||
[ico-version]: https://img.shields.io/packagist/v/ely/mojang-api.svg?style=flat-square
|
||||
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
|
||||
[ico-downloads]: https://img.shields.io/packagist/dt/ely/mojang-api.svg?style=flat-square
|
||||
[ico-build-status]: https://img.shields.io/travis/elyby/mojang-api/master.svg?style=flat-square
|
||||
|
||||
[link-packagist]: https://packagist.org/packages/ely/mojang-api
|
||||
[link-contributors]: ../../contributors
|
||||
[link-downloads]: https://packagist.org/packages/ely/mojang-api/stats
|
||||
[link-build-status]: https://travis-ci.org/elyby/mojang-api
|
35
composer.json
Normal file
35
composer.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "ely/mojang-api",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"type": "library",
|
||||
"authors": [
|
||||
{
|
||||
"name": "ErickSkrauch",
|
||||
"email": "erickskrauch@yandex.ru"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1.0",
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/guzzle": "^6.0.0",
|
||||
"ramsey/uuid": "^3.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ely/php-code-style": "^0.3.0",
|
||||
"phpunit/phpunit": "^8.0.0"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Ely\\Mojang\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Ely\\Mojang\\Test\\": "tests"
|
||||
}
|
||||
}
|
||||
}
|
2987
composer.lock
generated
Normal file
2987
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
phpunit.xml
Normal file
32
phpunit.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.0/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<logging>
|
||||
<log
|
||||
type="coverage-html"
|
||||
target="./build/coverage/html"
|
||||
/>
|
||||
<log
|
||||
type="coverage-clover"
|
||||
target="./build/coverage/log/coverage.xml"
|
||||
/>
|
||||
</logging>
|
||||
<testsuites>
|
||||
<testsuite name="Package Test Suite">
|
||||
<directory suffix=".php">./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">./</directory>
|
||||
<exclude>
|
||||
<directory suffix=".php">./vendor</directory>
|
||||
<directory suffix=".php">./tests</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
265
src/Api.php
Normal file
265
src/Api.php
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang;
|
||||
|
||||
use Ely\Mojang\Middleware\ResponseConverterMiddleware;
|
||||
use Ely\Mojang\Middleware\RetryMiddleware;
|
||||
use GuzzleHttp\Client as GuzzleClient;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class Api {
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $client;
|
||||
|
||||
public function __construct(ClientInterface $client) {
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $handler HTTP handler function to use with the stack. If no
|
||||
* handler is provided, the best handler for your
|
||||
* system will be utilized.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function create(callable $handler = null): self {
|
||||
$stack = HandlerStack::create($handler);
|
||||
// use after method because middleware executes in reverse order
|
||||
$stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
|
||||
$stack->push(RetryMiddleware::create(), 'retry');
|
||||
|
||||
return new static(new GuzzleClient([
|
||||
'handler' => $stack,
|
||||
'timeout' => 10,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
* @param int $atTime
|
||||
*
|
||||
* @return \Ely\Mojang\Response\ProfileInfo
|
||||
*
|
||||
* @throws \Ely\Mojang\Exception\MojangApiException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*
|
||||
* @url http://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time
|
||||
*/
|
||||
public function usernameToUUID(string $username, int $atTime = null): Response\ProfileInfo {
|
||||
$query = [];
|
||||
if ($atTime !== null) {
|
||||
$query['atTime'] = $atTime;
|
||||
}
|
||||
|
||||
$response = $this->getClient()->request('GET', "https://api.mojang.com/users/profiles/minecraft/{$username}", [
|
||||
'query' => $query,
|
||||
]);
|
||||
|
||||
$data = $this->decode($response->getBody()->getContents());
|
||||
|
||||
return Response\ProfileInfo::createFromResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uuid
|
||||
*
|
||||
* @return \Ely\Mojang\Response\ProfileResponse
|
||||
*
|
||||
* @throws \Ely\Mojang\Exception\MojangApiException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*
|
||||
* @url http://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
|
||||
*/
|
||||
public function uuidToTextures(string $uuid): Response\ProfileResponse {
|
||||
$response = $this->getClient()->request('GET', "https://sessionserver.mojang.com/session/minecraft/profile/{$uuid}", [
|
||||
'query' => [
|
||||
'unsigned' => false,
|
||||
],
|
||||
]);
|
||||
$body = $this->decode($response->getBody()->getContents());
|
||||
|
||||
return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to exchange username to the corresponding textures.
|
||||
*
|
||||
* @param string $username
|
||||
*
|
||||
* @return \Ely\Mojang\Response\ProfileResponse
|
||||
*
|
||||
* @throws GuzzleException
|
||||
* @throws \Ely\Mojang\Exception\MojangApiException
|
||||
*/
|
||||
public function usernameToTextures(string $username): Response\ProfileResponse {
|
||||
return $this->uuidToTextures($this->usernameToUUID($username)->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $login
|
||||
* @param string $password
|
||||
* @param string $clientToken
|
||||
*
|
||||
* @return \Ely\Mojang\Response\AuthenticateResponse
|
||||
*
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*
|
||||
* @url https://wiki.vg/Authentication#Authenticate
|
||||
*/
|
||||
public function authenticate(
|
||||
string $login,
|
||||
string $password,
|
||||
string $clientToken = null
|
||||
): Response\AuthenticateResponse {
|
||||
if ($clientToken === null) {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$clientToken = Uuid::uuid4()->toString();
|
||||
}
|
||||
|
||||
$response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
|
||||
'json' => [
|
||||
'username' => $login,
|
||||
'password' => $password,
|
||||
'clientToken' => $clientToken,
|
||||
'requestUser' => true,
|
||||
'agent' => [
|
||||
'name' => 'Minecraft',
|
||||
'version' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$body = $this->decode($response->getBody()->getContents());
|
||||
|
||||
return new Response\AuthenticateResponse(
|
||||
$body['accessToken'],
|
||||
$body['clientToken'],
|
||||
$body['availableProfiles'],
|
||||
$body['selectedProfile'],
|
||||
$body['user']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*
|
||||
* @url https://wiki.vg/Authentication#Validate
|
||||
*/
|
||||
public function validate(string $accessToken): bool {
|
||||
try {
|
||||
$response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
|
||||
'json' => [
|
||||
'accessToken' => $accessToken,
|
||||
],
|
||||
]);
|
||||
if ($response->getStatusCode() === 204) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception\ForbiddenException $e) {
|
||||
// Suppress exception and let it just exit below
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
* @param string $accountUuid
|
||||
* @param \Psr\Http\Message\StreamInterface|resource|string $skinContents
|
||||
* @param bool $isSlim
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*
|
||||
* @url https://wiki.vg/Mojang_API#Upload_Skin
|
||||
*/
|
||||
public function uploadSkin(string $accessToken, string $accountUuid, $skinContents, bool $isSlim): void {
|
||||
$this->getClient()->request('PUT', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
|
||||
'multipart' => [
|
||||
[
|
||||
'name' => 'file',
|
||||
'contents' => $skinContents,
|
||||
'filename' => 'char.png',
|
||||
],
|
||||
[
|
||||
'name' => 'model',
|
||||
'contents' => $isSlim ? 'slim' : '',
|
||||
],
|
||||
],
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $accessToken,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
* @param string $accountUuid
|
||||
* @param string $serverId
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*
|
||||
* @url https://wiki.vg/Protocol_Encryption#Client
|
||||
*/
|
||||
public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
|
||||
$this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
|
||||
'json' => [
|
||||
'accessToken' => $accessToken,
|
||||
'selectedProfile' => $accountUuid,
|
||||
'serverId' => $serverId,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
* @param string $serverId
|
||||
*
|
||||
* @return \Ely\Mojang\Response\ProfileResponse
|
||||
*
|
||||
* @throws \Ely\Mojang\Exception\NoContentException
|
||||
* @throws GuzzleException
|
||||
*
|
||||
* @url https://wiki.vg/Protocol_Encryption#Server
|
||||
*/
|
||||
public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
|
||||
$uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
|
||||
->withQuery(http_build_query([
|
||||
'username' => $username,
|
||||
'serverId' => $serverId,
|
||||
], '', '&', PHP_QUERY_RFC3986));
|
||||
$request = new Request('GET', $uri);
|
||||
$response = $this->getClient()->send($request);
|
||||
$rawBody = $response->getBody()->getContents();
|
||||
if (empty($rawBody)) {
|
||||
throw new Exception\NoContentException($request, $response);
|
||||
}
|
||||
|
||||
$body = $this->decode($rawBody);
|
||||
|
||||
return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ClientInterface
|
||||
*/
|
||||
protected function getClient(): ClientInterface {
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
private function decode(string $response): array {
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
}
|
20
src/Exception/ForbiddenException.php
Normal file
20
src/Exception/ForbiddenException.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Exception;
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ForbiddenException extends ClientException implements MojangApiException {
|
||||
|
||||
public function __construct(RequestInterface $request, ResponseInterface $response) {
|
||||
parent::__construct(
|
||||
'The request was executed with a non-existent or expired access token',
|
||||
$request,
|
||||
$response
|
||||
);
|
||||
}
|
||||
|
||||
}
|
10
src/Exception/MojangApiException.php
Normal file
10
src/Exception/MojangApiException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface MojangApiException extends Throwable {
|
||||
|
||||
}
|
16
src/Exception/NoContentException.php
Normal file
16
src/Exception/NoContentException.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Exception;
|
||||
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class NoContentException extends RequestException implements MojangApiException {
|
||||
|
||||
public function __construct(RequestInterface $request, ResponseInterface $response) {
|
||||
parent::__construct('No data were received in the response.', $request, $response);
|
||||
}
|
||||
|
||||
}
|
21
src/Exception/TooManyRequestsException.php
Normal file
21
src/Exception/TooManyRequestsException.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Exception;
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class TooManyRequestsException extends ClientException implements MojangApiException {
|
||||
|
||||
public function __construct(RequestInterface $request, ResponseInterface $response) {
|
||||
parent::__construct(
|
||||
'The request limit was exceeded. ' .
|
||||
'Read the documentation for the method requested to find out which RPS is allowed.',
|
||||
$request,
|
||||
$response
|
||||
);
|
||||
}
|
||||
|
||||
}
|
54
src/Middleware/ResponseConverterMiddleware.php
Normal file
54
src/Middleware/ResponseConverterMiddleware.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Middleware;
|
||||
|
||||
use Ely\Mojang\Exception;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ResponseConverterMiddleware {
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $nextHandler;
|
||||
|
||||
public function __construct(callable $nextHandler) {
|
||||
$this->nextHandler = $nextHandler;
|
||||
}
|
||||
|
||||
public function __invoke(RequestInterface $request, array $options): PromiseInterface {
|
||||
$fn = $this->nextHandler;
|
||||
/** @var PromiseInterface $promise */
|
||||
$promise = $fn($request, $options);
|
||||
|
||||
return $promise->then(static function($response) use ($request) {
|
||||
if ($response instanceof ResponseInterface) {
|
||||
$method = $request->getMethod();
|
||||
$statusCode = $response->getStatusCode();
|
||||
if ($method === 'GET' && $statusCode === 204) {
|
||||
throw new Exception\NoContentException($request, $response);
|
||||
}
|
||||
|
||||
if ($statusCode === 403) {
|
||||
throw new Exception\ForbiddenException($request, $response);
|
||||
}
|
||||
|
||||
if ($statusCode === 429) {
|
||||
throw new Exception\TooManyRequestsException($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
|
||||
public static function create(): callable {
|
||||
return static function(callable $handler): callable {
|
||||
return new static($handler);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
39
src/Middleware/RetryMiddleware.php
Normal file
39
src/Middleware/RetryMiddleware.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Middleware;
|
||||
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Middleware;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class RetryMiddleware {
|
||||
|
||||
public static function create(): callable {
|
||||
return Middleware::retry([static::class, 'shouldRetry']);
|
||||
}
|
||||
|
||||
public static function shouldRetry(
|
||||
int $retries,
|
||||
RequestInterface $request,
|
||||
?ResponseInterface $response,
|
||||
?GuzzleException $reason
|
||||
): bool {
|
||||
if ($retries >= 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($reason instanceof ConnectException) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($response !== null && (int)floor($response->getStatusCode() / 100) === 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
70
src/Response/AuthenticateResponse.php
Normal file
70
src/Response/AuthenticateResponse.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response;
|
||||
|
||||
class AuthenticateResponse {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $accessToken;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $clientToken;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $rawAvailableProfiles;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $rawSelectedProfile;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $rawUser;
|
||||
|
||||
public function __construct(
|
||||
string $accessToken,
|
||||
string $clientToken,
|
||||
array $availableProfiles,
|
||||
array $selectedProfile,
|
||||
array $user
|
||||
) {
|
||||
$this->accessToken = $accessToken;
|
||||
$this->clientToken = $clientToken;
|
||||
$this->rawAvailableProfiles = $availableProfiles;
|
||||
$this->rawSelectedProfile = $selectedProfile;
|
||||
$this->rawUser = $user;
|
||||
}
|
||||
|
||||
public function getAccessToken(): string {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
public function getClientToken(): string {
|
||||
return $this->clientToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ProfileInfo[]
|
||||
*/
|
||||
public function getAvailableProfiles(): array {
|
||||
return array_map([ProfileInfo::class, 'createFromResponse'], $this->rawAvailableProfiles);
|
||||
}
|
||||
|
||||
public function getSelectedProfile(): ProfileInfo {
|
||||
return ProfileInfo::createFromResponse($this->rawSelectedProfile);
|
||||
}
|
||||
|
||||
public function getUser(): AuthenticationResponseUserField {
|
||||
return new AuthenticationResponseUserField($this->rawUser['id'], $this->rawUser['properties']);
|
||||
}
|
||||
|
||||
}
|
30
src/Response/AuthenticationResponseUserField.php
Normal file
30
src/Response/AuthenticationResponseUserField.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response;
|
||||
|
||||
use Ely\Mojang\Response\Properties\Factory;
|
||||
|
||||
class AuthenticationResponseUserField {
|
||||
|
||||
private $id;
|
||||
|
||||
private $rawProperties;
|
||||
|
||||
public function __construct(string $id, array $rawProperties) {
|
||||
$this->id = $id;
|
||||
$this->rawProperties = $rawProperties;
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Ely\Mojang\Response\Properties\Property[]
|
||||
*/
|
||||
public function getProperties(): array {
|
||||
return array_map([Factory::class, 'createFromProp'], $this->rawProperties);
|
||||
}
|
||||
|
||||
}
|
72
src/Response/ProfileInfo.php
Normal file
72
src/Response/ProfileInfo.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response;
|
||||
|
||||
class ProfileInfo {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isLegacy;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isDemo;
|
||||
|
||||
public function __construct(string $id, string $name, bool $isLegacy = false, bool $isDemo = false) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->isLegacy = $isLegacy;
|
||||
$this->isDemo = $isDemo;
|
||||
}
|
||||
|
||||
public static function createFromResponse(array $response): self {
|
||||
return new static(
|
||||
$response['id'],
|
||||
$response['name'],
|
||||
$response['legacy'] ?? false,
|
||||
$response['demo'] ?? false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string user's uuid without dashes
|
||||
*/
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string username at the current time
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true means, that account not migrated into Mojang account
|
||||
*/
|
||||
public function isLegacy(): bool {
|
||||
return $this->isLegacy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true means, that account now in demo mode (not premium user)
|
||||
*/
|
||||
public function isDemo(): bool {
|
||||
return $this->isDemo;
|
||||
}
|
||||
|
||||
}
|
46
src/Response/ProfileResponse.php
Normal file
46
src/Response/ProfileResponse.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response;
|
||||
|
||||
use Ely\Mojang\Response\Properties\Factory;
|
||||
|
||||
class ProfileResponse {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $props;
|
||||
|
||||
public function __construct(string $id, string $name, array $rawProps) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->props = $rawProps;
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Ely\Mojang\Response\Properties\Property[]
|
||||
*/
|
||||
public function getProps(): array {
|
||||
return array_map([Factory::class, 'createFromProp'], $this->props);
|
||||
}
|
||||
|
||||
}
|
22
src/Response/properties/Factory.php
Normal file
22
src/Response/properties/Factory.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response\Properties;
|
||||
|
||||
class Factory {
|
||||
|
||||
private static $MAP = [
|
||||
'textures' => TexturesProperty::class,
|
||||
];
|
||||
|
||||
public static function createFromProp(array $prop): Property {
|
||||
$name = $prop['name'];
|
||||
if (isset(static::$MAP[$name])) {
|
||||
$className = static::$MAP[$name];
|
||||
return new $className($prop);
|
||||
}
|
||||
|
||||
return new Property($prop);
|
||||
}
|
||||
|
||||
}
|
31
src/Response/properties/Property.php
Normal file
31
src/Response/properties/Property.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response\Properties;
|
||||
|
||||
class Property {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
|
||||
public function __construct(array $prop) {
|
||||
$this->name = $prop['name'];
|
||||
$this->value = $prop['value'];
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getValue(): string {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
}
|
26
src/Response/properties/TexturesProperty.php
Normal file
26
src/Response/properties/TexturesProperty.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response\Properties;
|
||||
|
||||
class TexturesProperty extends Property {
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $signature;
|
||||
|
||||
public function __construct(array $prop) {
|
||||
parent::__construct($prop);
|
||||
$this->signature = $prop['signature'] ?? null;
|
||||
}
|
||||
|
||||
public function getTextures(): TexturesPropertyValue {
|
||||
return TexturesPropertyValue::createFromRawTextures($this->value);
|
||||
}
|
||||
|
||||
public function getSignature(): ?string {
|
||||
return $this->signature;
|
||||
}
|
||||
|
||||
}
|
90
src/Response/properties/TexturesPropertyValue.php
Normal file
90
src/Response/properties/TexturesPropertyValue.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response\Properties;
|
||||
|
||||
class TexturesPropertyValue {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $username;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $textures;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $timestamp;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $signatureRequired;
|
||||
|
||||
public function __construct(
|
||||
string $profileId,
|
||||
string $profileName,
|
||||
array $textures,
|
||||
int $timestamp,
|
||||
bool $signatureRequired = false
|
||||
) {
|
||||
$this->id = $profileId;
|
||||
$this->username = $profileName;
|
||||
$this->textures = $textures;
|
||||
$this->timestamp = (int)floor($timestamp / 1000);
|
||||
$this->signatureRequired = $signatureRequired;
|
||||
}
|
||||
|
||||
public static function createFromRawTextures(string $rawTextures): self {
|
||||
$decoded = json_decode(base64_decode($rawTextures), true);
|
||||
return new static(
|
||||
$decoded['profileId'],
|
||||
$decoded['profileName'],
|
||||
$decoded['textures'],
|
||||
$decoded['timestamp'],
|
||||
$decoded['signatureRequired'] ?? false
|
||||
);
|
||||
}
|
||||
|
||||
public function getProfileId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProfileName(): string {
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getTimestamp(): int {
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function isSignatureRequired(): bool {
|
||||
return $this->signatureRequired;
|
||||
}
|
||||
|
||||
public function getSkin(): ?TexturesPropertyValueSkin {
|
||||
if (!isset($this->textures['SKIN'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TexturesPropertyValueSkin::createFromTextures($this->textures['SKIN']);
|
||||
}
|
||||
|
||||
public function getCape(): ?TexturesPropertyValueCape {
|
||||
if (!isset($this->textures['CAPE'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TexturesPropertyValueCape($this->textures['CAPE']['url']);
|
||||
}
|
||||
|
||||
}
|
21
src/Response/properties/TexturesPropertyValueCape.php
Normal file
21
src/Response/properties/TexturesPropertyValueCape.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response\Properties;
|
||||
|
||||
class TexturesPropertyValueCape {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $url;
|
||||
|
||||
public function __construct(string $skinUrl) {
|
||||
$this->url = $skinUrl;
|
||||
}
|
||||
|
||||
public function getUrl(): string {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
}
|
36
src/Response/properties/TexturesPropertyValueSkin.php
Normal file
36
src/Response/properties/TexturesPropertyValueSkin.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Response\Properties;
|
||||
|
||||
class TexturesPropertyValueSkin {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isSlim;
|
||||
|
||||
public function __construct(string $skinUrl, bool $isSlim = false) {
|
||||
$this->url = $skinUrl;
|
||||
$this->isSlim = $isSlim;
|
||||
}
|
||||
|
||||
public static function createFromTextures(array $textures): self {
|
||||
$model = &$textures['metainfo']['model']; // ampersand to avoid notice about unexpected key
|
||||
return new static($textures['url'], $model === 'slim');
|
||||
}
|
||||
|
||||
public function getUrl(): string {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function isSlim(): bool {
|
||||
return $this->isSlim;
|
||||
}
|
||||
|
||||
}
|
347
tests/ApiTest.php
Normal file
347
tests/ApiTest.php
Normal file
@ -0,0 +1,347 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Test;
|
||||
|
||||
use Ely\Mojang\Api;
|
||||
use Ely\Mojang\Exception\NoContentException;
|
||||
use Ely\Mojang\Middleware\ResponseConverterMiddleware;
|
||||
use Ely\Mojang\Middleware\RetryMiddleware;
|
||||
use Ely\Mojang\Response\Properties\TexturesProperty;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Middleware;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use function GuzzleHttp\Psr7\parse_query;
|
||||
|
||||
class ApiTest extends TestCase {
|
||||
|
||||
/**
|
||||
* @var Api
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\Handler\MockHandler
|
||||
*/
|
||||
private $mockHandler;
|
||||
|
||||
/**
|
||||
* @var \Psr\Http\Message\RequestInterface[]
|
||||
*/
|
||||
private $history;
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->mockHandler = new MockHandler();
|
||||
$handlerStack = HandlerStack::create($this->mockHandler);
|
||||
$this->history = [];
|
||||
$handlerStack->push(Middleware::history($this->history), 'history');
|
||||
$handlerStack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_responses');
|
||||
$handlerStack->push(RetryMiddleware::create(), 'retry');
|
||||
$client = new Client(['handler' => $handlerStack]);
|
||||
$this->api = new Api($client);
|
||||
}
|
||||
|
||||
public function testCreate() {
|
||||
$this->assertInstanceOf(Api::class, Api::create());
|
||||
}
|
||||
|
||||
public function testUsernameToUuid() {
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'legacy' => false,
|
||||
]));
|
||||
|
||||
$result = $this->api->usernameToUUID('MockUsername');
|
||||
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$this->assertSame('https://api.mojang.com/users/profiles/minecraft/MockUsername', (string)$request->getUri());
|
||||
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $result->getId());
|
||||
$this->assertSame('MockUsername', $result->getName());
|
||||
$this->assertFalse($result->isLegacy());
|
||||
$this->assertFalse($result->isDemo());
|
||||
}
|
||||
|
||||
public function testUsernameToUuidWithAtParam() {
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'legacy' => false,
|
||||
]));
|
||||
|
||||
$this->api->usernameToUUID('MockUsername', 1553961511);
|
||||
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$this->assertSame(
|
||||
'https://api.mojang.com/users/profiles/minecraft/MockUsername?atTime=1553961511',
|
||||
(string)$request->getUri()
|
||||
);
|
||||
}
|
||||
|
||||
public function testUuidToTextures() {
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'properties' => [
|
||||
[
|
||||
'name' => 'textures',
|
||||
'value' => base64_encode(json_encode([
|
||||
'timestamp' => 1553961848860,
|
||||
'profileId' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'profileName' => 'MockUsername',
|
||||
'signatureRequired' => true,
|
||||
'textures' => [
|
||||
'SKIN' => [
|
||||
'url' => 'http://textures.minecraft.net/texture/292009a4925b58f02c77dadc3ecef07ea4c7472f64e0fdc32ce5522489362680',
|
||||
],
|
||||
'CAPE' => [
|
||||
'url' => 'http://textures.minecraft.net/texture/capePath',
|
||||
],
|
||||
],
|
||||
])),
|
||||
'signature' => 'mocked signature value',
|
||||
],
|
||||
],
|
||||
]));
|
||||
|
||||
$result = $this->api->uuidToTextures('86f6e3695b764412a29820cac1d4d0d6');
|
||||
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$this->assertSame(
|
||||
'https://sessionserver.mojang.com/session/minecraft/profile/86f6e3695b764412a29820cac1d4d0d6?unsigned=0',
|
||||
(string)$request->getUri()
|
||||
);
|
||||
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $result->getId());
|
||||
$this->assertSame('MockUsername', $result->getName());
|
||||
$props = $result->getProps();
|
||||
/** @var TexturesProperty $texturesProperty */
|
||||
$texturesProperty = $props[0];
|
||||
$this->assertInstanceOf(TexturesProperty::class, $texturesProperty);
|
||||
$this->assertSame('textures', $texturesProperty->getName());
|
||||
$this->assertSame('mocked signature value', $texturesProperty->getSignature());
|
||||
$textures = $texturesProperty->getTextures();
|
||||
$this->assertSame(1553961848, $textures->getTimestamp());
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $textures->getProfileId());
|
||||
$this->assertSame('MockUsername', $textures->getProfileName());
|
||||
$this->assertTrue($textures->isSignatureRequired());
|
||||
$this->assertNotNull($textures->getSkin());
|
||||
$this->assertSame(
|
||||
'http://textures.minecraft.net/texture/292009a4925b58f02c77dadc3ecef07ea4c7472f64e0fdc32ce5522489362680',
|
||||
$textures->getSkin()->getUrl()
|
||||
);
|
||||
$this->assertFalse($textures->getSkin()->isSlim());
|
||||
$this->assertSame('http://textures.minecraft.net/texture/capePath', $textures->getCape()->getUrl());
|
||||
}
|
||||
|
||||
public function testUsernameToTextures() {
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'legacy' => false,
|
||||
]));
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'properties' => [],
|
||||
]));
|
||||
|
||||
$this->api->usernameToTextures('MockUsername');
|
||||
/** @var \Psr\Http\Message\RequestInterface $request1 */
|
||||
/** @var \Psr\Http\Message\RequestInterface $request2 */
|
||||
[0 => ['request' => $request1], 1 => ['request' => $request2]] = $this->history;
|
||||
$this->assertSame('https://api.mojang.com/users/profiles/minecraft/MockUsername', (string)$request1->getUri());
|
||||
$this->assertStringStartsWith('https://sessionserver.mojang.com/session/minecraft/profile/86f6e3695b764412a29820cac1d4d0d6', (string)$request2->getUri());
|
||||
}
|
||||
|
||||
public function testAuthenticate() {
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'accessToken' => 'access token value',
|
||||
'clientToken' => 'client token value',
|
||||
'availableProfiles' => [
|
||||
[
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'legacy' => false,
|
||||
],
|
||||
],
|
||||
'selectedProfile' => [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'legacy' => false,
|
||||
],
|
||||
'user' => [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'properties' => [
|
||||
[
|
||||
'name' => 'preferredLanguage',
|
||||
'value' => 'en',
|
||||
],
|
||||
[
|
||||
'name' => 'twitch_access_token',
|
||||
'value' => 'twitch oauth token',
|
||||
],
|
||||
],
|
||||
],
|
||||
]));
|
||||
|
||||
$result = $this->api->authenticate('MockUsername', 'some password', 'client token value');
|
||||
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$this->assertSame('https://authserver.mojang.com/authenticate', (string)$request->getUri());
|
||||
|
||||
$this->assertSame('access token value', $result->getAccessToken());
|
||||
$this->assertSame('client token value', $result->getClientToken());
|
||||
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $result->getAvailableProfiles()[0]->getId());
|
||||
$this->assertSame('MockUsername', $result->getAvailableProfiles()[0]->getName());
|
||||
$this->assertFalse($result->getAvailableProfiles()[0]->isLegacy());
|
||||
$this->assertFalse($result->getAvailableProfiles()[0]->isDemo());
|
||||
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $result->getSelectedProfile()->getId());
|
||||
$this->assertSame('MockUsername', $result->getSelectedProfile()->getName());
|
||||
$this->assertFalse($result->getSelectedProfile()->isLegacy());
|
||||
$this->assertFalse($result->getSelectedProfile()->isDemo());
|
||||
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $result->getUser()->getId());
|
||||
|
||||
$this->assertSame('preferredLanguage', $result->getUser()->getProperties()[0]->getName());
|
||||
$this->assertSame('en', $result->getUser()->getProperties()[0]->getValue());
|
||||
$this->assertSame('twitch_access_token', $result->getUser()->getProperties()[1]->getName());
|
||||
$this->assertSame('twitch oauth token', $result->getUser()->getProperties()[1]->getValue());
|
||||
}
|
||||
|
||||
public function testAuthenticateWithNotSpecifiedClientToken() {
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'accessToken' => 'access token value',
|
||||
'clientToken' => 'client token value',
|
||||
'availableProfiles' => [],
|
||||
'selectedProfile' => [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'legacy' => false,
|
||||
],
|
||||
'user' => [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'properties' => [],
|
||||
],
|
||||
]));
|
||||
|
||||
$this->api->authenticate('MockUsername', 'some password');
|
||||
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$body = json_decode($request->getBody()->getContents(), true);
|
||||
// https://gist.github.com/johnelliott/cf77003f72f889abbc3f32785fa3df8d
|
||||
$this->assertRegExp('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $body['clientToken']);
|
||||
}
|
||||
|
||||
public function testValidateSuccessful() {
|
||||
$this->mockHandler->append(new Response(204));
|
||||
$this->assertTrue($this->api->validate('mocked access token'));
|
||||
}
|
||||
|
||||
public function testValidateInvalid() {
|
||||
$this->mockHandler->append(new Response(403));
|
||||
$this->assertFalse($this->api->validate('mocked access token'));
|
||||
}
|
||||
|
||||
public function testUploadSkinNotSlim() {
|
||||
$this->mockHandler->append(new Response(200));
|
||||
$this->api->uploadSkin('mocked access token', '86f6e3695b764412a29820cac1d4d0d6', 'skin contents', false);
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$this->assertSame('Bearer mocked access token', $request->getHeaderLine('Authorization'));
|
||||
$this->assertStringNotContainsString('slim', $request->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function testUploadSkinSlim() {
|
||||
$this->mockHandler->append(new Response(200));
|
||||
$this->api->uploadSkin('mocked access token', '86f6e3695b764412a29820cac1d4d0d6', 'skin contents', true);
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$this->assertStringContainsString('slim', $request->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function testJoinServer() {
|
||||
$this->mockHandler->append(new Response(200));
|
||||
$this->api->joinServer('mocked access token', '86f6e3695b764412a29820cac1d4d0d6', 'ad72fe1efe364e6eb78c644a9fba1d30');
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$params = json_decode($request->getBody()->getContents(), true);
|
||||
$this->assertSame('mocked access token', $params['accessToken']);
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $params['selectedProfile']);
|
||||
$this->assertSame('ad72fe1efe364e6eb78c644a9fba1d30', $params['serverId']);
|
||||
}
|
||||
|
||||
public function testHasJoinedServer() {
|
||||
$this->mockHandler->append($this->createResponse(200, [
|
||||
'id' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'name' => 'MockUsername',
|
||||
'properties' => [
|
||||
[
|
||||
'name' => 'textures',
|
||||
'value' => base64_encode(json_encode([
|
||||
'timestamp' => 1553961848860,
|
||||
'profileId' => '86f6e3695b764412a29820cac1d4d0d6',
|
||||
'profileName' => 'MockUsername',
|
||||
'signatureRequired' => true,
|
||||
'textures' => [
|
||||
'SKIN' => [
|
||||
'url' => 'http://textures.minecraft.net/texture/292009a4925b58f02c77dadc3ecef07ea4c7472f64e0fdc32ce5522489362680',
|
||||
],
|
||||
],
|
||||
])),
|
||||
'signature' => 'mocked signature value',
|
||||
],
|
||||
],
|
||||
]));
|
||||
$result = $this->api->hasJoinedServer('MockedUsername', 'ad72fe1efe364e6eb78c644a9fba1d30');
|
||||
/** @var \Psr\Http\Message\RequestInterface $request */
|
||||
$request = $this->history[0]['request'];
|
||||
$params = parse_query($request->getUri()->getQuery());
|
||||
$this->assertSame('MockedUsername', $params['username']);
|
||||
$this->assertSame('ad72fe1efe364e6eb78c644a9fba1d30', $params['serverId']);
|
||||
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $result->getId());
|
||||
$this->assertSame('MockUsername', $result->getName());
|
||||
$props = $result->getProps();
|
||||
/** @var TexturesProperty $texturesProperty */
|
||||
$texturesProperty = $props[0];
|
||||
$this->assertInstanceOf(TexturesProperty::class, $texturesProperty);
|
||||
$this->assertSame('textures', $texturesProperty->getName());
|
||||
$this->assertSame('mocked signature value', $texturesProperty->getSignature());
|
||||
$textures = $texturesProperty->getTextures();
|
||||
$this->assertSame(1553961848, $textures->getTimestamp());
|
||||
$this->assertSame('86f6e3695b764412a29820cac1d4d0d6', $textures->getProfileId());
|
||||
$this->assertSame('MockUsername', $textures->getProfileName());
|
||||
$this->assertTrue($textures->isSignatureRequired());
|
||||
$this->assertNotNull($textures->getSkin());
|
||||
$this->assertSame(
|
||||
'http://textures.minecraft.net/texture/292009a4925b58f02c77dadc3ecef07ea4c7472f64e0fdc32ce5522489362680',
|
||||
$textures->getSkin()->getUrl()
|
||||
);
|
||||
$this->assertFalse($textures->getSkin()->isSlim());
|
||||
$this->assertNull($textures->getCape());
|
||||
}
|
||||
|
||||
public function testHasJoinedServerEmptyResponse() {
|
||||
$this->mockHandler->append(new Response(200));
|
||||
$this->expectException(NoContentException::class);
|
||||
$this->api->hasJoinedServer('MockedUsername', 'ad72fe1efe364e6eb78c644a9fba1d30');
|
||||
}
|
||||
|
||||
private function createResponse(int $statusCode, array $response): ResponseInterface {
|
||||
return new Response($statusCode, ['content-type' => 'json'], json_encode($response));
|
||||
}
|
||||
|
||||
}
|
56
tests/Middleware/ResponseConverterMiddlewareTest.php
Normal file
56
tests/Middleware/ResponseConverterMiddlewareTest.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Test\Middleware;
|
||||
|
||||
use Ely\Mojang\Exception;
|
||||
use Ely\Mojang\Middleware\ResponseConverterMiddleware;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ResponseConverterMiddlewareTest extends TestCase {
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
* @dataProvider getResponses
|
||||
*/
|
||||
public function testInvoke(RequestInterface $request, ResponseInterface $response, string $expectedException) {
|
||||
$this->expectException($expectedException);
|
||||
$handler = new MockHandler([$response]);
|
||||
$middleware = new ResponseConverterMiddleware($handler);
|
||||
$middleware($request, [])->wait();
|
||||
}
|
||||
|
||||
public function getResponses(): iterable {
|
||||
yield [
|
||||
new Request('GET', 'http://localhost'),
|
||||
new Response(204, [], ''),
|
||||
Exception\NoContentException::class,
|
||||
];
|
||||
|
||||
yield [
|
||||
new Request('GET', 'http://localhost'),
|
||||
new Response(
|
||||
403,
|
||||
['Content-Type' => 'application/json'],
|
||||
'{"error":"ForbiddenOperationException","errorMessage":"Invalid token"}'
|
||||
),
|
||||
Exception\ForbiddenException::class,
|
||||
];
|
||||
|
||||
yield [
|
||||
new Request('GET', 'http://localhost'),
|
||||
new Response(
|
||||
429,
|
||||
['Content-Type' => 'application/json'],
|
||||
'{"error":"TooManyRequestsException","errorMessage":"The client has sent too many requests within a certain amount of time"}'
|
||||
),
|
||||
Exception\TooManyRequestsException::class,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
24
tests/Middleware/RetryMiddlewareTest.php
Normal file
24
tests/Middleware/RetryMiddlewareTest.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Test\Middleware;
|
||||
|
||||
use Ely\Mojang\Middleware\RetryMiddleware;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RetryMiddlewareTest extends TestCase {
|
||||
|
||||
public function testShouldRetry() {
|
||||
$r = new Request('GET', 'http://localhost');
|
||||
$this->assertFalse(RetryMiddleware::shouldRetry(0, $r, new Response(200), null), 'not retry on success response');
|
||||
$this->assertFalse(RetryMiddleware::shouldRetry(0, $r, new Response(403), null), 'not retry on client error');
|
||||
$this->assertTrue(RetryMiddleware::shouldRetry(0, $r, null, new ConnectException('', $r)), 'retry when network error happens');
|
||||
$this->assertTrue(RetryMiddleware::shouldRetry(0, $r, new Response(503), null), 'retry when 50x error 1 time');
|
||||
$this->assertTrue(RetryMiddleware::shouldRetry(1, $r, new Response(503), null), 'retry when 50x error 2 time');
|
||||
$this->assertFalse(RetryMiddleware::shouldRetry(2, $r, new Response(503), null), 'don\'t retry when 50x error 3 time');
|
||||
}
|
||||
|
||||
}
|
36
tests/Response/Properties/FactoryTest.php
Normal file
36
tests/Response/Properties/FactoryTest.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Test\Response\Properties;
|
||||
|
||||
use Ely\Mojang\Response\Properties\Factory;
|
||||
use Ely\Mojang\Response\Properties\Property;
|
||||
use Ely\Mojang\Response\Properties\TexturesProperty;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class FactoryTest extends TestCase {
|
||||
|
||||
/**
|
||||
* @param array $inputProps
|
||||
* @param string $expectedType
|
||||
*
|
||||
* @dataProvider getProps
|
||||
*/
|
||||
public function testCreate(array $inputProps, string $expectedType) {
|
||||
$this->assertInstanceOf($expectedType, Factory::createFromProp($inputProps));
|
||||
}
|
||||
|
||||
public function getProps(): iterable {
|
||||
yield [[
|
||||
'name' => 'textures',
|
||||
'value' => 'value',
|
||||
'signature' => '123',
|
||||
], TexturesProperty::class];
|
||||
|
||||
yield [[
|
||||
'name' => 'other',
|
||||
'value' => 'value',
|
||||
], Property::class];
|
||||
}
|
||||
|
||||
}
|
50
tests/Response/Properties/TexturesPropertyValueTest.php
Normal file
50
tests/Response/Properties/TexturesPropertyValueTest.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ely\Mojang\Test\Response\Properties;
|
||||
|
||||
use Ely\Mojang\Response\Properties\TexturesPropertyValue;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TexturesPropertyValueTest extends TestCase {
|
||||
|
||||
public function testGetSkin() {
|
||||
$object = new TexturesPropertyValue('', '', [
|
||||
'SKIN' => [
|
||||
'url' => 'skin url',
|
||||
],
|
||||
], 0);
|
||||
$this->assertNotNull($object->getSkin());
|
||||
$this->assertSame('skin url', $object->getSkin()->getUrl());
|
||||
$this->assertFalse($object->getSkin()->isSlim());
|
||||
|
||||
$object = new TexturesPropertyValue('', '', [
|
||||
'SKIN' => [
|
||||
'url' => 'skin url',
|
||||
'metainfo' => [
|
||||
'model' => 'slim',
|
||||
],
|
||||
],
|
||||
], 0);
|
||||
$this->assertNotNull($object->getSkin());
|
||||
$this->assertSame('skin url', $object->getSkin()->getUrl());
|
||||
$this->assertTrue($object->getSkin()->isSlim());
|
||||
|
||||
$object = new TexturesPropertyValue('', '', [], 0);
|
||||
$this->assertNull($object->getSkin());
|
||||
}
|
||||
|
||||
public function testGetCape() {
|
||||
$object = new TexturesPropertyValue('', '', [
|
||||
'CAPE' => [
|
||||
'url' => 'cape url',
|
||||
],
|
||||
], 0);
|
||||
$this->assertNotNull($object->getCape());
|
||||
$this->assertSame('cape url', $object->getCape()->getUrl());
|
||||
|
||||
$object = new TexturesPropertyValue('', '', [], 0);
|
||||
$this->assertNull($object->getCape());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user