Skip to content

Commit 98e2bcb

Browse files
committed
test(unit): add unit tests for DockerApiClientWrapper and DockerService
- Add tests for container listing and inspection in DockerService - Add basic tests for DockerApiClientWrapper - Update GEMINI.md with testing strategy for final classes - Update dependencies (symfony/http-client-contracts)
1 parent e1bf180 commit 98e2bcb

5 files changed

Lines changed: 213 additions & 1 deletion

File tree

GEMINI.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Gemini Context File
2+
3+
## Project Overview
4+
**Name:** php-docker-api-client
5+
**Type:** PHP Library / CLI Application
6+
**Description:** A PHP Docker API client generated from the official Docker API OpenAPI specification using [Jane PHP](https://github.com/janephp/janephp). It provides a type-safe and strictly typed interface to interact with the Docker daemon.
7+
8+
## Key Technologies
9+
* **Language:** PHP ~8.3+
10+
* **Core Library:** Jane PHP (OpenAPI Client Generator)
11+
* **Frameworks/Libs:** Symfony Console, Symfony HTTP Client
12+
* **Testing:** Codeception
13+
* **Code Style:** PHP-CS-Fixer
14+
15+
## Architecture
16+
The project follows a split architecture between generated code and application logic:
17+
* **`generated/`**: Contains the raw API client, endpoints, and models generated by Jane PHP. **Do not modify these files manually** unless debugging generation issues.
18+
* **`src/`**: Contains the manually written application logic, wrapper services, factories, and CLI commands.
19+
* **`spec/`**: Contains the Docker OpenAPI specifications (`.yaml`).
20+
21+
## Namespaces
22+
* **Generated Code:** `WebProject\DockerApi\Library\Generated`
23+
* **Application Code:** `WebProject\DockerApiClient`
24+
25+
## Build & Run Commands
26+
27+
### Installation
28+
```bash
29+
composer install
30+
```
31+
32+
### Code Generation
33+
Regenerate the API client from the OpenAPI spec:
34+
```bash
35+
composer generate
36+
# Maps to: XDEBUG_MODE=off jane-openapi generate
37+
```
38+
39+
### Testing
40+
Run the Codeception test suite:
41+
```bash
42+
composer tests
43+
# Maps to: vendor/bin/codecept run
44+
```
45+
Run tests with coverage (CI style):
46+
```bash
47+
vendor/bin/codecept build -c .
48+
vendor/bin/codecept run -c . -vvv --coverage --coverage-xml=coverage.xml --xml
49+
```
50+
51+
### CLI Usage
52+
The project includes a CLI tool `bin/docker-api`.
53+
```bash
54+
# List containers
55+
bin/docker-api docker:list-containers
56+
57+
# Listen for events
58+
bin/docker-api docker:events:listen
59+
```
60+
61+
## Development Conventions
62+
63+
### Coding Style
64+
The project enforces a strict coding style using `php-cs-fixer`.
65+
* **Config:** `.php-cs-fixer.php`
66+
* **Key Rules:**
67+
* `declare(strict_types=1);` is mandatory.
68+
* Global functions should be imported (e.g., `use function implode;`).
69+
* `null` type in unions is always placed first (e.g., `?string` or `null|string`).
70+
* Binary operators are aligned.
71+
* Classes are often `final`.
72+
73+
### Code Generation Workflow
74+
1. Update the OpenAPI spec in `spec/` (e.g., `docker-v1.51-patched.yaml`).
75+
2. Run `composer generate`.
76+
3. The configuration is defined in `.jane-openapi`.
77+
78+
### Testing
79+
* Tests are located in `tests/`.
80+
* The `Unit` suite is configured in `tests/Unit.suite.yml`.
81+
* Coverage reporting includes files in `src/`.
82+
* **Mocking Final Classes:** Since many classes are `final`, avoid mocking them directly. Instead, instantiate the `final` class and inject mocked dependencies (e.g., the generated `Client`) into its constructor.
83+
84+
### Continuous Integration
85+
* Workflows are defined in `.github/workflows/`.
86+
* `php-tests.yml` runs validation, installation, and tests with coverage on multiple PHP versions.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"phpstan/phpdoc-parser": "^2.3.1",
1212
"symfony/console": "^7.4.3 || ^8.0",
1313
"symfony/http-client": "^7.4.3 || ^8.0",
14+
"symfony/http-client-contracts": "^3.6",
1415
"symfony/property-access": "^7.4.3 || ^8.0",
1516
"symfony/property-info": "^7.4.3 || ^8.0",
1617
"symfony/runtime": "^7.4.1 || ^8.0",

composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit\Client;
6+
7+
use Codeception\Test\Unit;
8+
use WebProject\DockerApi\Library\Generated\Client;
9+
use WebProject\DockerApiClient\Client\DockerApiClientWrapper;
10+
11+
final class DockerApiClientWrapperTest extends Unit
12+
{
13+
public function testGetDockerClient(): void
14+
{
15+
$mockClient = $this->createMock(Client::class);
16+
$wrapper = new DockerApiClientWrapper('http://localhost', '/var/run/docker.sock', $mockClient);
17+
18+
$this->assertSame($mockClient, $wrapper->getDockerClient());
19+
}
20+
21+
public function testCreate(): void
22+
{
23+
$wrapper = DockerApiClientWrapper::create('http://localhost', '/var/run/docker.sock');
24+
$this->assertInstanceOf(DockerApiClientWrapper::class, $wrapper);
25+
$this->assertInstanceOf(Client::class, $wrapper->getDockerClient());
26+
}
27+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit\Service;
6+
7+
use Codeception\Test\Unit;
8+
use PHPUnit\Framework\MockObject\MockObject;
9+
use WebProject\DockerApi\Library\Generated\Client;
10+
use WebProject\DockerApi\Library\Generated\Model\ContainerConfig;
11+
use WebProject\DockerApi\Library\Generated\Model\ContainerInspectResponse;
12+
use WebProject\DockerApi\Library\Generated\Model\ContainerState;
13+
use WebProject\DockerApi\Library\Generated\Model\ContainerSummary;
14+
use WebProject\DockerApi\Library\Generated\Model\NetworkSettings;
15+
use WebProject\DockerApiClient\Client\DockerApiClientWrapper;
16+
use WebProject\DockerApiClient\Dto\DockerContainerDto;
17+
use WebProject\DockerApiClient\Service\DockerService;
18+
19+
final class DockerServiceTest extends Unit
20+
{
21+
private DockerApiClientWrapper $apiClientWrapper;
22+
private Client&MockObject $client;
23+
private DockerService $service;
24+
25+
protected function _before(): void
26+
{
27+
$this->client = $this->createMock(Client::class);
28+
$this->apiClientWrapper = new DockerApiClientWrapper('http://mocked-uri', '/mocked/socket', $this->client);
29+
$this->service = new DockerService($this->apiClientWrapper);
30+
}
31+
32+
public function testFindAllContainer(): void
33+
{
34+
$containerSummary = $this->createMock(ContainerSummary::class);
35+
$containerSummary->method('getId')->willReturn('123');
36+
37+
$this->client->expects($this->once())
38+
->method('containerList')
39+
->willReturn([$containerSummary]);
40+
41+
$inspectResponse = $this->createConfiguredMock(ContainerInspectResponse::class, [
42+
'getId' => '123',
43+
'getName' => '/test-container',
44+
'getImage' => 'nginx:latest',
45+
'getState' => $this->createConfiguredMock(ContainerState::class, ['getRunning' => true]),
46+
'getConfig' => $this->createConfiguredMock(ContainerConfig::class, ['getEnv' => ['FOO=bar']]),
47+
'getNetworkSettings' => $this->createConfiguredMock(NetworkSettings::class, ['getNetworks' => [], 'getPorts' => []]),
48+
]);
49+
50+
$this->client->expects($this->once())
51+
->method('containerInspect')
52+
->with('123')
53+
->willReturn($inspectResponse);
54+
55+
$result = $this->service->findAllContainer();
56+
$dtos = iterator_to_array($result);
57+
58+
$this->assertCount(1, $dtos);
59+
$this->assertArrayHasKey('123', $dtos);
60+
$this->assertInstanceOf(DockerContainerDto::class, $dtos['123']);
61+
$this->assertSame('123', $dtos['123']->id);
62+
$this->assertSame('test-container', $dtos['123']->getName());
63+
}
64+
65+
public function testFindContainer(): void
66+
{
67+
$inspectResponse = $this->createConfiguredMock(ContainerInspectResponse::class, [
68+
'getId' => '456',
69+
'getName' => '/single-container',
70+
'getImage' => 'php:8.3',
71+
'getState' => $this->createConfiguredMock(ContainerState::class, ['getRunning' => false]),
72+
'getConfig' => $this->createConfiguredMock(ContainerConfig::class, ['getEnv' => []]),
73+
'getNetworkSettings' => $this->createConfiguredMock(NetworkSettings::class, ['getNetworks' => [], 'getPorts' => []]),
74+
]);
75+
76+
$this->client->expects($this->once())
77+
->method('containerInspect')
78+
->with('456')
79+
->willReturn($inspectResponse);
80+
81+
$dto = $this->service->findContainer('456');
82+
83+
$this->assertInstanceOf(DockerContainerDto::class, $dto);
84+
$this->assertSame('456', $dto->id);
85+
}
86+
87+
public function testFindContainerNotFound(): void
88+
{
89+
$this->client->expects($this->once())
90+
->method('containerInspect')
91+
->with('999')
92+
->willReturn(new \stdClass());
93+
94+
$dto = $this->service->findContainer('999');
95+
96+
$this->assertNull($dto);
97+
}
98+
}

0 commit comments

Comments
 (0)