Merge branch 'valentin-pazushko-master'

This commit is contained in:
ErickSkrauch 2020-06-10 16:34:17 +03:00
commit 987afb244b
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E
6 changed files with 319 additions and 3 deletions

View File

@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- [Check if security questions are needed](https://wiki.vg/Mojang_API#Check_if_security_questions_are_needed) endpoint.
- [Get list of questions](https://wiki.vg/Mojang_API#Get_list_of_questions) endpoint.
- [Send back the answers](https://wiki.vg/Mojang_API#Send_back_the_answers) endpoint.
- [Statistics](https://wiki.vg/Mojang_API#Statistics) endpoint.
### Changed
- Changed the threshold value for [Playernames -> UUIDs](https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs) endpoint
from `100` to `10`.
## [0.2.0] - 2019-05-07 ## [0.2.0] - 2019-05-07
### Added ### Added

View File

@ -6,6 +6,7 @@ namespace Ely\Mojang;
use DateTime; use DateTime;
use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\ResponseConverterMiddleware;
use Ely\Mojang\Middleware\RetryMiddleware; use Ely\Mojang\Middleware\RetryMiddleware;
use Ely\Mojang\Response\QuestionResponse;
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
@ -150,8 +151,8 @@ class Api {
} }
} }
if (count($names) > 100) { if (count($names) > 10) {
throw new InvalidArgumentException('You cannot request more than 100 names per request'); throw new InvalidArgumentException('You cannot request more than 10 names per request');
} }
$response = $this->getClient()->request('POST', 'https://api.mojang.com/profiles/minecraft', [ $response = $this->getClient()->request('POST', 'https://api.mojang.com/profiles/minecraft', [
@ -452,6 +453,88 @@ class Api {
return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']); return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
} }
/**
* @param string $accessToken
* @throws GuzzleException
*
* @url https://wiki.vg/Mojang_API#Check_if_security_questions_are_needed
*/
public function isSecurityQuestionsNeeded(string $accessToken): void {
$request = new Request(
'GET',
'https://api.mojang.com/user/security/location',
['Authorization' => 'Bearer ' . $accessToken]
);
$response = $this->getClient()->send($request);
$rawBody = $response->getBody()->getContents();
if (!empty($rawBody)) {
$body = $this->decode($rawBody);
throw new Exception\OperationException($body['errorMessage'], $request, $response);
}
}
/**
* @param string $accessToken
* @return array
* @throws GuzzleException
*
* @url https://wiki.vg/Mojang_API#Get_list_of_questions
*/
public function questions(string $accessToken): array {
$request = new Request(
'GET',
'https://api.mojang.com/user/security/challenges',
['Authorization' => 'Bearer ' . $accessToken]
);
$response = $this->getClient()->send($request);
$result = [];
$body = $this->decode($response->getBody()->getContents());
foreach ($body as $question) {
$result[] = new QuestionResponse($question['question']['id'], $question['question']['question'], $question['answer']['id']);
}
return $result;
}
/**
* @param string $accessToken
* @param array $answers
* @throws GuzzleException
* @return bool
*
* @url https://wiki.vg/Mojang_API#Send_back_the_answers
*/
public function answer(string $accessToken, array $answers): bool {
$request = new Request(
'POST',
'https://api.mojang.com/user/security/location',
['Authorization' => 'Bearer ' . $accessToken],
json_encode($answers)
);
$response = $this->getClient()->send($request);
$rawBody = $response->getBody()->getContents();
return empty($rawBody);
}
/**
* @param array $metricKeys
* @return Response\StatisticsResponse
* @throws GuzzleException
*
* @url https://wiki.vg/Mojang_API#Statistics
*/
public function statistics(array $metricKeys) {
$response = $this->getClient()->request('POST', 'https://api.mojang.com/orders/statistics', [
'json' => [
'metricKeys' => $metricKeys,
],
]);
$body = $this->decode($response->getBody()->getContents());
return new Response\StatisticsResponse($body['total'], $body['last24h'], $body['saleVelocityPerSeconds']);
}
/** /**
* @return ClientInterface * @return ClientInterface
*/ */

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Ely\Mojang\Exception;
use GuzzleHttp\Exception\ClientException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class OperationException extends ClientException implements MojangApiException {
public function __construct(string $message, RequestInterface $request, ResponseInterface $response) {
parent::__construct($message, $request, $response);
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Ely\Mojang\Response;
class QuestionResponse {
/**
* @var int
*/
private $questionId;
/**
* @var string
*/
private $question;
/**
* @var int
*/
private $answerId;
public function __construct(int $questionId, string $question, int $answerId) {
$this->questionId = $questionId;
$this->question = $question;
$this->answerId = $answerId;
}
public function getQuestionId(): int {
return $this->questionId;
}
public function getQuestion(): string {
return $this->question;
}
public function getAnswerId(): int {
return $this->answerId;
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Ely\Mojang\Response;
class StatisticsResponse {
/**
* @var int
*/
private $total;
/**
* @var int
*/
private $last24h;
/**
* @var float
*/
private $saleVelocityPerSeconds;
public function __construct(int $total, int $last24h, float $saleVelocityPerSeconds) {
$this->total = $total;
$this->last24h = $last24h;
$this->saleVelocityPerSeconds = $saleVelocityPerSeconds;
}
public function getTotal(): int {
return $this->total;
}
public function getLast24H(): int {
return $this->last24h;
}
public function getSaleVelocityPerSeconds(): float {
return $this->saleVelocityPerSeconds;
}
}

View File

@ -6,12 +6,14 @@ namespace Ely\Mojang\Test;
use Ely\Mojang\Api; use Ely\Mojang\Api;
use Ely\Mojang\Exception\ForbiddenException; use Ely\Mojang\Exception\ForbiddenException;
use Ely\Mojang\Exception\NoContentException; use Ely\Mojang\Exception\NoContentException;
use Ely\Mojang\Exception\OperationException;
use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\ResponseConverterMiddleware;
use Ely\Mojang\Middleware\RetryMiddleware; use Ely\Mojang\Middleware\RetryMiddleware;
use Ely\Mojang\Response\ApiStatus; use Ely\Mojang\Response\ApiStatus;
use Ely\Mojang\Response\NameHistoryItem; use Ely\Mojang\Response\NameHistoryItem;
use Ely\Mojang\Response\ProfileInfo; use Ely\Mojang\Response\ProfileInfo;
use Ely\Mojang\Response\Properties\TexturesProperty; use Ely\Mojang\Response\Properties\TexturesProperty;
use Ely\Mojang\Response\QuestionResponse;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Handler\MockHandler;
@ -309,7 +311,7 @@ class ApiTest extends TestCase {
} }
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('You cannot request more than 100 names per request'); $this->expectExceptionMessage('You cannot request more than 10 names per request');
$this->api->playernamesToUuids($names); $this->api->playernamesToUuids($names);
} }
@ -609,6 +611,130 @@ class ApiTest extends TestCase {
$this->assertInstanceOf(ClientInterface::class, $child->getDefaultClient()); $this->assertInstanceOf(ClientInterface::class, $child->getDefaultClient());
} }
public function testIsSecurityQuestionsNeeded() {
$this->mockHandler->append(new Response(204));
$this->expectException(NoContentException::class);
$this->api->isSecurityQuestionsNeeded('mocked access token');
}
public function testIsSecurityQuestionsNeededOperationException() {
$this->mockHandler->append($this->createResponse(200, [
'error' => 'ForbiddenOperationException',
'errorMessage' => 'Current IP is not secured',
]));
$this->expectException(OperationException::class);
$this->api->isSecurityQuestionsNeeded('mocked access token');
}
public function testQuestions() {
$this->mockHandler->append($this->createResponse(200, [
[
'answer' => [
'id' => 123,
],
'question' => [
'id' => 1,
'question' => 'What is your favorite pet\'s name?',
],
],
[
'answer' => [
'id' => 456,
],
'question' => [
'id' => 2,
'question' => 'What is your favorite movie?',
],
],
[
'answer' => [
'id' => 789,
],
'question' => [
'id' => 3,
'question' => 'What is your favorite author\'s last name?',
],
],
]));
$result = $this->api->questions('mocked access token');
/** @var \Psr\Http\Message\RequestInterface $request */
$request = $this->history[0]['request'];
$this->assertSame('Bearer mocked access token', $request->getHeaderLine('Authorization'));
foreach ($result as $question) {
$this->assertInstanceOf(QuestionResponse::class, $question);
}
/** @var QuestionResponse $firstQuestion */
$firstQuestion = $result[0];
$this->assertSame(123, $firstQuestion->getAnswerId());
$this->assertSame(1, $firstQuestion->getQuestionId());
$this->assertSame('What is your favorite pet\'s name?', $firstQuestion->getQuestion());
/** @var QuestionResponse $secondQuestion */
$secondQuestion = $result[1];
$this->assertSame(456, $secondQuestion->getAnswerId());
$this->assertSame(2, $secondQuestion->getQuestionId());
$this->assertSame('What is your favorite movie?', $secondQuestion->getQuestion());
/** @var QuestionResponse $thirdQuestion */
$thirdQuestion = $result[2];
$this->assertSame(789, $thirdQuestion->getAnswerId());
$this->assertSame(3, $thirdQuestion->getQuestionId());
$this->assertSame('What is your favorite author\'s last name?', $thirdQuestion->getQuestion());
}
public function testAnswerOperationException() {
$this->mockHandler->append($this->createResponse(200, [
'error' => 'ForbiddenOperationException',
'errorMessage' => 'At least one answer was incorrect',
]));
$result = $this->api->answer('mocked access token', [
[
'id' => 123,
'answer' => 'foo',
],
[
'id' => 456,
'answer' => 'bar',
],
]);
$this->assertFalse($result);
}
public function testAnswer() {
$this->mockHandler->append(new Response(204));
$this->expectException(NoContentException::class);
$this->api->isSecurityQuestionsNeeded('mocked access token');
}
public function testStatistics() {
$this->mockHandler->append($this->createResponse(200, [
'total' => 145,
'last24h' => 13,
'saleVelocityPerSeconds' => 1.32,
]));
$result = $this->api->statistics([
'item_sold_minecraft',
'prepaid_card_redeemed_minecraft',
'item_sold_cobalt',
'item_sold_scrolls',
]);
/** @var \Psr\Http\Message\RequestInterface $request */
$request = $this->history[0]['request'];
$params = json_decode($request->getBody()->getContents(), true);
$this->assertSame([
'item_sold_minecraft',
'prepaid_card_redeemed_minecraft',
'item_sold_cobalt',
'item_sold_scrolls',
], $params['metricKeys']);
$this->assertSame(145, $result->getTotal());
$this->assertSame(13, $result->getLast24H());
$this->assertSame(1.32, $result->getSaleVelocityPerSeconds());
}
private function createResponse(int $statusCode, array $response): ResponseInterface { private function createResponse(int $statusCode, array $response): ResponseInterface {
return new Response($statusCode, ['content-type' => 'json'], json_encode($response)); return new Response($statusCode, ['content-type' => 'json'], json_encode($response));
} }