diff --git a/composer.json b/composer.json index 773c728..42cc19a 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "integration:old": "phpunit tests/Integration/OldTests --bootstrap vendor/autoload.php", "cs": "php-cs-fixer fix --dry-run", "cs:fix": "php-cs-fixer fix", - "phpstan": "phpstan analyse" + "phpstan": "phpstan analyse --memory-limit=256M" }, "config": { "allow-plugins": { diff --git a/src/Container/StartedTestContainer.php b/src/Container/StartedTestContainer.php index 3a75b0b..8d1acd3 100644 --- a/src/Container/StartedTestContainer.php +++ b/src/Container/StartedTestContainer.php @@ -23,6 +23,9 @@ interface StartedTestContainer public function getIpAddress(string $networkName): string; + /** + * @return array + */ public function getLabels(): array; public function logs(): string; @@ -35,6 +38,9 @@ interface StartedTestContainer public function getNetworkId(string $networkName): string; + /** + * @return string[] + */ public function getNetworkNames(): array; public function restart(): self; diff --git a/tests/Unit/Utils/HostResolverTest.php b/tests/Unit/Utils/HostResolverTest.php new file mode 100644 index 0000000..f428b94 --- /dev/null +++ b/tests/Unit/Utils/HostResolverTest.php @@ -0,0 +1,253 @@ +createMock(Docker::class); + $resolver = new HostResolver($dummyClient); + $host = $resolver->resolveHost(); + $this->assertEquals('tcp://another:2375', $host); + } + + public function testReturnsHostnameForTcpProtocols(): void + { + $protocols = ['tcp', 'http', 'https']; + foreach ($protocols as $protocol) { + putenv('DOCKER_HOST=' . $protocol . '://docker:2375'); + // Clear any override. + putenv('TESTCONTAINERS_HOST_OVERRIDE'); + $dummyClient = $this->createMock(Docker::class); + $resolver = new HostResolver($dummyClient); + $host = $resolver->resolveHost(); + $this->assertEquals('docker', $host, "Protocol {$protocol} did not return expected hostname."); + } + } + + public function testDoesNotReturnOverrideWhenAllowUserOverridesIsFalse(): void + { + $dummyClient = $this->createMock(Docker::class); + $resolver = new class ($dummyClient) extends HostResolver { + protected function allowUserOverrides(): bool + { + return false; + } + }; + + putenv('TESTCONTAINERS_HOST_OVERRIDE=tcp://another:2375'); + putenv('DOCKER_HOST=tcp://docker:2375'); + $host = $resolver->resolveHost(); + $this->assertEquals('docker', $host); + } + + public function testReturnsLocalhostForUnixAndNpipeProtocolsWhenNotInContainer(): void + { + $dummyClient = $this->createMock(Docker::class); + $resolver = new class ($dummyClient) extends HostResolver { + protected function isInContainer(): bool + { + return false; + } + }; + + foreach (['unix://docker:2375', 'npipe://docker:2375'] as $uri) { + putenv('DOCKER_HOST=' . $uri); + putenv('TESTCONTAINERS_HOST_OVERRIDE'); + $host = $resolver->resolveHost(); + $this->assertEquals('localhost', $host, "URI {$uri} should return 'localhost' when not in a container."); + } + } + + public function testReturnsHostFromGatewayWhenRunningInContainer(): void + { + // For this test we simulate that we are in a container and the Docker client returns a gateway. + $dockerClient = $this->getMockBuilder(Docker::class) + ->disableOriginalConstructor() + ->getMock(); + + // Build a fake network inspection response: + $fakeConfig = new class () { + public function getGateway(): ?string + { + return '172.0.0.1'; + } + }; + $fakeIPAM = new class ($fakeConfig) { + /** @var object[] */ + private array $config; + public function __construct(object $config) + { + $this->config = [$config]; + } + /** @return object[] */ + public function getConfig(): array + { + return $this->config; + } + }; + $fakeNetwork = new class ($fakeIPAM) { + private object $ipam; + public function __construct(object $ipam) + { + $this->ipam = $ipam; + } + public function getIPAM(): object + { + return $this->ipam; + } + }; + + // Expect that networkInspect will be called with "bridge" (since DOCKER_HOST does not contain "podman.sock") + $dockerClient->expects($this->once()) + ->method('networkInspect') + ->with($this->equalTo('bridge')) + ->willReturn($fakeNetwork); + + // Override isInContainer() to simulate being inside a container. + $resolver = new class ($dockerClient) extends HostResolver { + protected function isInContainer(): bool + { + return true; + } + }; + + putenv('DOCKER_HOST=unix://docker:2375'); + putenv('TESTCONTAINERS_HOST_OVERRIDE'); + $host = $resolver->resolveHost(); + $this->assertEquals('172.0.0.1', $host); + } + + public function testUsesBridgeNetworkAsGatewayForDockerProvider(): void + { + // For Docker provider (non-Podman) the network used should be "bridge". + $dockerClient = $this->getMockBuilder(Docker::class) + ->disableOriginalConstructor() + ->getMock(); + // Expect networkInspect to be called with "bridge" + $dockerClient->expects($this->once()) + ->method('networkInspect') + ->with($this->equalTo('bridge')) + ->willReturn(null); // Simulate not finding a gateway + + $resolver = new class ($dockerClient) extends HostResolver { + protected function isInContainer(): bool + { + return true; + } + }; + + putenv('DOCKER_HOST=unix://docker:2375'); + $host = $resolver->resolveHost(); + // Since no gateway is found, fallback is "localhost" + $this->assertEquals('localhost', $host); + } + + public function testUsesPodmanNetworkAsGatewayForPodmanProvider(): void + { + // For Podman, DOCKER_HOST contains "podman.sock" so the network should be "podman". + $dockerClient = $this->getMockBuilder(Docker::class) + ->disableOriginalConstructor() + ->getMock(); + // Expect networkInspect to be called with "podman" + $dockerClient->expects($this->once()) + ->method('networkInspect') + ->with($this->equalTo('podman')) + ->willReturn(null); // Simulate not finding a gateway + + $resolver = new class ($dockerClient) extends HostResolver { + protected function isInContainer(): bool + { + return true; + } + }; + + putenv('DOCKER_HOST=unix://podman.sock'); + $host = $resolver->resolveHost(); + $this->assertEquals('localhost', $host); + } + + public function testReturnsHostFromDefaultGatewayWhenRunningInContainer(): void + { + // Override both findGateway() and findDefaultGateway() to simulate a missing network gateway and a default gateway result. + $dummyClient = $this->createMock(Docker::class); + $resolver = new class ($dummyClient) extends HostResolver { + protected function isInContainer(): bool + { + return true; + } + protected function findGateway(string $networkName): ?string + { + return null; + } + protected function findDefaultGateway(): ?string + { + return '172.0.0.2'; + } + }; + + putenv('DOCKER_HOST=unix://docker:2375'); + $host = $resolver->resolveHost(); + $this->assertEquals('172.0.0.2', $host); + } + + public function testReturnsLocalhostIfUnableToFindGateway(): void + { + // Override to simulate that neither network inspection nor default gateway yield a result. + $dummyClient = $this->createMock(Docker::class); + $resolver = new class ($dummyClient) extends HostResolver { + protected function isInContainer(): bool + { + return true; + } + protected function findGateway(string $networkName): ?string + { + return null; + } + protected function findDefaultGateway(): ?string + { + return null; + } + }; + + putenv('DOCKER_HOST=unix://docker:2375'); + $host = $resolver->resolveHost(); + $this->assertEquals('localhost', $host); + } + + public function testThrowsForUnsupportedProtocol(): void + { + putenv('DOCKER_HOST=invalid://unknown'); + $dummyClient = $this->createMock(Docker::class); + $resolver = new HostResolver($dummyClient); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Unsupported Docker host scheme: invalid"); + + $resolver->resolveHost(); + } +} diff --git a/tests/Unit/Utils/TarBuilderTest.php b/tests/Unit/Utils/TarBuilderTest.php index b2f326f..965b9de 100644 --- a/tests/Unit/Utils/TarBuilderTest.php +++ b/tests/Unit/Utils/TarBuilderTest.php @@ -146,7 +146,7 @@ class TarBuilderTest extends TestCase mkdir($extractDir); $this->extractTar($tarPath, $extractDir); - $scanned = array_diff(scandir($extractDir), ['.', '..']); + $scanned = array_diff(scandir($extractDir) ?: [], ['.', '..']); $this->assertCount(0, $scanned, 'Expected empty directory'); } @@ -166,7 +166,7 @@ class TarBuilderTest extends TestCase mkdir($extractDir); $this->extractTar($tarPath, $extractDir); - $scanned = array_diff(scandir($extractDir), ['.', '..']); + $scanned = array_diff(scandir($extractDir) ?: [], ['.', '..']); $this->assertCount(0, $scanned, 'Expected no files after clear()'); }