Implemented security question-answer flow endpoints and statistics endpoint

(cherry picked from commit 481c984de83cf242b181882d92dae4bc66bc25d5)
This commit is contained in:
valentinpahusko 2019-10-18 17:02:56 +03:00
parent 1bb4365e55
commit ff6ef65a5e
6 changed files with 344 additions and 3 deletions

View File

@ -6,6 +6,8 @@ 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\AnswerResponse;
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 +152,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 +454,85 @@ 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 {
$uri = new Uri('https://api.mojang.com/user/security/location');
$request = new Request('GET', $uri, ['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 {
$uri = new Uri('https://api.mojang.com/user/security/challenges');
$request = new Request('GET', $uri, ['Authorization' => 'Bearer ' . $accessToken]);
$response = $this->getClient()->send($request);
$rawBody = $response->getBody()->getContents();
if (empty($rawBody)) {
throw new Exception\NoContentException($request, $response);
}
$result = [];
$body = $this->decode($rawBody);
foreach ($body as $question) {
$result[] = [
'answer' => new AnswerResponse($question['answer']['id']),
'question' => new QuestionResponse($question['question']['id'], $question['question']['question']),
];
}
return $result;
}
/**
* @param string $accessToken
* @param array $answers
* @throws GuzzleException
*
* @url https://wiki.vg/Mojang_API#Send_back_the_answers
*/
public function answer(string $accessToken, array $answers): void {
$uri = new Uri('https://api.mojang.com/user/security/location');
$request = new Request('POST', $uri, ['Authorization' => 'Bearer ' . $accessToken], json_encode($answers));
$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 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,
]);
$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,31 @@
<?php
declare(strict_types=1);
namespace Ely\Mojang\Response;
class AnswerResponse {
/**
* @var int
*/
public $id;
/**
* @var string|null
*/
public $answer;
public function __construct(int $id, ?string $answer = null) {
$this->id = $id;
$this->answer = $answer;
}
public function getId(): int {
return $this->id;
}
public function getAnswer(): ?string {
return $this->answer;
}
}

View File

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

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,15 @@ 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\AnswerResponse;
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 +312,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 +612,144 @@ 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->assertArrayHasKey('answer', $question);
$this->assertArrayHasKey('question', $question);
$this->assertInstanceOf(AnswerResponse::class, $question['answer']);
$this->assertInstanceOf(QuestionResponse::class, $question['question']);
}
/** @var AnswerResponse $firstAnswer */
$firstAnswer = $result[0]['answer'];
/** @var QuestionResponse $firstQuestion */
$firstQuestion = $result[0]['question'];
$this->assertSame(123, $firstAnswer->getId());
$this->assertNull($firstAnswer->getAnswer());
$this->assertSame(1, $firstQuestion->getId());
$this->assertSame('What is your favorite pet\'s name?', $firstQuestion->getQuestion());
/** @var AnswerResponse $secondAnswer */
$secondAnswer = $result[1]['answer'];
/** @var QuestionResponse $secondQuestion */
$secondQuestion = $result[1]['question'];
$this->assertSame(456, $secondAnswer->getId());
$this->assertNull($secondAnswer->getAnswer());
$this->assertSame(2, $secondQuestion->getId());
$this->assertSame('What is your favorite movie?', $secondQuestion->getQuestion());
/** @var AnswerResponse $thirdAnswer */
$thirdAnswer = $result[2]['answer'];
/** @var QuestionResponse $thirdQuestion */
$thirdQuestion = $result[2]['question'];
$this->assertSame(789, $thirdAnswer->getId());
$this->assertNull($thirdAnswer->getAnswer());
$this->assertSame(3, $thirdQuestion->getId());
$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',
]));
$this->expectException(OperationException::class);
$this->api->answer('mocked access token', [
[
'id' => 123,
'answer' => 'foo',
],
[
'id' => 456,
'answer' => 'bar',
],
]);
}
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([
'metricKeys' => [
'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));
} }