330 lines
11 KiB
PHP
330 lines
11 KiB
PHP
<?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);
|
|
});
|
|
});
|
|
});
|