Files
michaelschiemer/tests/Unit/Framework/Api/ApiRequestTraitTest.php

461 lines
15 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Api\ApiException;
use App\Framework\Api\ApiRequestTrait;
use App\Framework\Http\Headers;
use App\Framework\Http\Method;
use App\Framework\Http\Status;
use App\Framework\HttpClient\ClientOptions;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\ClientResponse;
use App\Framework\HttpClient\HttpClient;
// Test class that uses the trait
final class TestApiClient
{
use ApiRequestTrait;
public function __construct(
string $baseUrl,
HttpClient $httpClient,
ClientOptions $defaultOptions
) {
$this->baseUrl = $baseUrl;
$this->httpClient = $httpClient;
$this->defaultOptions = $defaultOptions;
}
// Expose protected method for testing
public function testDecodeJson(ClientResponse $response): array
{
return $this->decodeJson($response);
}
}
describe('ApiRequestTrait', function () {
beforeEach(function () {
$this->httpClient = Mockery::mock(HttpClient::class);
$this->defaultOptions = ClientOptions::withTimeout(30);
$this->baseUrl = 'https://api.example.com';
$this->apiClient = new TestApiClient(
$this->baseUrl,
$this->httpClient,
$this->defaultOptions
);
});
afterEach(function () {
Mockery::close();
});
describe('sendRequest', function () {
it('sends GET request successfully', function () {
$expectedResponse = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{"data": "success"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->with(Mockery::on(function ($request) {
return $request instanceof ClientRequest
&& $request->method === Method::GET
&& str_contains($request->url, '/api/users');
}))
->andReturn($expectedResponse);
$response = $this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/users',
data: []
);
expect($response)->toBe($expectedResponse);
expect($response->status)->toBe(Status::OK);
});
it('sends POST request with data', function () {
$expectedResponse = new ClientResponse(
status: Status::CREATED,
headers: new Headers([]),
body: '{"id": 123}'
);
$this->httpClient
->shouldReceive('send')
->once()
->with(Mockery::on(function ($request) {
return $request instanceof ClientRequest
&& $request->method === Method::POST
&& str_contains($request->url, '/api/users')
&& !empty($request->body); // Data is stored in body as JSON
}))
->andReturn($expectedResponse);
$response = $this->apiClient->sendRequest(
method: Method::POST,
endpoint: '/api/users',
data: ['name' => 'John Doe', 'email' => 'john@example.com']
);
expect($response->status)->toBe(Status::CREATED);
});
it('strips leading slash from endpoint', function () {
$expectedResponse = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{}'
);
$this->httpClient
->shouldReceive('send')
->once()
->with(Mockery::on(function ($request) {
// URL should be clean without double slashes
return $request->url === 'https://api.example.com/api/test';
}))
->andReturn($expectedResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
});
it('handles endpoint without leading slash', function () {
$expectedResponse = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{}'
);
$this->httpClient
->shouldReceive('send')
->once()
->with(Mockery::on(function ($request) {
return $request->url === 'https://api.example.com/api/test';
}))
->andReturn($expectedResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: 'api/test'
);
});
it('uses custom options when provided', function () {
$customOptions = ClientOptions::withTimeout(60);
$expectedResponse = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{}'
);
$this->httpClient
->shouldReceive('send')
->once()
->with(Mockery::on(function ($request) use ($customOptions) {
return $request->options === $customOptions;
}))
->andReturn($expectedResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test',
data: [],
options: $customOptions
);
});
it('uses default options when none provided', function () {
$expectedResponse = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{}'
);
$this->httpClient
->shouldReceive('send')
->once()
->with(Mockery::on(function ($request) {
return $request->options === $this->defaultOptions;
}))
->andReturn($expectedResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
});
});
describe('error handling', function () {
it('throws ApiException for 400 errors', function () {
$errorResponse = new ClientResponse(
status: Status::BAD_REQUEST,
headers: new Headers([]),
body: '{"error": "Invalid request"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
})->throws(ApiException::class);
it('throws ApiException for 401 errors', function () {
$errorResponse = new ClientResponse(
status: Status::UNAUTHORIZED,
headers: new Headers([]),
body: '{"error": "Unauthorized"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
})->throws(ApiException::class);
it('throws ApiException for 404 errors', function () {
$errorResponse = new ClientResponse(
status: Status::NOT_FOUND,
headers: new Headers([]),
body: '{"error": "Not found"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
})->throws(ApiException::class);
it('throws ApiException for 500 errors', function () {
$errorResponse = new ClientResponse(
status: Status::INTERNAL_SERVER_ERROR,
headers: new Headers([]),
body: '{"error": "Server error"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
})->throws(ApiException::class);
it('formats error message with detail field', function () {
$errorResponse = new ClientResponse(
status: Status::BAD_REQUEST,
headers: new Headers([]),
body: '{"detail": "Validation failed"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
try {
$this->apiClient->sendRequest(
method: Method::POST,
endpoint: '/api/users'
);
expect(false)->toBeTrue(); // Should not reach here
} catch (ApiException $e) {
expect($e->getMessage())->toContain('Validation failed');
}
});
it('formats error message with validation_messages', function () {
$errorResponse = new ClientResponse(
status: Status::UNPROCESSABLE_ENTITY,
headers: new Headers([]),
body: '{"detail": "Validation failed", "validation_messages": {"email": "Invalid format"}}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
try {
$this->apiClient->sendRequest(
method: Method::POST,
endpoint: '/api/users'
);
expect(false)->toBeTrue(); // Should not reach here
} catch (ApiException $e) {
expect($e->getMessage())->toContain('Validation failed');
expect($e->getMessage())->toContain('Validierungsfehler');
expect($e->getMessage())->toContain('email');
}
});
it('formats error message with error field', function () {
$errorResponse = new ClientResponse(
status: Status::INTERNAL_SERVER_ERROR,
headers: new Headers([]),
body: '{"error": "Database connection failed"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
try {
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
expect(false)->toBeTrue(); // Should not reach here
} catch (ApiException $e) {
expect($e->getMessage())->toContain('Database connection failed');
}
});
it('formats generic error message when no error fields present', function () {
$errorResponse = new ClientResponse(
status: Status::BAD_REQUEST,
headers: new Headers([]),
body: '{"some_field": "some_value"}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($errorResponse);
try {
$this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
expect(false)->toBeTrue(); // Should not reach here
} catch (ApiException $e) {
expect($e->getMessage())->toBe('API-Fehler');
}
});
it('does not throw exception for 2xx responses', function () {
$successResponse = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{"success": true}'
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($successResponse);
$response = $this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
expect($response->status)->toBe(Status::OK);
});
it('does not throw exception for 3xx responses', function () {
$redirectResponse = new ClientResponse(
status: Status::MOVED_PERMANENTLY,
headers: new Headers(['Location' => 'https://example.com/new']),
body: ''
);
$this->httpClient
->shouldReceive('send')
->once()
->andReturn($redirectResponse);
$response = $this->apiClient->sendRequest(
method: Method::GET,
endpoint: '/api/test'
);
expect($response->status)->toBe(Status::MOVED_PERMANENTLY);
});
});
describe('decodeJson', function () {
it('decodes valid JSON response', function () {
$response = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{"name": "John", "age": 30}'
);
$data = $this->apiClient->testDecodeJson($response);
expect($data)->toBeArray();
expect($data['name'])->toBe('John');
expect($data['age'])->toBe(30);
});
it('returns empty array for invalid JSON', function () {
$response = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: 'Invalid JSON {'
);
$data = $this->apiClient->testDecodeJson($response);
expect($data)->toBe([]);
});
it('returns empty array for empty body', function () {
$response = new ClientResponse(
status: Status::NO_CONTENT,
headers: new Headers([]),
body: ''
);
$data = $this->apiClient->testDecodeJson($response);
expect($data)->toBe([]);
});
it('decodes nested JSON structures', function () {
$response = new ClientResponse(
status: Status::OK,
headers: new Headers([]),
body: '{"user": {"name": "John", "email": "john@example.com"}, "meta": {"total": 1}}'
);
$data = $this->apiClient->testDecodeJson($response);
expect($data)->toBeArray();
expect($data['user'])->toBeArray();
expect($data['user']['name'])->toBe('John');
expect($data['meta']['total'])->toBe(1);
});
});
});