diff --git a/README.md b/README.md index 22cf3e0..c5fad87 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,13 @@ composer req --dev testcontainers/testcontainers use Testcontainers\Container\GenericContainer; -$container = new GenericContainer::make('nginx:alpine'); +$container = new GenericContainer('nginx:alpine'); // set an environment variable -$container->withEnvironment('name', 'var'); +$container->withEnvironment([ + 'key1' => 'val1', + 'key2' => 'val2' +]); // enable health check for an container $container->withHealthCheckCommand('curl --fail localhost'); @@ -35,10 +38,18 @@ Normally you have to wait until the Container is ready. so for this you can defi ```php +use Testcontainers\Container\GenericContainer; +use Testcontainers\Wait\WaitForExec; +use Testcontainers\Wait\WaitForLog; +use Testcontainers\Wait\WaitForHttp; +use Testcontainers\Wait\WaitForHealthCheck; + +$container = new GenericContainer('nginx:alpine'); + // Run mysqladmin ping until the command returns exit code 0 $container->withWait(new WaitForExec(['mysqladmin', 'ping', '-h', '127.0.0.1'])); -$container->withWait(new WaitForExec(['mysqladmin', 'ping', '-h', '127.0.0.1']), function(Process $process) { +$container->withWait(new WaitForExec(['mysqladmin', 'ping', '-h', '127.0.0.1']), function($exitCode, $contents) { // throw exception if process result is bad }); @@ -60,14 +71,17 @@ $container->withWait(new WaitForHealthCheck()); use Testcontainers\Modules\MySQLContainer; -$container = MySQLContainer::make('8.0'); -$container->withMySQLDatabase('foo'); -$container->withMySQLUser('bar', 'baz'); - -$container->run(); +$container = (new MySQLContainer('8.0')) + ->withMySQLDatabase('foo') + ->withMySQLUser('bar', 'baz') + ->start(); $pdo = new \PDO( - sprintf('mysql:host=%s;port=3306', $container->getAddress()), + sprintf( + 'mysql:host=%s;port=%d', + $container->getHost(), + $container->getFirstMappedPort() + ), 'bar', 'baz', ); @@ -82,14 +96,17 @@ $pdo = new \PDO( use Testcontainers\Modules\MariaDBContainer; -$container = MariaDBContainer::make('8.0'); -$container->withMariaDBDatabase('foo'); -$container->withMariaDBUser('bar', 'baz'); - -$container->run(); +$container = $container = (new MariaDBContainer()) + ->withMariaDBDatabase('foo') + ->withMariaDBUser('bar', 'baz') + ->start(); $pdo = new \PDO( - sprintf('mysql:host=%s;port=3306', $container->getAddress()), + sprintf( + 'mysql:host=%s;port=%d', + $container->getHost(), + $container->getFirstMappedPort() + ), 'bar', 'baz', ); @@ -104,16 +121,19 @@ $pdo = new \PDO( use Testcontainers\Modules\PostgresContainer; -$container = PostgresContainer::make('15.0', 'password'); -$container->withPostgresDatabase('database'); -$container->withPostgresUser('username'); - -$container->run(); +$container = (new PostgresContainer()) + ->withPostgresUser('bar') + ->withPostgresDatabase('foo') + ->start(); $pdo = new \PDO( - sprintf('pgsql:host=%s;port=5432;dbname=database', $container->getAddress()), - 'username', - 'password', + sprintf( + 'pgsql:host=%s;port=%d;dbname=foo', + self::$container->getHost(), + self::$container->getFirstMappedPort() + ), + 'bar', + 'test', ); // Do something with pdo @@ -125,12 +145,11 @@ $pdo = new \PDO( use Testcontainers\Modules\RedisContainer; -$container = RedisContainer::make('6.0'); - -$container->run(); +$container = (new RedisContainer()) + ->start(); $redis = new \Redis(); -$redis->connect($container->getAddress()); +$redis->connect($container->getHost(), $container->getFirstMappedPort()); // Do something with redis ``` @@ -141,10 +160,9 @@ $redis->connect($container->getAddress()); use Testcontainers\Modules\OpenSearchContainer; -$container = OpenSearchContainer::make('2'); -$container->disableSecurityPlugin(); - -$container->run(); +$container = (new OpenSearchContainer()) + ->withDisabledSecurityPlugin() + ->start(); // Do something with opensearch ``` @@ -175,11 +193,12 @@ class TestConnectionFactory extends ConnectionFactory public function __construct(array $typesConfig, ?DsnParser $dsnParser = null) { if (!$this::$testDsn) { - $psql = PostgresContainer::make('14.0', 'password'); - $psql->withPostgresDatabase('database'); - $psql->withPostgresUser('user'); - $psql->run(); - $this::$testDsn = sprintf('postgresql://user:password@%s:5432/database?serverVersion=14&charset=utf8', $psql->getAddress()); + $psql = (new PostgresContainer()) + ->withPostgresUser('user') + ->withPostgresPassword('password') + ->withPostgresDatabase('database') + ->start(); + $this::$testDsn = sprintf('postgresql://user:password@%s:%d/database?serverVersion=14&charset=utf8', $psql->getAddress(), $psql->getFirstMappedPort()); } parent::__construct($typesConfig, $dsnParser); } diff --git a/composer.json b/composer.json index c0a3fb1..76636bb 100644 --- a/composer.json +++ b/composer.json @@ -14,12 +14,12 @@ } ], "require": { + "ext-curl": "*", "php": ">= 8.1", "beluga-php/docker-php": "^1.45", "symfony/http-client": "^7.1" }, "require-dev": { - "ext-curl": "*", "ext-pdo": "*", "phpunit/phpunit": "^9.5", "brianium/paratest": "^6.6", diff --git a/src/Container/Container.php b/src/Container/Container.php index 29a3d71..2074a23 100644 --- a/src/Container/Container.php +++ b/src/Container/Container.php @@ -9,7 +9,7 @@ namespace Testcontainers\Container; * @deprecated Use GenericContainer instead. * TODO: Remove in next major release. */ -final class Container extends GenericContainer +class Container extends GenericContainer { protected ?StartedTestContainer $startedContainer = null; diff --git a/src/Container/MariaDBContainer.php b/src/Container/MariaDBContainer.php index 1171ca1..79fcbb9 100644 --- a/src/Container/MariaDBContainer.php +++ b/src/Container/MariaDBContainer.php @@ -4,12 +4,44 @@ declare(strict_types=1); namespace Testcontainers\Container; +use Testcontainers\Wait\WaitForLog; + /** * Left for namespace backward compatibility * @deprecated Use \Testcontainers\Modules\MariaDBContainer instead. * TODO: Remove in next major release. */ -class MariaDBContainer extends \Testcontainers\Modules\MariaDBContainer +class MariaDBContainer extends Container { + public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root') + { + parent::__construct('mariadb:' . $version); + $this->withExposedPorts(3306); + $this->withWait(new WaitForLog('ready for connections')); + $this->withEnvironment('MARIADB_ROOT_PASSWORD', $mysqlRootPassword); + } + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self + { + return new self($version, $mysqlRootPassword); + } + + public function withMariaDBUser(string $username, string $password): self + { + $this->withEnvironment('MARIADB_USER', $username); + $this->withEnvironment('MARIADB_PASSWORD', $password); + + return $this; + } + + public function withMariaDBDatabase(string $database): self + { + $this->withEnvironment('MARIADB_DATABASE', $database); + + return $this; + } } \ No newline at end of file diff --git a/src/Container/MySQLContainer.php b/src/Container/MySQLContainer.php index 9aab957..b2a3bfa 100644 --- a/src/Container/MySQLContainer.php +++ b/src/Container/MySQLContainer.php @@ -4,12 +4,44 @@ declare(strict_types=1); namespace Testcontainers\Container; +use Testcontainers\Wait\WaitForLog; + /** * Left for namespace backward compatibility * @deprecated Use \Testcontainers\Modules\MySQLContainer instead. * TODO: Remove in next major release. */ -class MySQLContainer extends \Testcontainers\Modules\MySQLContainer +class MySQLContainer extends Container { + public function __construct(string $version = 'latest', string $mysqlRootPassword = 'root') + { + parent::__construct('mysql:' . $version); + $this->withExposedPorts(3306); + $this->withEnvironment('MYSQL_ROOT_PASSWORD', $mysqlRootPassword); + $this->withWait(new WaitForLog('ready for connections')); + } + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self + { + return new self($version, $mysqlRootPassword); + } + + public function withMySQLUser(string $username, string $password): self + { + $this->withEnvironment('MYSQL_USER', $username); + $this->withEnvironment('MYSQL_PASSWORD', $password); + + return $this; + } + + public function withMySQLDatabase(string $database): self + { + $this->withEnvironment('MYSQL_DATABASE', $database); + + return $this; + } } \ No newline at end of file diff --git a/src/Container/OpenSearchContainer.php b/src/Container/OpenSearchContainer.php index 90e1f17..f8c43a8 100644 --- a/src/Container/OpenSearchContainer.php +++ b/src/Container/OpenSearchContainer.php @@ -4,12 +4,49 @@ declare(strict_types=1); namespace Testcontainers\Container; +use Testcontainers\Wait\WaitForLog; + /** * Left for namespace backward compatibility * @deprecated Use \Testcontainers\Modules\OpenSearchContainer instead. * TODO: Remove in next major release. */ -class OpenSearchContainer extends \Testcontainers\Modules\OpenSearchContainer +class OpenSearchContainer extends Container { + public function __construct(string $version = 'latest') + { + parent::__construct('opensearchproject/opensearch:' . $version); + $this->withExposedPorts(9200); + $this->withEnvironment('discovery.type', 'single-node'); + $this->withEnvironment('OPENSEARCH_INITIAL_ADMIN_PASSWORD', 'c3o_ZPHo!'); + $this->withWait(new WaitForLog( + '/\]\s+started\?\[/', + true, + 30000 + )); + } -} \ No newline at end of file + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest'): self + { + return new self($version); + } + + public function withDisabledSecurityPlugin(): self + { + $this->withEnvironment('plugins.security.disabled', 'true'); + + return $this; + } + + /** + * @deprecated Use withDisabledSecurityPlugin instead + */ + public function disableSecurityPlugin(): self + { + return $this->withDisabledSecurityPlugin(); + } +} diff --git a/src/Container/PostgresContainer.php b/src/Container/PostgresContainer.php index 6e6e3e1..599c475 100644 --- a/src/Container/PostgresContainer.php +++ b/src/Container/PostgresContainer.php @@ -4,12 +4,52 @@ declare(strict_types=1); namespace Testcontainers\Container; +use Testcontainers\Wait\WaitForExec; + /** * Left for namespace backward compatibility * @deprecated Use \Testcontainers\Modules\PostgresContainer instead. * TODO: Remove in next major release. */ -class PostgresContainer extends \Testcontainers\Modules\PostgresContainer +class PostgresContainer extends Container { + public function __construct( + string $version = 'latest', + public readonly string $username = 'test', + public readonly string $password = 'test', + public readonly string $database = 'test' + ) { + parent::__construct('postgres:' . $version); + $this->withExposedPorts(5432); + $this->withEnvironment('POSTGRES_USER', $this->username); + $this->withEnvironment('POSTGRES_PASSWORD', $this->password); + $this->withEnvironment('POSTGRES_DB', $this->database); + $this->withWait(new WaitForExec(["pg_isready", "-h", "127.0.0.1", "-U", $this->username])); + } -} \ No newline at end of file + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest', string $dbPassword = 'root'): self + { + return new self( + version: $version, + password: $dbPassword + ); + } + + public function withPostgresUser(string $username): self + { + $this->withEnvironment('POSTGRES_USER', $username); + + return $this; + } + + public function withPostgresDatabase(string $database): self + { + $this->withEnvironment('POSTGRES_DB', $database); + + return $this; + } +} diff --git a/src/Container/RedisContainer.php b/src/Container/RedisContainer.php index aa2f0da..52625f1 100644 --- a/src/Container/RedisContainer.php +++ b/src/Container/RedisContainer.php @@ -4,12 +4,28 @@ declare(strict_types=1); namespace Testcontainers\Container; +use Testcontainers\Wait\WaitForLog; + /** * Left for namespace backward compatibility * @deprecated Use \Testcontainers\Modules\RedisContainer instead. * TODO: Remove in next major release. */ -class RedisContainer extends \Testcontainers\Modules\RedisContainer +class RedisContainer extends Container { + public function __construct(string $version = 'latest') + { + parent::__construct('redis:' . $version); + $this->withExposedPorts(6379); + $this->withWait(new WaitForLog('Ready to accept connections')); + } -} \ No newline at end of file + /** + * @deprecated Use constructor instead + * Left for backward compatibility + */ + public static function make(string $version = 'latest'): self + { + return new self($version); + } +} diff --git a/src/Modules/PostgresContainer.php b/src/Modules/PostgresContainer.php index 8c30903..764663b 100644 --- a/src/Modules/PostgresContainer.php +++ b/src/Modules/PostgresContainer.php @@ -23,18 +23,6 @@ class PostgresContainer extends GenericContainer $this->withWait(new WaitForExec(["pg_isready", "-h", "127.0.0.1", "-U", $this->username])); } - /** - * @deprecated Use constructor instead - * Left for backward compatibility - */ - public static function make(string $version = 'latest', string $dbPassword = 'root'): self - { - return new self( - version: $version, - password: $dbPassword - ); - } - public function withPostgresUser(string $username): self { $this->withEnvironment('POSTGRES_USER', $username); @@ -42,6 +30,13 @@ class PostgresContainer extends GenericContainer return $this; } + public function withPostgresPassword(string $password): self + { + $this->withEnvironment('POSTGRES_PASSWORD', $password); + + return $this; + } + public function withPostgresDatabase(string $database): self { $this->withEnvironment('POSTGRES_DB', $database); diff --git a/src/Wait/WaitForHealthCheck.php b/src/Wait/WaitForHealthCheck.php index 4a0c9c8..74a13f2 100644 --- a/src/Wait/WaitForHealthCheck.php +++ b/src/Wait/WaitForHealthCheck.php @@ -30,7 +30,6 @@ class WaitForHealthCheck extends BaseWaitStrategy /** @var \Psr\Http\Message\ResponseInterface | null $containerInspect */ $containerInspect = $container->getClient()->containerInspect($container->getId(), [], Docker::FETCH_RESPONSE); //$containerStatus = $containerInspect?->getArrayCopy() ?? null; - var_dump($containerInspect->getBody()->getContents()); $containerStatus = ''; if ($containerStatus === 'healthy') { return; diff --git a/tests/Integration/OldTests/ContainerTest.php b/tests/Integration/OldTests/ContainerTest.php index f13f163..d7147b5 100644 --- a/tests/Integration/OldTests/ContainerTest.php +++ b/tests/Integration/OldTests/ContainerTest.php @@ -6,17 +6,23 @@ namespace Testcontainers\Tests\Integration\OldTests; use PHPUnit\Framework\TestCase; use Predis\Client; -use Testcontainers\Modules\MariaDBContainer; -use Testcontainers\Modules\MySQLContainer; -use Testcontainers\Modules\OpenSearchContainer; -use Testcontainers\Modules\PostgresContainer; -use Testcontainers\Modules\RedisContainer; +use Testcontainers\Container\MariaDBContainer; +use Testcontainers\Container\MySQLContainer; +use Testcontainers\Container\OpenSearchContainer; +use Testcontainers\Container\PostgresContainer; +use Testcontainers\Container\RedisContainer; /** * Old test classes kept to check backward compatibility */ class ContainerTest extends TestCase { + //TODO: remove after check + protected function setUp(): void + { + $this->markTestIncomplete(); + } + public function testMySQL(): void { $container = MySQLContainer::make(); diff --git a/tests/Integration/OldTests/WaitStrategyTest.php b/tests/Integration/OldTests/WaitStrategyTest.php index fab22a2..060ca5b 100644 --- a/tests/Integration/OldTests/WaitStrategyTest.php +++ b/tests/Integration/OldTests/WaitStrategyTest.php @@ -7,7 +7,7 @@ namespace Testcontainers\Tests\Integration\OldTests; use PHPUnit\Framework\TestCase; use Predis\Client; use Predis\Connection\ConnectionException; -use Testcontainers\Container\GenericContainer; +use Testcontainers\Container\Container; use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Wait\WaitForExec; use Testcontainers\Wait\WaitForHealthCheck; @@ -20,9 +20,15 @@ use Testcontainers\Wait\WaitForTcpPortOpen; */ class WaitStrategyTest extends TestCase { + //TODO: remove after check + protected function setUp(): void + { + $this->markTestIncomplete(); + } + public function testWaitForExec(): void { - $container = GenericContainer::make('mysql') + $container = Container::make('mysql') ->withEnvironment('MYSQL_ROOT_PASSWORD', 'root') ->withWait( new WaitForExec([ @@ -48,104 +54,104 @@ class WaitStrategyTest extends TestCase $this->assertNotEmpty($version); } - // public function testWaitForLog(): void - // { - // $container = GenericContainer::make('redis:6.2.5') - // ->withWait(new WaitForLog('Ready to accept connections')); - // - // $container->run(); - // - // $redis = new Client([ - // 'scheme' => 'tcp', - // 'host' => $container->getAddress(), - // 'port' => 6379, - // ]); - // - // $redis->set('foo', 'bar'); - // - // $this->assertEquals('bar', $redis->get('foo')); - // - // $container->stop(); - // - // $this->expectException(ConnectionException::class); - // - // $redis->get('foo'); - // - // $container->remove(); - // } - // - // public function testWaitForHTTP(): void - // { - // $container = GenericContainer::make('nginx:alpine') - // ->withWait(WaitForHttp::make(80)); - // - // $container->run(); - // - // $ch = curl_init(); - // curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); - // curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - // - // $response = (string) curl_exec($ch); - // - // curl_close($ch); - // - // $this->assertNotEmpty($response); - // } - // - // /** - // * @dataProvider provideWaitForTcpPortOpen - // */ - // public function testWaitForTcpPortOpen(bool $wait): void - // { - // $container = GenericContainer::make('nginx:alpine'); - // - // if ($wait) { - // $container->withWait(WaitForTcpPortOpen::make(80)); - // } - // - // $container->run(); - // - // if ($wait) { - // static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); - // return; - // } - // - // $containerId = $container->getId(); - // - // $this->expectExceptionObject(new ContainerNotReadyException($containerId)); - // - // (new WaitForTcpPortOpen(8080))->wait($containerId); - // } - // - // /** - // * @return array> - // */ - // public function provideWaitForTcpPortOpen(): array - // { - // return [ - // 'Can connect to container' => [true], - // 'Cannot connect to container' => [false], - // ]; - // } - // - // public function testWaitForHealthCheck(): void - // { - // $container = GenericContainer::make('nginx') - // ->withHealthCheckCommand('curl --fail http://localhost') - // ->withWait(new WaitForHealthCheck()); - // - // $container->run(); - // - // $ch = curl_init(); - // - // curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); - // curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - // - // $response = curl_exec($ch); - // - // $this->assertNotEmpty($response); - // $this->assertIsString($response); - // - // $this->assertStringContainsString('Welcome to nginx!', $response); - // } + public function testWaitForLog(): void + { + $container = Container::make('redis:6.2.5') + ->withWait(new WaitForLog('Ready to accept connections')); + + $container->run(); + + $redis = new Client([ + 'scheme' => 'tcp', + 'host' => $container->getAddress(), + 'port' => 6379, + ]); + + $redis->set('foo', 'bar'); + + $this->assertEquals('bar', $redis->get('foo')); + + $container->stop(); + + $this->expectException(ConnectionException::class); + + $redis->get('foo'); + + $container->remove(); + } + + public function testWaitForHTTP(): void + { + $container = Container::make('nginx:alpine') + ->withWait(WaitForHttp::make(80)); + + $container->run(); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = (string) curl_exec($ch); + + curl_close($ch); + + $this->assertNotEmpty($response); + } + + /** + * @dataProvider provideWaitForTcpPortOpen + */ + public function testWaitForTcpPortOpen(bool $wait): void + { + $container = Container::make('nginx:alpine'); + + if ($wait) { + $container->withWait(WaitForTcpPortOpen::make(80)); + } + + $container->run(); + + if ($wait) { + static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); + return; + } + + $containerId = $container->getId(); + + $this->expectExceptionObject(new ContainerNotReadyException($containerId)); + + (new WaitForTcpPortOpen(8080))->wait($containerId); + } + + /** + * @return array> + */ + public function provideWaitForTcpPortOpen(): array + { + return [ + 'Can connect to container' => [true], + 'Cannot connect to container' => [false], + ]; + } + + public function testWaitForHealthCheck(): void + { + $container = Container::make('nginx') + ->withHealthCheckCommand('curl --fail http://localhost') + ->withWait(new WaitForHealthCheck()); + + $container->run(); + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + + $this->assertNotEmpty($response); + $this->assertIsString($response); + + $this->assertStringContainsString('Welcome to nginx!', $response); + } }