Compare commits
	
		
			2 Commits
		
	
	
		
			70080bf80a
			...
			6e5f929dc3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6e5f929dc3 | |||
| 7eac908bc4 | 
@ -39,6 +39,13 @@ services:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $username: '%app.usetreno.username%'
 | 
			
		||||
            $password: '%app.usetreno.password%'
 | 
			
		||||
            $retryCount: 2
 | 
			
		||||
            $retryWaitSeconds: 0.5
 | 
			
		||||
 | 
			
		||||
    App\Service\Remote\UsetrenoQRCodeProvider:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $retryCount: 2
 | 
			
		||||
            $retryWaitSeconds: 0.5
 | 
			
		||||
 | 
			
		||||
    App\Service\CachedQRCodeGenerator:
 | 
			
		||||
        arguments:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Remote\Usetreno;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,9 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote\Exception;
 | 
			
		||||
 | 
			
		||||
class UsetrenoQRCodeRemoteServerErrorException extends UsetrenoQRCodeException
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								src/Service/Remote/RetryingFailClientTrait.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/Service/Remote/RetryingFailClientTrait.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use Exception;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
 | 
			
		||||
trait RetryingFailClientTrait
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Lepsi zopakovat request kvuli drobnemu vypadku site nebo sluzby (u nedestruktivni operace)
 | 
			
		||||
     * nez hodit klientovi rovnou 500
 | 
			
		||||
     *
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param float $sleep
 | 
			
		||||
     * @param array<string> $catchableExceptions
 | 
			
		||||
     * @param LoggerInterface $logger
 | 
			
		||||
     * @param callable $callback
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    protected function retryingFailRequest(
 | 
			
		||||
        int $count,
 | 
			
		||||
        float $sleep,
 | 
			
		||||
        array $catchableExceptions,
 | 
			
		||||
        LoggerInterface $logger,
 | 
			
		||||
        callable $callback
 | 
			
		||||
    ): mixed {
 | 
			
		||||
        for ($i = 0; ; $i++) {
 | 
			
		||||
            try {
 | 
			
		||||
                return $callback();
 | 
			
		||||
            } catch (Exception $e) {
 | 
			
		||||
                foreach ($catchableExceptions as $exceptionClass) {
 | 
			
		||||
                    if ($e instanceof $exceptionClass) {
 | 
			
		||||
                        $logger->error("transport: fail request retrying... got catchable exception", [
 | 
			
		||||
                            'exception' => $e,
 | 
			
		||||
                            'try' => $i
 | 
			
		||||
                        ]);
 | 
			
		||||
 | 
			
		||||
                        usleep((int) ($sleep * 1_000_000));
 | 
			
		||||
 | 
			
		||||
                        if ($i == $count) {
 | 
			
		||||
                            throw $e;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        continue 2;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                throw $e;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // phpstan fail
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -5,7 +5,9 @@ namespace App\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use App\Entity\Remote\Usetreno\AuthRequest;
 | 
			
		||||
use App\Service\Remote\Exception\AuthorizeException;
 | 
			
		||||
use Nubium\Exception\ThisShouldNeverHappenException;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\HttpClient\Exception\TransportException;
 | 
			
		||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
 | 
			
		||||
@ -16,12 +18,16 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface;
 | 
			
		||||
 | 
			
		||||
class UsetrenoHttpClient implements HttpClientInterface
 | 
			
		||||
{
 | 
			
		||||
    use RetryingFailClientTrait;
 | 
			
		||||
 | 
			
		||||
    const AUTHORIZE_API = "https://topapi.top-test.cz/chameleon/api/v1/token";
 | 
			
		||||
 | 
			
		||||
    protected ?string $authorizationToken = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(protected HttpClientInterface $innerClient, protected readonly LoggerInterface $logger, protected readonly string $username,
 | 
			
		||||
                                protected readonly string $password) { }
 | 
			
		||||
                                protected readonly string $password,
 | 
			
		||||
                                protected readonly float $retryWaitSeconds,
 | 
			
		||||
                                protected readonly int $retryCount) { }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $method
 | 
			
		||||
@ -88,26 +94,35 @@ class UsetrenoHttpClient implements HttpClientInterface
 | 
			
		||||
            "AUTHORIZE_API" => static::AUTHORIZE_API
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $rq = $this->innerClient->request(
 | 
			
		||||
            "POST",
 | 
			
		||||
            static::AUTHORIZE_API,
 | 
			
		||||
            [
 | 
			
		||||
                'json' => new AuthRequest($this->username, $this->password),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
        $responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds,
 | 
			
		||||
            [AuthorizeException::class, TransportException::class], $this->logger, function() {
 | 
			
		||||
            $rq = $this->innerClient->request(
 | 
			
		||||
                "POST",
 | 
			
		||||
                static::AUTHORIZE_API,
 | 
			
		||||
                [
 | 
			
		||||
                    'json' => new AuthRequest($this->username, $this->password),
 | 
			
		||||
                ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        $statusCode = $rq->getStatusCode();
 | 
			
		||||
        $responseData = $rq->getContent(false);
 | 
			
		||||
            $statusCode = $rq->getStatusCode();
 | 
			
		||||
            $responseData = $rq->getContent(false);
 | 
			
		||||
 | 
			
		||||
        if ($statusCode != 200) {
 | 
			
		||||
            $this->logger->error("authorization request status code is not ok", [
 | 
			
		||||
               "AUTHORIZE_API" => static::AUTHORIZE_API,
 | 
			
		||||
                "statusCode" => $statusCode,
 | 
			
		||||
                "content" => $responseData,
 | 
			
		||||
                "username" => $this->username
 | 
			
		||||
            ]);
 | 
			
		||||
            if ($statusCode != 200) {
 | 
			
		||||
                $this->logger->error("authorization request status code is not ok", [
 | 
			
		||||
                    "AUTHORIZE_API" => static::AUTHORIZE_API,
 | 
			
		||||
                    "statusCode" => $statusCode,
 | 
			
		||||
                    "content" => $responseData,
 | 
			
		||||
                    "username" => $this->username
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
            throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
                throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $responseData;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!is_string($responseData)) {
 | 
			
		||||
            throw new ThisShouldNeverHappenException("responseData is not a string");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->authorizationToken = $this->processAuthorizeResponse($responseData);
 | 
			
		||||
 | 
			
		||||
@ -7,16 +7,23 @@ use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use App\Service\CacheableQRCodeGeneratorInterface;
 | 
			
		||||
use App\Service\Remote\Edge\QRCodeEntityConverter;
 | 
			
		||||
use App\Service\Remote\Exception\UsetrenoQRCodeException;
 | 
			
		||||
use App\Service\Remote\Exception\UsetrenoQRCodeRemoteServerErrorException;
 | 
			
		||||
use Nubium\Exception\ThisShouldNeverHappenException;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\HttpClient\Exception\TransportException;
 | 
			
		||||
 | 
			
		||||
class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface
 | 
			
		||||
{
 | 
			
		||||
    use RetryingFailClientTrait;
 | 
			
		||||
 | 
			
		||||
    const QRCODE_API = 'https://topapi.top-test.cz/chameleon/api/v1/qr-code/create-for-bank-account-payment';
 | 
			
		||||
    const QRCODE_METHOD = 'POST';
 | 
			
		||||
 | 
			
		||||
    public function __construct(protected readonly LoggerInterface $logger,
 | 
			
		||||
                                protected readonly UsetrenoHttpClient $client,
 | 
			
		||||
                                protected readonly QRCodeEntityConverter $codeEntityConverter) { }
 | 
			
		||||
                                protected readonly QRCodeEntityConverter $codeEntityConverter,
 | 
			
		||||
                                protected readonly float $retryWaitSeconds,
 | 
			
		||||
                                protected readonly int $retryCount) { }
 | 
			
		||||
 | 
			
		||||
    public function generateQRCodeFromEntity(QRCode $entity): string
 | 
			
		||||
    {
 | 
			
		||||
@ -27,27 +34,41 @@ class UsetrenoQRCodeProvider implements CacheableQRCodeGeneratorInterface
 | 
			
		||||
            "edgeEntity" => $edgeEntity
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [
 | 
			
		||||
            'json' => $edgeEntity,
 | 
			
		||||
        ]);
 | 
			
		||||
        $responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds,
 | 
			
		||||
            [TransportException::class, UsetrenoQRCodeRemoteServerErrorException::class], $this->logger,
 | 
			
		||||
            function() use ($edgeEntity) {
 | 
			
		||||
                $response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [
 | 
			
		||||
                    'json' => $edgeEntity,
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
        $statusCode = $response->getStatusCode();
 | 
			
		||||
        $responseData = $response->getContent(false);
 | 
			
		||||
                $statusCode = $response->getStatusCode();
 | 
			
		||||
                $responseData = $response->getContent(false);
 | 
			
		||||
 | 
			
		||||
        if ($statusCode != 200) {
 | 
			
		||||
            $this->logger->error("qrcode request status code is not ok", [
 | 
			
		||||
                "AUTHORIZE_API" => static::QRCODE_API,
 | 
			
		||||
                "statusCode" => $statusCode,
 | 
			
		||||
                "content" => $responseData,
 | 
			
		||||
            ]);
 | 
			
		||||
                if ($statusCode != 200) {
 | 
			
		||||
                    $this->logger->error("qrcode request status code is not ok", [
 | 
			
		||||
                        "AUTHORIZE_API" => static::QRCODE_API,
 | 
			
		||||
                        "statusCode" => $statusCode,
 | 
			
		||||
                        "content" => $responseData,
 | 
			
		||||
                    ]);
 | 
			
		||||
 | 
			
		||||
            throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
                    if ($statusCode > 500) {
 | 
			
		||||
                        throw new UsetrenoQRCodeRemoteServerErrorException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $this->logger->debug("QRCode generation success", [
 | 
			
		||||
                    "responseContent" => $responseData,
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
                return $responseData;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!is_string($responseData)) {
 | 
			
		||||
            throw new ThisShouldNeverHappenException("responseData is not a string");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->logger->debug("QRCode generation success", [
 | 
			
		||||
            "responseContent" => $responseData,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        return $this->parseBase64String($this->processQRCodeResponseEntity($responseData));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										56
									
								
								tests/Service/Remote/RetryingFailClientTraitTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								tests/Service/Remote/RetryingFailClientTraitTest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Tests\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use App\Service\Remote\RetryingFailClientTrait;
 | 
			
		||||
use App\Tests\Common\LoggerTrait;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
 | 
			
		||||
class RetryingFailClientTraitTest extends TestCase {
 | 
			
		||||
    use LoggerTrait;
 | 
			
		||||
 | 
			
		||||
    private int $callCount = 0;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::setUp(); // TODO: Change the autogenerated stub
 | 
			
		||||
        $this->callCount = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSuccess() {
 | 
			
		||||
        $trait = new class {
 | 
			
		||||
            use RetryingFailClientTrait {
 | 
			
		||||
                retryingFailRequest as public; // make the method public
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        $result = $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () {
 | 
			
		||||
            $this->callCount = $this->callCount + 1;
 | 
			
		||||
            return 'foo';
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(1, $this->callCount);
 | 
			
		||||
        $this->assertEquals('foo', $result);
 | 
			
		||||
    }
 | 
			
		||||
    public function testRetyingFail() {
 | 
			
		||||
        $trait = new class {
 | 
			
		||||
            use RetryingFailClientTrait {
 | 
			
		||||
                retryingFailRequest as public; // make the method public
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () {
 | 
			
		||||
                $this->callCount = $this->callCount + 1;
 | 
			
		||||
                throw new \RuntimeException("test");
 | 
			
		||||
            });
 | 
			
		||||
        } catch (\RuntimeException) {
 | 
			
		||||
            // do nothing
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(3, $this->callCount);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -24,7 +24,7 @@ class UsetrenoHttpClientTest extends TestCase
 | 
			
		||||
        ]);
 | 
			
		||||
        $authorizedRequestResponse = clone $authMockResponse;
 | 
			
		||||
        $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]);
 | 
			
		||||
        $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar");
 | 
			
		||||
        $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0);
 | 
			
		||||
        $client->request("POST", "https://www.root.cz/");
 | 
			
		||||
        $this->assertEquals("https://topapi.top-test.cz/chameleon/api/v1/token", $authMockResponse->getRequestUrl());
 | 
			
		||||
        $headers = $authorizedRequestResponse->getRequestOptions()['headers'];
 | 
			
		||||
@ -48,7 +48,7 @@ class UsetrenoHttpClientTest extends TestCase
 | 
			
		||||
 | 
			
		||||
        $authorizedRequestResponse = clone $authMockResponse;
 | 
			
		||||
        $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]);
 | 
			
		||||
        $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar");
 | 
			
		||||
        $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0);
 | 
			
		||||
        $client->request("POST", "https://www.root.cz/");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ class UsetrenoQRCodeProviderTest extends TestCase
 | 
			
		||||
            ->method('convert')
 | 
			
		||||
            ->will($this->returnValue($edgeEntity));
 | 
			
		||||
 | 
			
		||||
        return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock);
 | 
			
		||||
        return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock, 0, 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function createQRCodeEntityPair() {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user