@@ -402,6 +402,105 @@ public function testContainerWithNoExposedPortsReturnsNull(): void
402402 /** @Then firstExposedPort should return null */
403403 self ::assertNull ($ started ->getAddress ()->getPorts ()->firstExposedPort ());
404404 self ::assertEmpty ($ started ->getAddress ()->getPorts ()->exposedPorts ());
405+
406+ /** @And firstHostPort should return null */
407+ self ::assertNull ($ started ->getAddress ()->getPorts ()->firstHostPort ());
408+ self ::assertEmpty ($ started ->getAddress ()->getPorts ()->hostPorts ());
409+ }
410+
411+ public function testContainerWithHostPortMapping (): void
412+ {
413+ /** @Given a container with a host port mapping */
414+ $ container = TestableGenericDockerContainer::createWith (
415+ image: 'mysql:8.4 ' ,
416+ name: 'host-port ' ,
417+ client: $ this ->client
418+ )->withPortMapping (portOnHost: 33060 , portOnContainer: 3306 );
419+
420+ /** @And the Docker daemon returns a response with host port bindings */
421+ $ this ->client ->withDockerRunResponse (output: InspectResponseFixture::containerId ());
422+ $ this ->client ->withDockerInspectResponse (
423+ inspectResult: InspectResponseFixture::build (
424+ hostname: 'host-port ' ,
425+ exposedPorts: ['3306/tcp ' => (object )[]],
426+ hostPortBindings: [
427+ '3306/tcp ' => [['HostIp ' => '0.0.0.0 ' , 'HostPort ' => '33060 ' ]]
428+ ]
429+ )
430+ );
431+
432+ /** @When the container is started */
433+ $ started = $ container ->run ();
434+
435+ /** @Then the exposed port should be the container-internal port */
436+ self ::assertSame (expected: 3306 , actual: $ started ->getAddress ()->getPorts ()->firstExposedPort ());
437+
438+ /** @And the host port should be the host-mapped port */
439+ self ::assertSame (expected: 33060 , actual: $ started ->getAddress ()->getPorts ()->firstHostPort ());
440+ self ::assertSame (expected: [33060 ], actual: $ started ->getAddress ()->getPorts ()->hostPorts ());
441+ }
442+
443+ public function testContainerWithMultipleHostPortMappings (): void
444+ {
445+ /** @Given a container with multiple host port mappings */
446+ $ container = TestableGenericDockerContainer::createWith (
447+ image: 'nginx:latest ' ,
448+ name: 'multi-host-port ' ,
449+ client: $ this ->client
450+ )
451+ ->withPortMapping (portOnHost: 8080 , portOnContainer: 80 )
452+ ->withPortMapping (portOnHost: 8443 , portOnContainer: 443 );
453+
454+ /** @And the Docker daemon returns a response with multiple host port bindings */
455+ $ this ->client ->withDockerRunResponse (output: InspectResponseFixture::containerId ());
456+ $ this ->client ->withDockerInspectResponse (
457+ inspectResult: InspectResponseFixture::build (
458+ hostname: 'multi-host-port ' ,
459+ exposedPorts: ['80/tcp ' => (object )[], '443/tcp ' => (object )[]],
460+ hostPortBindings: [
461+ '80/tcp ' => [['HostIp ' => '0.0.0.0 ' , 'HostPort ' => '8080 ' ]],
462+ '443/tcp ' => [['HostIp ' => '0.0.0.0 ' , 'HostPort ' => '8443 ' ]]
463+ ]
464+ )
465+ );
466+
467+ /** @When the container is started */
468+ $ started = $ container ->run ();
469+
470+ /** @Then both exposed and host ports should be available */
471+ self ::assertSame (expected: [80 , 443 ], actual: $ started ->getAddress ()->getPorts ()->exposedPorts ());
472+ self ::assertSame (expected: [8080 , 8443 ], actual: $ started ->getAddress ()->getPorts ()->hostPorts ());
473+ self ::assertSame (expected: 8080 , actual: $ started ->getAddress ()->getPorts ()->firstHostPort ());
474+ }
475+
476+ public function testContainerWithExposedPortButNoHostBinding (): void
477+ {
478+ /** @Given a container with an exposed port but no host binding */
479+ $ container = TestableGenericDockerContainer::createWith (
480+ image: 'redis:latest ' ,
481+ name: 'no-host-bind ' ,
482+ client: $ this ->client
483+ );
484+
485+ /** @And the Docker daemon returns a response with exposed port but null host bindings */
486+ $ this ->client ->withDockerRunResponse (output: InspectResponseFixture::containerId ());
487+ $ this ->client ->withDockerInspectResponse (
488+ inspectResult: InspectResponseFixture::build (
489+ hostname: 'no-host-bind ' ,
490+ exposedPorts: ['6379/tcp ' => (object )[]],
491+ hostPortBindings: ['6379/tcp ' => null ]
492+ )
493+ );
494+
495+ /** @When the container is started */
496+ $ started = $ container ->run ();
497+
498+ /** @Then the exposed port should be available */
499+ self ::assertSame (expected: 6379 , actual: $ started ->getAddress ()->getPorts ()->firstExposedPort ());
500+
501+ /** @And the host port should be null since there is no binding */
502+ self ::assertNull ($ started ->getAddress ()->getPorts ()->firstHostPort ());
503+ self ::assertEmpty ($ started ->getAddress ()->getPorts ()->hostPorts ());
405504 }
406505
407506 public function testEnvironmentVariableReturnsEmptyStringForMissingKey (): void
0 commit comments