ldc-arcadia-genesis-php-client/tests/Unit/Repository/AbstractApiRepositoryTest.php

331 lines
11 KiB
PHP
Raw Permalink Normal View History

<?php
declare(strict_types=1);
namespace Tests\Unit\Repository;
use DateTime;
use DateTimeImmutable;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Psr7\Request;
use Microsoft\Kiota\Abstractions\ApiException;
use Mockery;
use Uwil\ArcadiaApiPhpClient\Error\ApiError;
use Uwil\ArcadiaApiPhpClient\Error\AuthenticationError;
use Uwil\ArcadiaApiPhpClient\Error\NetworkError;
use Uwil\ArcadiaApiPhpClient\Error\RateLimitError;
use Uwil\ArcadiaApiPhpClient\Error\ServerError;
use Uwil\ArcadiaApiPhpClient\Error\UnexpectedApiError;
use Uwil\ArcadiaApiPhpClient\Error\ValidationError;
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PaginationDto;
use Uwil\ArcadiaApiPhpClient\Lib\Pagination\Pagination;
use Uwil\ArcadiaApiPhpClient\Repository\AbstractApiRepository;
use Uwil\ArcadiaKiotaOpenApiClient\ApiClient;
use Uwil\ArcadiaKiotaOpenApiClient\Models\InvalidParamItem;
use Uwil\ArcadiaKiotaOpenApiClient\Models\PaginationMeta;
use Uwil\ArcadiaKiotaOpenApiClient\Models\ProblemDetails;
use Uwil\ArcadiaKiotaOpenApiClient\Models\ValidationErrorProblemDetails;
function createConcreteApiRepository(): AbstractApiRepository
{
$apiClient = Mockery::mock(ApiClient::class);
return new class ($apiClient) extends AbstractApiRepository
{
public function callExecute(callable $callback): mixed
{
return $this->executeApiCall($callback);
}
public function callCatchNotFound(callable $callback): mixed
{
return $this->catchNotFound($callback);
}
public function callToDateTime(?\DateTimeInterface $date): ?DateTime
{
return $this->toDateTime($date);
}
public function callToPaginationDto(?PaginationMeta $meta, Pagination $pagination): PaginationDto
{
return $this->toPaginationDto($meta, $pagination);
}
};
}
describe('AbstractApiRepository', function () {
describe('executeApiCall', function () {
it('returns callback result on success', function () {
// GIVEN
$repository = createConcreteApiRepository();
// WHEN
$result = $repository->callExecute(fn () => 'success_payload');
// THEN
expect($result)->toBe('success_payload');
});
it('converts thrown exception to ApiError', function () {
// GIVEN
$repository = createConcreteApiRepository();
$exception = new \RuntimeException('Something went wrong', 500);
// WHEN
$result = $repository->callExecute(function () use ($exception) {
throw $exception;
});
// THEN
expect($result)->toBeInstanceOf(ApiError::class);
expect($result)->toBeInstanceOf(UnexpectedApiError::class);
expect($result->message())->toBe('Something went wrong');
});
});
describe('catchNotFound', function () {
it('returns callback result on success', function () {
// GIVEN
$repository = createConcreteApiRepository();
// WHEN
$result = $repository->callCatchNotFound(fn () => 'found_item');
// THEN
expect($result)->toBe('found_item');
});
it('returns null when ApiException has status 404', function () {
// GIVEN
$repository = createConcreteApiRepository();
$exception = new ApiException('Resource not found');
$exception->setResponseStatusCode(404);
// WHEN
$result = $repository->callCatchNotFound(function () use ($exception) {
throw $exception;
});
// THEN
expect($result)->toBeNull();
});
it('returns null when ProblemDetails has status 404', function () {
// GIVEN
$repository = createConcreteApiRepository();
$problem = new ProblemDetails();
$problem->setStatus(404);
$problem->setDetail('Not found');
// WHEN
$result = $repository->callCatchNotFound(function () use ($problem) {
throw $problem;
});
// THEN
expect($result)->toBeNull();
});
it('returns ApiError when ApiException status is not 404', function () {
// GIVEN
$repository = createConcreteApiRepository();
$exception = new ApiException('Internal error');
$exception->setResponseStatusCode(500);
// WHEN
$result = $repository->callCatchNotFound(function () use ($exception) {
throw $exception;
});
// THEN
expect($result)->toBeInstanceOf(ApiError::class);
expect($result)->toBeInstanceOf(ServerError::class);
expect($result->statusCode())->toBe(500);
});
it('returns ApiError when generic exception is thrown', function () {
// GIVEN
$repository = createConcreteApiRepository();
// WHEN
$result = $repository->callCatchNotFound(function () {
throw new \InvalidArgumentException('Bad arg');
});
// THEN
expect($result)->toBeInstanceOf(ApiError::class);
expect($result)->toBeInstanceOf(UnexpectedApiError::class);
});
});
describe('toApiError conversion', function () {
it('converts ValidationErrorProblemDetails to ValidationError with violations', function () {
// GIVEN
$repository = createConcreteApiRepository();
$param1 = new InvalidParamItem();
$param1->setName('email');
$param1->setReason('must be a valid email');
$problem = new ValidationErrorProblemDetails();
$problem->setDetail('Validation failed');
$problem->setInvalidParams([$param1]);
// WHEN
$result = $repository->callExecute(function () use ($problem) {
throw $problem;
});
// THEN
expect($result)->toBeInstanceOf(ValidationError::class);
/** @var ValidationError $result */
expect($result->message())->toBe('Validation failed');
expect($result->violations)->toBe(['email' => 'must be a valid email']);
});
it('converts ProblemDetails with 401/403 to AuthenticationError', function () {
// GIVEN
$repository = createConcreteApiRepository();
$problem = new ProblemDetails();
$problem->setStatus(401);
$problem->setTitle('Unauthorized');
// WHEN
$result = $repository->callExecute(function () use ($problem) {
throw $problem;
});
// THEN
expect($result)->toBeInstanceOf(AuthenticationError::class);
expect($result->statusCode())->toBe(401);
});
it('converts ProblemDetails with 429 to RateLimitError', function () {
// GIVEN
$repository = createConcreteApiRepository();
$problem = new ProblemDetails();
$problem->setStatus(429);
$problem->setTitle('Too Many Requests');
// WHEN
$result = $repository->callExecute(function () use ($problem) {
throw $problem;
});
// THEN
expect($result)->toBeInstanceOf(RateLimitError::class);
expect($result->statusCode())->toBe(429);
});
it('converts ProblemDetails with 500 to ServerError', function () {
// GIVEN
$repository = createConcreteApiRepository();
$problem = new ProblemDetails();
$problem->setStatus(502);
$problem->setDetail('Bad Gateway');
// WHEN
$result = $repository->callExecute(function () use ($problem) {
throw $problem;
});
// THEN
expect($result)->toBeInstanceOf(ServerError::class);
expect($result->statusCode())->toBe(502);
});
it('converts ConnectException to NetworkError', function () {
// GIVEN
$repository = createConcreteApiRepository();
$request = new Request('GET', 'https://api.example.com');
$exception = new ConnectException('Connection refused', $request);
// WHEN
$result = $repository->callExecute(function () use ($exception) {
throw $exception;
});
// THEN
expect($result)->toBeInstanceOf(NetworkError::class);
expect($result->message())->toBe('Connection refused');
});
});
describe('toDateTime', function () {
it('returns null when input is null', function () {
// GIVEN
$repository = createConcreteApiRepository();
// WHEN
$result = $repository->callToDateTime(null);
// THEN
expect($result)->toBeNull();
});
it('returns same DateTime instance when input is DateTime', function () {
// GIVEN
$repository = createConcreteApiRepository();
$date = new DateTime('2024-01-15 10:00:00');
// WHEN
$result = $repository->callToDateTime($date);
// THEN
expect($result)->toBe($date);
});
it('converts DateTimeImmutable to DateTime preserving value', function () {
// GIVEN
$repository = createConcreteApiRepository();
$immutable = new DateTimeImmutable('2024-01-15T10:00:00+00:00');
// WHEN
$result = $repository->callToDateTime($immutable);
// THEN
expect($result)->toBeInstanceOf(DateTime::class);
expect($result?->format(DATE_RFC3339))->toBe($immutable->format(DATE_RFC3339));
});
});
describe('toPaginationDto', function () {
it('maps PaginationMeta to PaginationDto', function () {
// GIVEN
$repository = createConcreteApiRepository();
$pagination = Pagination::firstPage();
$meta = new PaginationMeta();
$meta->setPage(2);
$meta->setPageSize(25);
$meta->setTotalItems(100);
$meta->setTotalPages(4);
// WHEN
$dto = $repository->callToPaginationDto($meta, $pagination);
// THEN
expect($dto)->toBeInstanceOf(PaginationDto::class);
expect($dto->page)->toBe(2);
expect($dto->pageSize)->toBe(25);
expect($dto->totalItems)->toBe(100);
expect($dto->totalPages)->toBe(4);
});
it('creates default PaginationDto when meta is null', function () {
// GIVEN
$repository = createConcreteApiRepository();
$pagination = Pagination::firstPage();
// WHEN
$dto = $repository->callToPaginationDto(null, $pagination);
// THEN
expect($dto)->toBeInstanceOf(PaginationDto::class);
expect($dto->page)->toBe(1);
expect($dto->pageSize)->toBe(50);
expect($dto->totalItems)->toBe(0);
expect($dto->totalPages)->toBe(0);
});
});
});