diff --git a/src/Exception/FailedModPortalRequestException.php b/src/Exception/FailedModPortalRequestException.php new file mode 100644 index 0000000..4433a01 --- /dev/null +++ b/src/Exception/FailedModPortalRequestException.php @@ -0,0 +1,23 @@ + + * @license http://opensource.org/licenses/GPL-3.0 GPL v3 + */ +class FailedModPortalRequestException extends ServerException +{ + private const MESSAGE = 'Request to the Factorio Mod Portal failed: %s'; + + public function __construct(string $message, ?Throwable $previous = null) + { + parent::__construct(sprintf(self::MESSAGE, $message), 503, $previous); + } +} diff --git a/src/Service/ModPortalService.php b/src/Service/ModPortalService.php index 9d868c7..ab21ad2 100644 --- a/src/Service/ModPortalService.php +++ b/src/Service/ModPortalService.php @@ -9,9 +9,10 @@ use BluePsyduck\FactorioModPortalClient\Entity\Release; use BluePsyduck\FactorioModPortalClient\Entity\Version; use BluePsyduck\FactorioModPortalClient\Exception\ClientException; +use BluePsyduck\FactorioModPortalClient\Exception\ErrorResponseException; use BluePsyduck\FactorioModPortalClient\Request\FullModRequest; use BluePsyduck\FactorioModPortalClient\Utils\ModUtils; -use GuzzleHttp\Promise\PromiseInterface; +use FactorioItemBrowser\CombinationApi\Server\Exception\FailedModPortalRequestException; use GuzzleHttp\Promise\Utils; /** @@ -33,28 +34,35 @@ public function __construct(ClientInterface $modPortalClient) * Requests the mods from the Mod Portal API. Not known mods will be missing in the result array. * @param array $modNames * @return array + * @throws FailedModPortalRequestException */ public function requestMods(array $modNames): array { + $mods = []; $promises = []; try { foreach ($modNames as $modName) { $request = new FullModRequest(); $request->setName($modName); - $promises[$modName] = $this->modPortalClient->sendRequest($request); + + $promises[] = $this->modPortalClient->sendRequest($request)->then( + function (Mod $mod) use (&$mods): void { + $mods[$mod->getName()] = $mod; + }, + function (ClientException $exception): void { + // Ignore mods not existing on the mod portal. + if ($exception instanceof ErrorResponseException && $exception->getCode() === 404) { + return; + } + throw new FailedModPortalRequestException($exception->getMessage(), $exception); + }, + ); } } catch (ClientException $e) { + throw new FailedModPortalRequestException($e->getMessage(), $e); } - $mods = []; - $responses = Utils::settle($promises)->wait(); - foreach ($responses as $response) { - if ($response['state'] === PromiseInterface::FULFILLED) { - /** @var Mod $mod */ - $mod = $response['value']; - $mods[$mod->getName()] = $mod; - } - } + Utils::all($promises)->wait(); return $mods; } diff --git a/src/Service/ValidationService.php b/src/Service/ValidationService.php index fc4a612..2dfd972 100644 --- a/src/Service/ValidationService.php +++ b/src/Service/ValidationService.php @@ -11,6 +11,7 @@ use FactorioItemBrowser\CombinationApi\Client\Constant\ValidationProblemType; use FactorioItemBrowser\CombinationApi\Client\Transfer\ValidatedMod; use FactorioItemBrowser\CombinationApi\Client\Transfer\ValidationProblem; +use FactorioItemBrowser\CombinationApi\Server\Exception\FailedModPortalRequestException; use FactorioItemBrowser\Common\Constant\Constant; /** @@ -33,6 +34,7 @@ public function __construct(ModPortalService $modPortalService) * @param array $modNames * @param Version $factorioVersion * @return array + * @throws FailedModPortalRequestException */ public function validate(array $modNames, Version $factorioVersion): array { diff --git a/test/src/Exception/FailedModPortalRequestExceptionTest.php b/test/src/Exception/FailedModPortalRequestExceptionTest.php new file mode 100644 index 0000000..3956385 --- /dev/null +++ b/test/src/Exception/FailedModPortalRequestExceptionTest.php @@ -0,0 +1,31 @@ + + * @license http://opensource.org/licenses/GPL-3.0 GPL v3 + * @covers \FactorioItemBrowser\CombinationApi\Server\Exception\FailedModPortalRequestException + */ +class FailedModPortalRequestExceptionTest extends TestCase +{ + public function test(): void + { + $message = 'abc'; + $previous = $this->createMock(Throwable::class); + + $exception = new FailedModPortalRequestException($message, $previous); + + $this->assertSame('Request to the Factorio Mod Portal failed: abc', $exception->getMessage()); + $this->assertSame(503, $exception->getCode()); + $this->assertSame($previous, $exception->getPrevious()); + } +} diff --git a/test/src/Service/ModPortalServiceTest.php b/test/src/Service/ModPortalServiceTest.php index d9334d2..ca4eeb8 100644 --- a/test/src/Service/ModPortalServiceTest.php +++ b/test/src/Service/ModPortalServiceTest.php @@ -9,9 +9,12 @@ use BluePsyduck\FactorioModPortalClient\Entity\Release; use BluePsyduck\FactorioModPortalClient\Entity\Version; use BluePsyduck\FactorioModPortalClient\Exception\ClientException; +use BluePsyduck\FactorioModPortalClient\Exception\ErrorResponseException; use BluePsyduck\FactorioModPortalClient\Request\FullModRequest; +use FactorioItemBrowser\CombinationApi\Server\Exception\FailedModPortalRequestException; use FactorioItemBrowser\CombinationApi\Server\Service\ModPortalService; use GuzzleHttp\Promise\FulfilledPromise; +use GuzzleHttp\Promise\RejectedPromise; use PHPUnit\Framework\TestCase; /** @@ -23,6 +26,9 @@ */ class ModPortalServiceTest extends TestCase { + /** + * @throws FailedModPortalRequestException + */ public function testRequestMods(): void { $modNames = ['abc', 'def', 'ghi']; @@ -38,6 +44,7 @@ public function testRequestMods(): void $mod2->setName('def'); $promise1 = new FulfilledPromise($mod1); $promise2 = new FulfilledPromise($mod2); + $promise3 = new RejectedPromise(new ErrorResponseException('test', 404, '', '')); $expectedResult = [ 'abc' => $mod1, 'def' => $mod2, @@ -54,7 +61,7 @@ public function testRequestMods(): void ->willReturnOnConsecutiveCalls( $promise1, $promise2, - $this->throwException($this->createMock(ClientException::class)), + $promise3, ); $instance = new ModPortalService($modPortalClient); @@ -63,6 +70,65 @@ public function testRequestMods(): void $this->assertEquals($expectedResult, $result); } + public function testRequestModsWithServerException(): void + { + $modNames = ['abc', 'def']; + $expectedRequest1 = new FullModRequest(); + $expectedRequest1->setName('abc'); + $expectedRequest2 = new FullModRequest(); + $expectedRequest2->setName('def'); + $mod1 = new Mod(); + $mod1->setName('abc'); + $promise1 = new FulfilledPromise($mod1); + $promise2 = new RejectedPromise(new ErrorResponseException('test', 500, '', '')); + + $modPortalClient = $this->createMock(ClientInterface::class); + $modPortalClient->expects($this->exactly(2)) + ->method('sendRequest') + ->withConsecutive( + [$this->equalTo($expectedRequest1)], + [$this->equalTo($expectedRequest2)], + ) + ->willReturnOnConsecutiveCalls( + $promise1, + $promise2, + ); + + $this->expectException(FailedModPortalRequestException::class); + + $instance = new ModPortalService($modPortalClient); + $instance->requestMods($modNames); + } + + public function testRequestModsWithInitialException(): void + { + $modNames = ['abc', 'def']; + $expectedRequest1 = new FullModRequest(); + $expectedRequest1->setName('abc'); + $expectedRequest2 = new FullModRequest(); + $expectedRequest2->setName('def'); + $mod1 = new Mod(); + $mod1->setName('abc'); + $promise1 = new FulfilledPromise($mod1); + + $modPortalClient = $this->createMock(ClientInterface::class); + $modPortalClient->expects($this->exactly(2)) + ->method('sendRequest') + ->withConsecutive( + [$this->equalTo($expectedRequest1)], + [$this->equalTo($expectedRequest2)], + ) + ->willReturnOnConsecutiveCalls( + $promise1, + $this->throwException($this->createMock(ClientException::class)), + ); + + $this->expectException(FailedModPortalRequestException::class); + + $instance = new ModPortalService($modPortalClient); + $instance->requestMods($modNames); + } + public function testSelectLatestReleases(): void { $release1 = new Release();