feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
460
tests/Unit/Framework/Api/ApiRequestTraitTest.php
Normal file
460
tests/Unit/Framework/Api/ApiRequestTraitTest.php
Normal file
@@ -0,0 +1,460 @@
|
||||
<?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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user