From ff6ef65a5e2286c9ce61b78b156d1b19c9bd4f73 Mon Sep 17 00:00:00 2001 From: valentinpahusko Date: Fri, 18 Oct 2019 17:02:56 +0300 Subject: [PATCH 1/3] Implemented security question-answer flow endpoints and statistics endpoint (cherry picked from commit 481c984de83cf242b181882d92dae4bc66bc25d5) --- src/Api.php | 85 +++++++++++++++- src/Exception/OperationException.php | 16 +++ src/Response/AnswerResponse.php | 31 ++++++ src/Response/QuestionResponse.php | 31 ++++++ src/Response/StatisticsResponse.php | 41 ++++++++ tests/ApiTest.php | 143 ++++++++++++++++++++++++++- 6 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 src/Exception/OperationException.php create mode 100644 src/Response/AnswerResponse.php create mode 100644 src/Response/QuestionResponse.php create mode 100644 src/Response/StatisticsResponse.php diff --git a/src/Api.php b/src/Api.php index 55c3eda..85667c2 100644 --- a/src/Api.php +++ b/src/Api.php @@ -6,6 +6,8 @@ namespace Ely\Mojang; use DateTime; use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\RetryMiddleware; +use Ely\Mojang\Response\AnswerResponse; +use Ely\Mojang\Response\QuestionResponse; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; @@ -150,8 +152,8 @@ class Api { } } - if (count($names) > 100) { - throw new InvalidArgumentException('You cannot request more than 100 names per request'); + if (count($names) > 10) { + throw new InvalidArgumentException('You cannot request more than 10 names per request'); } $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']); } + /** + * @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 */ diff --git a/src/Exception/OperationException.php b/src/Exception/OperationException.php new file mode 100644 index 0000000..b791826 --- /dev/null +++ b/src/Exception/OperationException.php @@ -0,0 +1,16 @@ +id = $id; + $this->answer = $answer; + } + + public function getId(): int { + return $this->id; + } + + public function getAnswer(): ?string { + return $this->answer; + } + +} diff --git a/src/Response/QuestionResponse.php b/src/Response/QuestionResponse.php new file mode 100644 index 0000000..997790d --- /dev/null +++ b/src/Response/QuestionResponse.php @@ -0,0 +1,31 @@ +id = $id; + $this->question = $question; + } + + public function getId(): int { + return $this->id; + } + + public function getQuestion(): string { + return $this->question; + } + +} diff --git a/src/Response/StatisticsResponse.php b/src/Response/StatisticsResponse.php new file mode 100644 index 0000000..5f3d06e --- /dev/null +++ b/src/Response/StatisticsResponse.php @@ -0,0 +1,41 @@ +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; + } + +} diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 3b23e23..7d9a872 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -6,12 +6,15 @@ namespace Ely\Mojang\Test; use Ely\Mojang\Api; use Ely\Mojang\Exception\ForbiddenException; use Ely\Mojang\Exception\NoContentException; +use Ely\Mojang\Exception\OperationException; use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\RetryMiddleware; +use Ely\Mojang\Response\AnswerResponse; use Ely\Mojang\Response\ApiStatus; use Ely\Mojang\Response\NameHistoryItem; use Ely\Mojang\Response\ProfileInfo; use Ely\Mojang\Response\Properties\TexturesProperty; +use Ely\Mojang\Response\QuestionResponse; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; use GuzzleHttp\Handler\MockHandler; @@ -309,7 +312,7 @@ class ApiTest extends TestCase { } $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); } @@ -609,6 +612,144 @@ class ApiTest extends TestCase { $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 { return new Response($statusCode, ['content-type' => 'json'], json_encode($response)); } From 3ce60beaf1fc4029b78d076bc535605b172e23b3 Mon Sep 17 00:00:00 2001 From: valentinpahusko Date: Mon, 10 Feb 2020 17:29:39 +0300 Subject: [PATCH 2/3] Fixed bugs --- src/Api.php | 48 ++++++++++++++++--------------- src/Response/AnswerResponse.php | 31 -------------------- src/Response/QuestionResponse.php | 20 +++++++++---- tests/ApiTest.php | 47 +++++++++++------------------- 4 files changed, 56 insertions(+), 90 deletions(-) delete mode 100644 src/Response/AnswerResponse.php diff --git a/src/Api.php b/src/Api.php index 85667c2..30ba9fd 100644 --- a/src/Api.php +++ b/src/Api.php @@ -6,7 +6,6 @@ namespace Ely\Mojang; use DateTime; use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\RetryMiddleware; -use Ely\Mojang\Response\AnswerResponse; use Ely\Mojang\Response\QuestionResponse; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; @@ -461,8 +460,11 @@ class Api { * @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]); + $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)) { @@ -479,21 +481,16 @@ class Api { * @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]); + $request = new Request( + 'GET', + 'https://api.mojang.com/user/security/challenges', + ['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); + $body = $this->decode($response->getBody()->getContents()); foreach ($body as $question) { - $result[] = [ - 'answer' => new AnswerResponse($question['answer']['id']), - 'question' => new QuestionResponse($question['question']['id'], $question['question']['question']), - ]; + $result[] = new QuestionResponse($question['question']['id'], $question['question']['question'], $question['answer']['id']); } return $result; @@ -503,18 +500,21 @@ class Api { * @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): void { - $uri = new Uri('https://api.mojang.com/user/security/location'); - $request = new Request('POST', $uri, ['Authorization' => 'Bearer ' . $accessToken], json_encode($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(); - if (!empty($rawBody)) { - $body = $this->decode($rawBody); - throw new Exception\OperationException($body['errorMessage'], $request, $response); - } + + return empty($rawBody); } /** @@ -526,7 +526,9 @@ class Api { */ public function statistics(array $metricKeys) { $response = $this->getClient()->request('POST', 'https://api.mojang.com/orders/statistics', [ - 'json' => $metricKeys, + 'json' => [ + 'metricKeys' => $metricKeys, + ], ]); $body = $this->decode($response->getBody()->getContents()); diff --git a/src/Response/AnswerResponse.php b/src/Response/AnswerResponse.php deleted file mode 100644 index a5ffc3d..0000000 --- a/src/Response/AnswerResponse.php +++ /dev/null @@ -1,31 +0,0 @@ -id = $id; - $this->answer = $answer; - } - - public function getId(): int { - return $this->id; - } - - public function getAnswer(): ?string { - return $this->answer; - } - -} diff --git a/src/Response/QuestionResponse.php b/src/Response/QuestionResponse.php index 997790d..8b6af34 100644 --- a/src/Response/QuestionResponse.php +++ b/src/Response/QuestionResponse.php @@ -8,24 +8,34 @@ class QuestionResponse { /** * @var int */ - private $id; + private $questionId; /** * @var string */ private $question; - public function __construct(int $id, string $question) { - $this->id = $id; + /** + * @var int + */ + private $answerId; + + public function __construct(int $questionId, string $question, int $answerId) { + $this->questionId = $questionId; $this->question = $question; + $this->answerId = $answerId; } - public function getId(): int { - return $this->id; + public function getQuestionId(): int { + return $this->questionId; } public function getQuestion(): string { return $this->question; } + public function getAnswerId(): int { + return $this->answerId; + } + } diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 7d9a872..e0c6f4e 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -9,7 +9,6 @@ use Ely\Mojang\Exception\NoContentException; use Ely\Mojang\Exception\OperationException; use Ely\Mojang\Middleware\ResponseConverterMiddleware; use Ely\Mojang\Middleware\RetryMiddleware; -use Ely\Mojang\Response\AnswerResponse; use Ely\Mojang\Response\ApiStatus; use Ely\Mojang\Response\NameHistoryItem; use Ely\Mojang\Response\ProfileInfo; @@ -663,37 +662,25 @@ class ApiTest extends TestCase { $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']); + $this->assertInstanceOf(QuestionResponse::class, $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()); + $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 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()); + $secondQuestion = $result[1]; + $this->assertSame(456, $secondQuestion->getAnswerId()); + $this->assertSame(2, $secondQuestion->getQuestionId()); $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()); + $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()); } @@ -702,8 +689,7 @@ class ApiTest extends TestCase { 'error' => 'ForbiddenOperationException', 'errorMessage' => 'At least one answer was incorrect', ])); - $this->expectException(OperationException::class); - $this->api->answer('mocked access token', [ + $result = $this->api->answer('mocked access token', [ [ 'id' => 123, 'answer' => 'foo', @@ -713,6 +699,7 @@ class ApiTest extends TestCase { 'answer' => 'bar', ], ]); + $this->assertFalse($result); } public function testAnswer() { @@ -728,12 +715,10 @@ class ApiTest extends TestCase { 'saleVelocityPerSeconds' => 1.32, ])); $result = $this->api->statistics([ - 'metricKeys' => [ - 'item_sold_minecraft', - 'prepaid_card_redeemed_minecraft', - 'item_sold_cobalt', - 'item_sold_scrolls', - ], + 'item_sold_minecraft', + 'prepaid_card_redeemed_minecraft', + 'item_sold_cobalt', + 'item_sold_scrolls', ]); /** @var \Psr\Http\Message\RequestInterface $request */ $request = $this->history[0]['request']; From d5baf0e29317db685e766f1b0151b25be220ca19 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 10 Jun 2020 16:29:52 +0300 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 398bb59..80c569b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). ## [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 ### Added