From 0c99ffbf0f34d383a8d80a6e58fa6a2a5290526e Mon Sep 17 00:00:00 2001 From: AdzerKI Date: Thu, 22 Oct 2026 21:47:34 +0300 Subject: [PATCH 1/2] fix: skip docker-php server plugins for the factory HTTP client --- src/ContainerClient/DockerContainerClient.php | 4 +- .../DockerContainerClientTest.php | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/ContainerClient/DockerContainerClient.php b/src/ContainerClient/DockerContainerClient.php index e308a68..a908244 100644 --- a/src/ContainerClient/DockerContainerClient.php +++ b/src/ContainerClient/DockerContainerClient.php @@ -91,12 +91,14 @@ private static function createHttpClient(): ClientInterface /** * Builds the DockerClient from the given HTTP client, using the injected factory if set. + * The server plugins are applied only to an injected HTTP client: the client from + * DockerClientFactory already adds the daemon address and the negotiated API path. */ private static function createDockerClient(ClientInterface $httpClient): DockerClient { return self::$dockerClientFactory !== null ? (self::$dockerClientFactory)($httpClient) - : DockerClient::create($httpClient); + : DockerClient::create($httpClient, [], [], self::$httpClientFactory !== null); } /** diff --git a/tests/Unit/ContainerClient/DockerContainerClientTest.php b/tests/Unit/ContainerClient/DockerContainerClientTest.php index be9a3c5..08b1965 100644 --- a/tests/Unit/ContainerClient/DockerContainerClientTest.php +++ b/tests/Unit/ContainerClient/DockerContainerClientTest.php @@ -247,6 +247,72 @@ public function testHeaderDefaultsPluginIsConfiguredWithUserAgentHeader(): void ); } + // ------------------------------------------------------------------------- + // API version path of the default client + // ------------------------------------------------------------------------- + + public function testDefaultClientUsesNegotiatedApiVersionPathOnce(): void + { + // A daemon whose maximum API version is below the installed docker-php-api line, + // as Podman reports. Requests must carry only the negotiated version path. + $log = (string) tempnam(sys_get_temp_dir(), 'tc-docker-api-log'); + $router = (string) tempnam(sys_get_temp_dir(), 'tc-docker-api-router'); + file_put_contents($router, <<<'PHP' + assertNotFalse($probe); + $address = (string) stream_socket_get_name($probe, false); + fclose($probe); + + $server = proc_open( + [PHP_BINARY, '-S', $address, $router], + [1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']], + $pipes, + null, + ['TC_DOCKER_API_LOG' => $log] + ); + $this->assertIsResource($server); + + $dockerHost = getenv('DOCKER_HOST'); + $apiVersion = getenv('DOCKER_API_VERSION'); + try { + $deadline = microtime(true) + 5; + while (($connection = @fsockopen('tcp://' . $address)) === false && microtime(true) < $deadline) { + usleep(50000); + } + $this->assertNotFalse($connection, 'The stub Docker daemon did not start'); + fclose($connection); + + putenv('DOCKER_HOST=tcp://' . $address); + putenv('DOCKER_API_VERSION'); + + DockerContainerClient::getDockerClient(); + + $this->assertSame(['/_ping', '/v1.41/info'], file($log, FILE_IGNORE_NEW_LINES)); + } finally { + putenv($dockerHost === false ? 'DOCKER_HOST' : 'DOCKER_HOST=' . $dockerHost); + putenv($apiVersion === false ? 'DOCKER_API_VERSION' : 'DOCKER_API_VERSION=' . $apiVersion); + proc_terminate($server); + proc_close($server); + unlink($router); + unlink($log); + } + } + public function testUserAgentHeaderContainsUnknownWhenVersionResolutionFails(): void { // Injects no-socket factories (same pattern as above), then calls From 31c2f2a4ae0e480e8537ba476255369b31a06045 Mon Sep 17 00:00:00 2001 From: AdzerKI Date: Fri, 23 Oct 2026 21:51:35 +0300 Subject: [PATCH 2/2] test: mock the daemon responses instead of a PHP server --- .../DockerContainerClientTest.php | 75 +++++++------------ 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/tests/Unit/ContainerClient/DockerContainerClientTest.php b/tests/Unit/ContainerClient/DockerContainerClientTest.php index 08b1965..43d100c 100644 --- a/tests/Unit/ContainerClient/DockerContainerClientTest.php +++ b/tests/Unit/ContainerClient/DockerContainerClientTest.php @@ -7,8 +7,13 @@ use Docker\Docker as DockerClient; use Http\Client\Common\Plugin\HeaderDefaultsPlugin; use Http\Client\Common\PluginClient; +use Http\Client\Common\PluginClientFactory; +use Http\Client\HttpAsyncClient; +use Http\Discovery\Psr17FactoryDiscovery; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; use Testcontainers\ContainerClient\DockerContainerClient; /** @@ -255,62 +260,38 @@ public function testDefaultClientUsesNegotiatedApiVersionPathOnce(): void { // A daemon whose maximum API version is below the installed docker-php-api line, // as Podman reports. Requests must carry only the negotiated version path. - $log = (string) tempnam(sys_get_temp_dir(), 'tc-docker-api-log'); - $router = (string) tempnam(sys_get_temp_dir(), 'tc-docker-api-router'); - file_put_contents($router, <<<'PHP' - createMock(ClientInterface::class); + $daemon->method('sendRequest')->willReturnCallback( + static function (RequestInterface $request) use (&$paths): ResponseInterface { + $path = $request->getUri()->getPath(); + $paths[] = $path; + $responses = Psr17FactoryDiscovery::findResponseFactory(); + $streams = Psr17FactoryDiscovery::findStreamFactory(); + + return match ($path) { + '/_ping' => $responses->createResponse(200)->withHeader('API-Version', '1.41')->withBody($streams->createStream('OK')), + '/v1.41/info' => $responses->createResponse(200)->withBody($streams->createStream('{}')), + default => $responses->createResponse(404)->withBody($streams->createStream('404 page not found')), + }; } - PHP); - - $probe = stream_socket_server('tcp://127.0.0.1:0'); - $this->assertNotFalse($probe); - $address = (string) stream_socket_get_name($probe, false); - fclose($probe); - - $server = proc_open( - [PHP_BINARY, '-S', $address, $router], - [1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']], - $pipes, - null, - ['TC_DOCKER_API_LOG' => $log] ); - $this->assertIsResource($server); - $dockerHost = getenv('DOCKER_HOST'); $apiVersion = getenv('DOCKER_API_VERSION'); + putenv('DOCKER_API_VERSION'); + PluginClientFactory::setFactory( + static fn (ClientInterface|HttpAsyncClient $socketClient, array $plugins): PluginClient => new PluginClient($daemon, $plugins) + ); try { - $deadline = microtime(true) + 5; - while (($connection = @fsockopen('tcp://' . $address)) === false && microtime(true) < $deadline) { - usleep(50000); - } - $this->assertNotFalse($connection, 'The stub Docker daemon did not start'); - fclose($connection); - - putenv('DOCKER_HOST=tcp://' . $address); - putenv('DOCKER_API_VERSION'); - DockerContainerClient::getDockerClient(); - - $this->assertSame(['/_ping', '/v1.41/info'], file($log, FILE_IGNORE_NEW_LINES)); } finally { - putenv($dockerHost === false ? 'DOCKER_HOST' : 'DOCKER_HOST=' . $dockerHost); + PluginClientFactory::setFactory( + static fn (ClientInterface|HttpAsyncClient $client, array $plugins): PluginClient => new PluginClient($client, $plugins) + ); putenv($apiVersion === false ? 'DOCKER_API_VERSION' : 'DOCKER_API_VERSION=' . $apiVersion); - proc_terminate($server); - proc_close($server); - unlink($router); - unlink($log); } + + $this->assertSame(['/_ping', '/v1.41/info'], $paths); } public function testUserAgentHeaderContainsUnknownWhenVersionResolutionFails(): void