From b7b8b1ad0c24db403b25982e96c845f77972b361 Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Sun, 20 Feb 2022 18:55:44 +0200 Subject: [PATCH 01/12] Fix Sqlite primary key column detection on PHP 8.1 --- src/Codeception/Lib/Driver/Sqlite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codeception/Lib/Driver/Sqlite.php b/src/Codeception/Lib/Driver/Sqlite.php index c6ad0a9f..242d9306 100644 --- a/src/Codeception/Lib/Driver/Sqlite.php +++ b/src/Codeception/Lib/Driver/Sqlite.php @@ -70,7 +70,7 @@ public function getPrimaryKey(string $tableName): array $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($columns as $column) { - if ($column['pk'] !== '0') { + if ($column['pk'] !== '0' && $column['pk'] !== 0) { $primaryKey []= $column['name']; } } From cbc1954901b4a3e5bf2e7bb81e1868757fb54c41 Mon Sep 17 00:00:00 2001 From: Thomas Faurbye Nielsen Date: Sat, 5 Mar 2022 19:49:46 +0100 Subject: [PATCH 02/12] Add skip_cleanup_if_failed option for not cleaning up failed tests (#30) * Add no_cleanup_failed option for not cleaning up failed tests * Update to handle all databases * Change name from 'no_cleanup_failed' to 'skip_cleanup_if_failed' --- src/Codeception/Module/Db.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 8d41bba9..6c116b60 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -62,7 +62,7 @@ * * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher) * * databases - include more database configs and switch between them in tests. * * initial_queries - list of queries to be executed right after connection to the database has been initiated, i.e. creating the database if it does not exist or preparing the database collation - * + * * skip_cleanup_if_failed - Do not perform the cleanup if the tests failed. If this is used, manual cleanup might be required when re-running * ## Example * * modules: @@ -76,6 +76,7 @@ * cleanup: true * reconnect: true * waitlock: 10 + * skip_cleanup_if_failed: true * ssl_key: '/path/to/client-key.pem' * ssl_cert: '/path/to/client-cert.pem' * ssl_ca: '/path/to/ca-cert.pem' @@ -260,6 +261,7 @@ class Db extends Module implements DbInterface 'waitlock' => 0, 'dump' => null, 'populator' => null, + 'skip_cleanup_if_failed' => false, ]; /** @@ -635,6 +637,15 @@ public function _before(TestInterface $test): void parent::_before($test); } + public function _failed(TestInterface $test, $fail) + { + foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { + if ($databaseConfig['skip_cleanup_if_failed'] ?? false) { + $this->insertedRows[$databaseKey] = []; + } + } + } + public function _after(TestInterface $test): void { $this->removeInsertedForDatabases(); @@ -748,7 +759,8 @@ protected function loadDumpUsingDriver(string $databaseKey): void } /** - * Inserts an SQL record into a database. This record will be erased after the test. + * Inserts an SQL record into a database. This record will be erased after the test, + * unless you've configured "skip_cleanup_if_failed", and the test fails. * * ```php * Date: Sat, 5 Mar 2022 20:54:50 +0200 Subject: [PATCH 03/12] Null safety in destructor --- src/Codeception/Lib/Driver/Db.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index b69017ef..fdcdb4e4 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -91,7 +91,7 @@ public function __construct(string $dsn, string $user = null, string $password = public function __destruct() { - if ($this->dbh->inTransaction()) { + if ($this->dbh !== null && $this->dbh->inTransaction()) { $this->dbh->rollBack(); } From b65b3ee39274a52021166815edff6c71682b7cd7 Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Thu, 23 Jun 2022 10:11:38 +0300 Subject: [PATCH 04/12] 2.x branch is compatible with Codeception 4 only --- composer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a4038822..95fbd01d 100644 --- a/composer.json +++ b/composer.json @@ -18,10 +18,7 @@ "php": "^7.4 | ^8.0", "ext-json": "*", "ext-pdo": "*", - "codeception/codeception": "*@dev" - }, - "conflict": { - "codeception/codeception": "<4.0" + "codeception/codeception": "^4.1" }, "autoload":{ "classmap": ["src/"] From fba7f0664d129cfebdace3c02d6404704117535d Mon Sep 17 00:00:00 2001 From: Jonathan Massuchetti Date: Thu, 1 Dec 2022 19:27:46 +0100 Subject: [PATCH 05/12] feat: use rows value to delete inserted row if primary key is filled --- src/Codeception/Module/Db.php | 9 +++++++-- tests/data/dumps/mysql.sql | 12 +++++++++++- tests/unit/Codeception/Module/Db/MySqlDbTest.php | 12 ++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 6c116b60..d41d3340 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -801,8 +801,13 @@ private function addInsertedRow(string $table, array $row, $id): void $primaryKey = $this->_getDriver()->getPrimaryKey($table); $primary = []; if ($primaryKey !== []) { - if ($id && count($primaryKey) === 1) { - $primary [$primaryKey[0]] = $id; + $filledKeys = array_intersect($primaryKey, array_keys($row)); + $primaryKeyIsFilled = count($filledKeys) === count($primaryKey); + + if ($primaryKeyIsFilled) { + $primary = array_intersect_key($row, array_flip($primaryKey)); + } elseif ($id && count($primaryKey) === 1) { + $primary[$primaryKey[0]] = $id; } else { foreach ($primaryKey as $column) { if (isset($row[$column])) { diff --git a/tests/data/dumps/mysql.sql b/tests/data/dumps/mysql.sql index 4102f7ef..b1ca296c 100644 --- a/tests/data/dumps/mysql.sql +++ b/tests/data/dumps/mysql.sql @@ -94,8 +94,18 @@ CREATE TABLE `no_pk` ( `status` varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `auto_increment_not_on_pk` ( + `id` int(11) NOT NULL, + `counter` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE INDEX counter ON `auto_increment_not_on_pk` (counter); +ALTER TABLE `auto_increment_not_on_pk` + MODIFY counter int AUTO_INCREMENT; + CREATE TABLE `empty_table` ( `id` int(11) NOT NULL AUTO_INCREMENT, `field` varchar(255), PRIMARY KEY(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 7fb56397..c0cf32e9 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -101,4 +101,16 @@ public function testGrabColumnFromDatabase() ], $emails); } + + public function testHaveInDatabaseAutoIncrementOnANonPrimaryKey() + { + $testData = [ + 'id' => 777, + ]; + $this->module->haveInDatabase('auto_increment_not_on_pk', $testData); + $this->module->seeInDatabase('auto_increment_not_on_pk', $testData); + $this->module->_after(Stub::makeEmpty(TestInterface::class)); + + $this->module->dontSeeInDatabase('auto_increment_not_on_pk', $testData); + } } From 6a50bb3bc78b8968b060758b3d5532e1448b84d5 Mon Sep 17 00:00:00 2001 From: Jonathan Massuchetti Date: Sat, 3 Dec 2022 00:38:52 +0100 Subject: [PATCH 06/12] feat: support auto increment on a composite pk --- src/Codeception/Module/Db.php | 14 ++++++++------ tests/data/dumps/mysql.sql | 7 +++++++ tests/unit/Codeception/Module/Db/MySqlDbTest.php | 16 ++++++++++++++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index d41d3340..9660ad52 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -759,8 +759,8 @@ protected function loadDumpUsingDriver(string $databaseKey): void } /** - * Inserts an SQL record into a database. This record will be erased after the test, - * unless you've configured "skip_cleanup_if_failed", and the test fails. + * Inserts an SQL record into a database. This record will be erased after the test, + * unless you've configured "skip_cleanup_if_failed", and the test fails. * * ```php * module->_before($testCase1); - + $connection1 = $this->module->dbh->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); $this->module->_after($testCase1); @@ -83,7 +83,7 @@ public function testInitialQueriesAreExecuted() ]; $this->module->_reconfigure($config); $this->module->_before(Stub::makeEmpty(TestInterface::class)); - + $usedDatabaseName = $this->module->dbh->query('SELECT DATABASE();')->fetch(PDO::FETCH_COLUMN); $this->assertSame($dbName, $usedDatabaseName); @@ -113,4 +113,16 @@ public function testHaveInDatabaseAutoIncrementOnANonPrimaryKey() $this->module->dontSeeInDatabase('auto_increment_not_on_pk', $testData); } + + public function testHaveInDatabaseAutoIncrementOnCompositePrimaryKey() + { + $testData = [ + 'id' => 777, + ]; + $this->module->haveInDatabase('auto_increment_on_composite_pk', $testData); + $this->module->seeInDatabase('auto_increment_on_composite_pk', $testData); + $this->module->_after(Stub::makeEmpty(TestInterface::class)); + + $this->module->dontSeeInDatabase('auto_increment_on_composite_pk', $testData); + } } From 32afb4bfd7837192df63879c5f152b325ea12da1 Mon Sep 17 00:00:00 2001 From: Jesus The Hun Date: Sat, 3 Dec 2022 10:35:37 +0100 Subject: [PATCH 07/12] Add grabEntryFromDatabase and grabEntriesFromDatabase methods * feat: mysql helper to grab entire rows * fix: use semantic assertions * feat: grabEntryFromDatabase makes the test fail if no row is found * misc: code style * Fix syntax error made while resolving conflict Co-authored-by: Gintautas Miselis --- src/Codeception/Module/Db.php | 73 ++++++++++++++++++- .../Codeception/Module/Db/AbstractDbTest.php | 2 +- .../Codeception/Module/Db/MySqlDbTest.php | 50 +++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 9660ad52..20e3cae7 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -729,7 +729,7 @@ public function _loadDump(string $databaseKey = null, array $databaseConfig = nu $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; - if ($databaseConfig['populator']) { + if (!empty($databaseConfig['populator'])) { $this->loadDumpUsingPopulator($databaseKey, $databaseConfig); return; } @@ -956,6 +956,77 @@ public function grabFromDatabase(string $table, string $column, array $criteria return $this->proceedSeeInDatabase($table, $column, $criteria); } + /** + * Fetches a whole entry from a database. + * Make the test fail if the entry is not found. + * Provide table name, desired column and criteria. + * + * ``` php + * grabEntryFromDatabase('users', array('name' => 'Davert')); + * ``` + * Comparison expressions can be used as well: + * + * ```php + * grabEntryFromDatabase('posts', ['num_comments >=' => 100]); + * $user = $I->grabEntryFromDatabase('users', ['email like' => 'miles%']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * @return array Returns a single entry value + * @throws PDOException|Exception + */ + public function grabEntryFromDatabase(string $table, array $criteria = []) + { + $query = $this->_getDriver()->select('*', $table, $criteria); + $parameters = array_values($criteria); + $this->debugSection('Query', $query); + $this->debugSection('Parameters', $parameters); + $sth = $this->_getDriver()->executeQuery($query, $parameters); + + $result = $sth->fetch(PDO::FETCH_ASSOC, 0); + + if ($result === false) { + throw new \AssertionError("No matching row found"); + } + + return $result; + } + + /** + * Fetches a set of entries from a database. + * Provide table name and criteria. + * + * ``` php + * grabEntriesFromDatabase('users', array('name' => 'Davert')); + * ``` + * Comparison expressions can be used as well: + * + * ```php + * grabEntriesFromDatabase('posts', ['num_comments >=' => 100]); + * $user = $I->grabEntriesFromDatabase('users', ['email like' => 'miles%']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * @return array Returns an array of all matched rows + * @throws PDOException|Exception + */ + public function grabEntriesFromDatabase(string $table, array $criteria = []) + { + $query = $this->_getDriver()->select('*', $table, $criteria); + $parameters = array_values($criteria); + $this->debugSection('Query', $query); + $this->debugSection('Parameters', $parameters); + $sth = $this->_getDriver()->executeQuery($query, $parameters); + + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + /** * Returns the number of rows in a database * diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index 9f4ccfe9..325482c8 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -175,7 +175,7 @@ public function testLoadWithPopulator() 'cleanup' => true, ] ); - $this->module->_loadDump(); + $this->module->_loadDump(null, $this->getConfig()); $this->assertTrue($this->module->_isPopulated()); $this->module->seeInDatabase('users', ['name' => 'davert']); } diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 27c10599..8e083b8b 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -91,6 +91,7 @@ public function testInitialQueriesAreExecuted() public function testGrabColumnFromDatabase() { + $this->module->_beforeSuite(); $emails = $this->module->grabColumnFromDatabase('users', 'email'); $this->assertSame( [ @@ -102,6 +103,55 @@ public function testGrabColumnFromDatabase() $emails); } + public function testGrabEntryFromDatabaseShouldFailIfNotFound() + { + try { + $this->module->grabEntryFromDatabase('users', ['email' => 'doesnot@exist.info']); + $this->fail("should have thrown an exception"); + } catch (\Throwable $t) { + $this->assertInstanceOf(AssertionError::class, $t); + } + } + + public function testGrabEntryFromDatabaseShouldReturnASingleEntry() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntryFromDatabase('users', ['is_active' => true]); + + $this->assertArrayNotHasKey(0, $result); + } + + public function testGrabEntryFromDatabaseShouldReturnAnAssocArray() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntryFromDatabase('users', ['is_active' => true]); + + $this->assertArrayHasKey('is_active', $result); + } + + public function testGrabEntriesFromDatabaseShouldReturnAnEmptyArrayIfNoRowMatches() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntriesFromDatabase('users', ['email' => 'doesnot@exist.info']); + $this->assertEquals([], $result); + } + + public function testGrabEntriesFromDatabaseShouldReturnAllMatchedRows() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntriesFromDatabase('users', ['is_active' => true]); + + $this->assertCount(3, $result); + } + + public function testGrabEntriesFromDatabaseShouldReturnASetOfAssocArray() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntriesFromDatabase('users', ['is_active' => true]); + + $this->assertEquals(true, array_key_exists('is_active', $result[0])); + } + public function testHaveInDatabaseAutoIncrementOnANonPrimaryKey() { $testData = [ From cf3b77e795b5c02feefd0f26d8bb5d5878b8028c Mon Sep 17 00:00:00 2001 From: Jesus The Hun Date: Sat, 3 Dec 2022 10:38:30 +0100 Subject: [PATCH 08/12] add Dockerfiles and docker-compose for local testing * add Dockerfiles and docker-compose for local testing * fix: env defaults * fix: populator requires config * fix: use cli base image and import composer from official image * fix: key check triggering warning * fix: exclude Dockerfiles from git archives Co-authored-by: Gintautas Miselis --- .gitattributes | 1 + docker-compose.yml | 42 +++++++++++++++++++ php74.Dockerfile | 33 +++++++++++++++ php81.Dockerfile | 33 +++++++++++++++ .../Codeception/Module/Db/MySqlDbTest.php | 6 ++- 5 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 docker-compose.yml create mode 100644 php74.Dockerfile create mode 100644 php81.Dockerfile diff --git a/.gitattributes b/.gitattributes index 87f36790..6e7735a0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ /Robofile.php export-ignore /*.md export-ignore /*.yml export-ignore +/*.Dockerfile export-ignore diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2de6dfef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +version: "3.9" + +services: + + php74: + image: codeception-module-db-php74:2.2.0 + build: + context: . + dockerfile: ./php74.Dockerfile + environment: + MYSQL_DSN: "mysql:host=host.docker.internal;port=3102;dbname=codeception" + MYSQL_USER: root + MYSQL_PASSWORD: codeception + XDEBUG_MODE: "debug" + XDEBUG_CONFIG: "client_host=host.docker.internal; client_port=9000; mode=debug; start_wih_request=1" + PHP_IDE_CONFIG: "serverName=codeception-module-db" # the name must be the same as in your PHP -> Server -> "name" field + volumes: + - ".:/var/www/html" + + php81: + image: codeception-module-db-php81:2.2.0 + build: + context: . + dockerfile: ./php81.Dockerfile + environment: + MYSQL_DSN: "mysql:host=host.docker.internal;port=3102;dbname=codeception" + MYSQL_USER: root + MYSQL_PASSWORD: codeception + XDEBUG_MODE: "debug" + XDEBUG_CONFIG: "client_host=host.docker.internal; client_port=9000; mode=debug; start_wih_request=1" + PHP_IDE_CONFIG: "serverName=codeception-module-db" # the name must be the same as in your PHP -> Server -> "name" field + volumes: + - ".:/var/www/html" + + mariadb105: + image: mariadb:10.5 + environment: + MARIADB_ROOT_PASSWORD: codeception + MARIADB_DATABASE: codeception + ports: + - "3102:3306" + diff --git a/php74.Dockerfile b/php74.Dockerfile new file mode 100644 index 00000000..0b13fe86 --- /dev/null +++ b/php74.Dockerfile @@ -0,0 +1,33 @@ +FROM php:7.4-cli + +RUN apt-get update && \ + apt-get install -y \ + unzip \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + mariadb-client-10.5 + +RUN docker-php-ext-install pdo pdo_mysql && docker-php-ext-enable pdo pdo_mysql +RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli +RUN docker-php-ext-install zip + +RUN pecl install xdebug-3.1.5 && \ + echo zend_extension=xdebug.so > $PHP_INI_DIR/conf.d/xdebug.ini + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +COPY composer.json . +COPY composer.lock . + +RUN composer install --no-autoloader + +COPY . . + +RUN composer dump-autoload -o + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/php81.Dockerfile b/php81.Dockerfile new file mode 100644 index 00000000..9e6b8bcd --- /dev/null +++ b/php81.Dockerfile @@ -0,0 +1,33 @@ +FROM php:8.1-cli + +RUN apt-get update && \ + apt-get install -y \ + unzip \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + mariadb-client-10.5 + +RUN docker-php-ext-install pdo pdo_mysql && docker-php-ext-enable pdo pdo_mysql +RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli +RUN docker-php-ext-install zip + +RUN pecl install xdebug-3.1.5 && \ + echo zend_extension=xdebug.so > $PHP_INI_DIR/conf.d/xdebug.ini + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +COPY composer.json . +COPY composer.lock . + +RUN composer install --no-autoloader + +COPY . . + +RUN composer dump-autoload -o + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 8e083b8b..058aa4f5 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -23,11 +23,13 @@ public function getPopulator(): string public function getConfig(): array { $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost'; + $user = getenv('MYSQL_USER') ? getenv('MYSQL_USER') : 'root'; $password = getenv('MYSQL_PASSWORD') ? getenv('MYSQL_PASSWORD') : ''; + $dsn = getenv('MYSQL_DSN') ? getenv('MYSQL_DSN') : 'mysql:host='.$host.';dbname=codeception_test'; return [ - 'dsn' => 'mysql:host='.$host.';dbname=codeception_test', - 'user' => 'root', + 'dsn' => $dsn, + 'user' => $user, 'password' => $password, 'dump' => 'tests/data/dumps/mysql.sql', 'reconnect' => true, From 65c5ed9d56825e419ea9954eaf8fdcaf7da5b5ed Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Sat, 3 Dec 2022 11:51:05 +0200 Subject: [PATCH 09/12] Remove unnecessary and incorrect @return annotation --- src/Codeception/Module/Db.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 20e3cae7..1ad42e6b 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -529,7 +529,6 @@ private function readSql($databaseKey = null, $databaseConfig = null): void } /** - * @return bool|null|string|string[] * @throws ModuleConfigException */ private function readSqlFile(string $filePath): ?string From fd38a56259bc395553fe71d2f7a1cf9beaae9658 Mon Sep 17 00:00:00 2001 From: rizort Date: Sat, 18 Mar 2023 09:29:16 +0200 Subject: [PATCH 10/12] Throw exception with advice to increase pcre.backtrack_limit if preg_replace returned null during dump loading. --- src/Codeception/Module/Db.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 1ad42e6b..51978f31 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -529,7 +529,7 @@ private function readSql($databaseKey = null, $databaseConfig = null): void } /** - * @throws ModuleConfigException + * @throws ModuleConfigException|ModuleException */ private function readSqlFile(string $filePath): ?string { @@ -545,7 +545,16 @@ private function readSqlFile(string $filePath): ?string $sql = file_get_contents(Configuration::projectDir() . $filePath); // remove C-style comments (except MySQL directives) - return preg_replace('#/\*(?!!\d+).*?\*/#s', '', $sql); + $replaced = preg_replace('#/\*(?!!\d+).*?\*/#s', '', $sql); + + if (!empty($sql) && is_null($replaced)) { + throw new ModuleException( + __CLASS__, + "Please, increase pcre.backtrack_limit value in PHP CLI config" + ); + } + + return $replaced; } private function connect($databaseKey, $databaseConfig): void From 96fcd4c03ebd17d00b37cd8b606c28e8a70d6e2e Mon Sep 17 00:00:00 2001 From: Sergei Matros Date: Sat, 18 Mar 2023 09:35:25 +0200 Subject: [PATCH 11/12] Fixed encoding of binary values in assertion messages Co-authored-by: sm --- src/Codeception/Lib/Driver/Db.php | 7 +++++++ src/Codeception/Module/Db.php | 6 +++--- tests/data/dumps/mysql.sql | 9 +++++---- tests/data/dumps/postgres.sql | 12 +++++++----- tests/data/dumps/sqlite.sql | 10 +++++----- tests/data/sqlite.db | Bin 36864 -> 36864 bytes .../Codeception/Module/Db/AbstractDbTest.php | 7 +++++++ 7 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index fdcdb4e4..b5680459 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -294,6 +294,8 @@ public function executeQuery($query, array $params): PDOStatement $type = PDO::PARAM_BOOL; } elseif (is_int($param)) { $type = PDO::PARAM_INT; + } elseif ($this->isBinary($param)) { + $type = PDO::PARAM_LOB; } else { $type = PDO::PARAM_STR; } @@ -342,4 +344,9 @@ public function getOptions(): array { return $this->options; } + + protected function isBinary(string $string): bool + { + return false === mb_detect_encoding($string, null, true); + } } diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 51978f31..2464bda0 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -845,7 +845,7 @@ public function seeInDatabase(string $table, array $criteria = []): void $this->assertGreaterThan( 0, $res, - 'No matching records found for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR) . ' in table ' . $table + 'No matching records found for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE) . ' in table ' . $table ); } @@ -871,7 +871,7 @@ public function seeNumRecords(int $expectedNumber, string $table, array $criteri 'The number of found rows (%d) does not match expected number %d for criteria %s in table %s', $actualNumber, $expectedNumber, - json_encode($criteria, JSON_THROW_ON_ERROR), + json_encode($criteria, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE), $table ) ); @@ -883,7 +883,7 @@ public function dontSeeInDatabase(string $table, array $criteria = []): void $this->assertLessThan( 1, $count, - 'Unexpectedly found matching records for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR) . ' in table ' . $table + 'Unexpectedly found matching records for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE) . ' in table ' . $table ); } diff --git a/tests/data/dumps/mysql.sql b/tests/data/dumps/mysql.sql index 3f9059fc..3617afd5 100644 --- a/tests/data/dumps/mysql.sql +++ b/tests/data/dumps/mysql.sql @@ -16,6 +16,7 @@ insert into `groups`(`id`,`name`,`enabled`,`created_at`) values (2,'jazzman',0, CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, + `uuid` binary(16) DEFAULT NULL, `name` varchar(30) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `is_active` bit(1) DEFAULT b'1', @@ -24,13 +25,13 @@ CREATE TABLE `users` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (1,'davert','davert@mail.ua', b'1','2012-02-01 21:17:04'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (1,0x11edc34b01d972fa9c1d0242ac120006,'davert','davert@mail.ua', b'1','2012-02-01 21:17:04'); -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (2,'nick','nick@mail.ua', b'1','2012-02-01 21:17:15'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (2,null,'nick','nick@mail.ua', b'1','2012-02-01 21:17:15'); -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (3,'miles','miles@davis.com', b'1','2012-02-01 21:17:25'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (3,null,'miles','miles@davis.com', b'1','2012-02-01 21:17:25'); -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (4,'bird','charlie@parker.com', b'0','2012-02-01 21:17:39'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (4,null,'bird','charlie@parker.com', b'0','2012-02-01 21:17:39'); diff --git a/tests/data/dumps/postgres.sql b/tests/data/dumps/postgres.sql index a95dfe0f..13b87d0e 100755 --- a/tests/data/dumps/postgres.sql +++ b/tests/data/dumps/postgres.sql @@ -28,6 +28,7 @@ SET default_with_oids = false; DROP TABLE IF EXISTS users CASCADE; CREATE TABLE users ( name character varying(30), + uuid bytea, email character varying(50), created_at timestamp without time zone DEFAULT now(), id integer NOT NULL @@ -181,6 +182,7 @@ ALTER SEQUENCE permissions_id_seq OWNED BY permissions.id; DROP TABLE IF EXISTS users CASCADE; CREATE TABLE users ( name character varying(30), + uuid bytea, email character varying(50), created_at timestamp without time zone DEFAULT now(), id integer NOT NULL @@ -332,11 +334,11 @@ SELECT pg_catalog.setval('permissions_id_seq', 10, true); -- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: - -- -COPY users (name, email, created_at, id) FROM stdin; -davert davert@mail.ua \N 1 -nick nick@mail.ua 2012-02-02 22:30:31.748 2 -miles miles@davis.com 2012-02-02 22:30:52.166 3 -bird charlie@parker.com 2012-02-02 22:32:13.107 4 +COPY users (name, uuid, email, created_at, id) FROM stdin; +davert \\x11edc34b01d972fa9c1d0242ac120006 davert@mail.ua \N 1 +nick NULL nick@mail.ua 2012-02-02 22:30:31.748 2 +miles NULL miles@davis.com 2012-02-02 22:30:52.166 3 +bird NULL charlie@parker.com 2012-02-02 22:32:13.107 4 \. diff --git a/tests/data/dumps/sqlite.sql b/tests/data/dumps/sqlite.sql index 4fbfeb95..87d65cd9 100755 --- a/tests/data/dumps/sqlite.sql +++ b/tests/data/dumps/sqlite.sql @@ -11,11 +11,11 @@ INSERT INTO "permissions" VALUES(5,3,2,'member'); INSERT INTO "permissions" VALUES(7,4,2,'admin'); DROP TABLE IF EXISTS "users"; -CREATE TABLE "users" ("name" VARCHAR, "email" VARCHAR, "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP); -INSERT INTO "users" VALUES('davert','davert@mail.ua','2012-02-01 21:17:04'); -INSERT INTO "users" VALUES('nick','nick@mail.ua','2012-02-01 21:17:15'); -INSERT INTO "users" VALUES('miles','miles@davis.com','2012-02-01 21:17:25'); -INSERT INTO "users" VALUES('bird','charlie@parker.com','2012-02-01 21:17:39'); +CREATE TABLE "users" ("name" VARCHAR, "uuid" BLOB DEFAULT NULL, "email" VARCHAR, "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP); +INSERT INTO "users" VALUES('davert',X'11edc34b01d972fa9c1d0242ac120006','davert@mail.ua','2012-02-01 21:17:04'); +INSERT INTO "users" VALUES('nick',null,'nick@mail.ua','2012-02-01 21:17:15'); +INSERT INTO "users" VALUES('miles',null,'miles@davis.com','2012-02-01 21:17:25'); +INSERT INTO "users" VALUES('bird',null,'charlie@parker.com','2012-02-01 21:17:39'); DROP TABLE IF EXISTS "empty_table"; CREATE TABLE "empty_table" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , "field" VARCHAR); diff --git a/tests/data/sqlite.db b/tests/data/sqlite.db index 6c7c70e451b5db94d66ed1dcfe7bf6074fa51875..b8b0d252a70ff2ed7592f9d4ccb45196f21b2df9 100644 GIT binary patch delta 169 zcmZozz|^pSX+n~e_e5R>1_lo95(a)ZzAt{YDygGsoRV0UT2vzV_OLhO V&7xm(WSN}S2r;m2zVEN-001BpE8qYC delta 125 zcmZozz|^pSX+n~e%m!Wt1_lo9eg=LwzAtb|~o!5Lb2j4$KEg@PgbXizLC)@jLvuHE3h)>S<*JM#= c0*cJ_SLM-WWRcV~PDw0FEh^c3-(S%I0Hc&2O8@`> diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index 325482c8..f880866b 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -64,6 +64,11 @@ public function testConnectionIsKeptForTheWholeSuite() $this->module->_afterSuite(); } + public function testSeeInDatabaseWithBinary() + { + $this->module->seeInDatabase('users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); + } + public function testSeeInDatabase() { $this->module->seeInDatabase('users', ['name' => 'davert']); @@ -71,6 +76,7 @@ public function testSeeInDatabase() public function testCountInDatabase() { + $this->module->seeNumRecords(1, 'users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); $this->module->seeNumRecords(1, 'users', ['name' => 'davert']); $this->module->seeNumRecords(0, 'users', ['name' => 'davert', 'email' => 'xxx@yyy.zz']); $this->module->seeNumRecords(0, 'users', ['name' => 'user1']); @@ -78,6 +84,7 @@ public function testCountInDatabase() public function testDontSeeInDatabase() { + $this->module->dontSeeInDatabase('users', ['uuid' => hex2bin('ffffffffffffffffffffffffffffffff')]); $this->module->dontSeeInDatabase('users', ['name' => 'user1']); } From 2f752db5bed43e611c036bbb7cefca0cf133f07c Mon Sep 17 00:00:00 2001 From: Szabolcs Hajdu Date: Mon, 4 Mar 2024 20:31:58 +0100 Subject: [PATCH 12/12] #49 Cast last insert id to string to avoid type error when pdo returns with false in case of dblib (#55) --- .github/workflows/main.yml | 30 ++- docker-compose.yml | 51 ++++- php74.Dockerfile | 30 ++- php81.Dockerfile | 30 ++- src/Codeception/Lib/Driver/Db.php | 2 +- tests/data/dumps/mssql.sql | 76 ++++++++ tests/data/scripts/mssql.sh | 19 ++ tests/data/scripts/wait-for-it.sh | 184 ++++++++++++++++++ .../unit/Codeception/Lib/Driver/MysqlTest.php | 12 +- .../Codeception/Lib/Driver/PostgresTest.php | 20 +- .../Codeception/Module/Db/AbstractDbTest.php | 33 +++- .../Module/Db/MssqlDblibDbTest.php | 39 ++++ .../Module/Db/MssqlSqlSrvDbTest.php | 39 ++++ .../Codeception/Module/Db/MySqlDbTest.php | 9 +- .../Module/Db/PostgreSqlDbTest.php | 14 +- 15 files changed, 526 insertions(+), 62 deletions(-) create mode 100644 tests/data/dumps/mssql.sql create mode 100755 tests/data/scripts/mssql.sh create mode 100644 tests/data/scripts/wait-for-it.sh create mode 100644 tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php create mode 100644 tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 917498f2..3cb9ebdc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,12 +31,32 @@ jobs: --health-retries 5 ports: - 5432:5432 + mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + SA_PASSWORD: P@ssw0rd + ACCEPT_EULA: 'Y' + ports: + - 1433:1433 + options: >- + --health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'P@ssw0rd' -d master -Q 'SELECT COUNT(*) FROM master.dbo.spt_values;'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: matrix: php: [7.4, 8.0] + include: + - php: 7.4 + sqlsrv: 5.9.0 + - php: 8.0 + sqlsrv: 5.10.1 steps: + - name: Create default database for sqlsrv as image does not support it + run: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'P@ssw0rd' -Q 'CREATE DATABASE codeception_test' + - name: Checkout code uses: actions/checkout@v2 @@ -44,7 +64,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: pdo, pgsql, mysql, sqlite + extensions: pdo, pgsql, mysql, sqlite, sqlsrv-${{ matrix.sqlsrv }}, pdo_sqlsrv-${{ matrix.sqlsrv }}, pdo_dblib coverage: none - name: Validate composer.json and composer.lock @@ -56,5 +76,11 @@ jobs: - name: Run test suite run: php vendor/bin/codecept run env: - PGPASSWORD: postgres MYSQL_HOST: 127.0.0.1 + MYSQL_DB: codeception_test + PG_HOST: 127.0.0.1 + PG_DB: codeception_test + PG_PASSWORD: postgres + MSSQL_HOST: 127.0.0.1 + MSSQL_DB: codeception_test + MSSQL_PASSWORD: P@ssw0rd diff --git a/docker-compose.yml b/docker-compose.yml index 2de6dfef..a40c234f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,21 @@ version: "3.9" services: - php74: image: codeception-module-db-php74:2.2.0 build: context: . dockerfile: ./php74.Dockerfile environment: - MYSQL_DSN: "mysql:host=host.docker.internal;port=3102;dbname=codeception" - MYSQL_USER: root + MYSQL_HOST: host.docker.internal + MYSQL_DB: codeception MYSQL_PASSWORD: codeception + PG_HOST: host.docker.internal + PG_DB: codeception + PG_PASSWORD: codeception + MSSQL_HOST: host.docker.internal + MSSQL_DB: codeception + MSSQL_PASSWORD: 'P@ssw0rd' XDEBUG_MODE: "debug" XDEBUG_CONFIG: "client_host=host.docker.internal; client_port=9000; mode=debug; start_wih_request=1" PHP_IDE_CONFIG: "serverName=codeception-module-db" # the name must be the same as in your PHP -> Server -> "name" field @@ -23,20 +28,46 @@ services: context: . dockerfile: ./php81.Dockerfile environment: - MYSQL_DSN: "mysql:host=host.docker.internal;port=3102;dbname=codeception" - MYSQL_USER: root + MYSQL_HOST: host.docker.internal + MYSQL_DB: codeception MYSQL_PASSWORD: codeception + PG_HOST: host.docker.internal + PG_DB: codeception + PG_PASSWORD: codeception + MSSQL_HOST: host.docker.internal + MSSQL_DB: codeception + MSSQL_PASSWORD: 'P@ssw0rd' XDEBUG_MODE: "debug" XDEBUG_CONFIG: "client_host=host.docker.internal; client_port=9000; mode=debug; start_wih_request=1" PHP_IDE_CONFIG: "serverName=codeception-module-db" # the name must be the same as in your PHP -> Server -> "name" field volumes: - ".:/var/www/html" - mariadb105: - image: mariadb:10.5 + mysql: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: codeception + MYSQL_DATABASE: codeception + ports: + - "3306:3306" + + postgres: + image: postgres environment: - MARIADB_ROOT_PASSWORD: codeception - MARIADB_DATABASE: codeception + POSTGRES_PASSWORD: codeception + POSTGRES_DB: codeception ports: - - "3102:3306" + - "5432:5432" + + mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + SA_PASSWORD: 'P@ssw0rd' + MSSQL_DATABASE: codeception + ACCEPT_EULA: 'Y' + ports: + - "1433:1433" + volumes: + - ./tests/data/scripts:/scripts:ro + entrypoint: [ "/bin/bash", "-c", "/scripts/mssql.sh" ] diff --git a/php74.Dockerfile b/php74.Dockerfile index 0b13fe86..45a1db86 100644 --- a/php74.Dockerfile +++ b/php74.Dockerfile @@ -1,5 +1,7 @@ FROM php:7.4-cli +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ + RUN apt-get update && \ apt-get install -y \ unzip \ @@ -7,27 +9,21 @@ RUN apt-get update && \ git \ zlib1g-dev \ libzip-dev \ - mariadb-client-10.5 - -RUN docker-php-ext-install pdo pdo_mysql && docker-php-ext-enable pdo pdo_mysql -RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli -RUN docker-php-ext-install zip - -RUN pecl install xdebug-3.1.5 && \ - echo zend_extension=xdebug.so > $PHP_INI_DIR/conf.d/xdebug.ini + libpq-dev \ + mariadb-client-10.5 + +RUN install-php-extensions \ + pdo_mysql-stable \ + pdo_pgsql-stable \ + pdo_dblib-stable \ + pdo_sqlsrv-5.9.0 \ + pgsql-stable \ + zip-stable \ + xdebug-3.1.5 COPY --from=composer /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html -COPY composer.json . -COPY composer.lock . - -RUN composer install --no-autoloader - -COPY . . - -RUN composer dump-autoload -o - ENTRYPOINT ["tail"] CMD ["-f", "/dev/null"] diff --git a/php81.Dockerfile b/php81.Dockerfile index 9e6b8bcd..ca24ccde 100644 --- a/php81.Dockerfile +++ b/php81.Dockerfile @@ -1,5 +1,7 @@ FROM php:8.1-cli +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ + RUN apt-get update && \ apt-get install -y \ unzip \ @@ -7,27 +9,21 @@ RUN apt-get update && \ git \ zlib1g-dev \ libzip-dev \ - mariadb-client-10.5 - -RUN docker-php-ext-install pdo pdo_mysql && docker-php-ext-enable pdo pdo_mysql -RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli -RUN docker-php-ext-install zip - -RUN pecl install xdebug-3.1.5 && \ - echo zend_extension=xdebug.so > $PHP_INI_DIR/conf.d/xdebug.ini + libpq-dev \ + mariadb-client-10.5 + +RUN install-php-extensions \ + pdo_mysql-stable \ + pdo_pgsql-stable \ + pdo_dblib-stable \ + pdo_sqlsrv-5.11.0 \ + pgsql-stable \ + zip-stable \ + xdebug-3.1.5 COPY --from=composer /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html -COPY composer.json . -COPY composer.lock . - -RUN composer install --no-autoloader - -COPY . . - -RUN composer dump-autoload -o - ENTRYPOINT ["tail"] CMD ["-f", "/dev/null"] diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index b5680459..2f7385ec 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -250,7 +250,7 @@ public function deleteQueryByCriteria(string $tableName, array $criteria): void public function lastInsertId(string $tableName): string { - return $this->getDbh()->lastInsertId(); + return (string)$this->getDbh()->lastInsertId(); } public function getQuotedName(string $name): string diff --git a/tests/data/dumps/mssql.sql b/tests/data/dumps/mssql.sql new file mode 100644 index 00000000..93fe7075 --- /dev/null +++ b/tests/data/dumps/mssql.sql @@ -0,0 +1,76 @@ +CREATE TABLE [dbo].[groups] ( + [id] INT NOT NULL IDENTITY(1,1), + [name] VARCHAR(100) NULL, + [enabled] BIT NULL, + [created_at] DATETIME NOT NULL CONSTRAINT DF_groups_created_at DEFAULT GETDATE(), + CONSTRAINT PK_groups PRIMARY KEY CLUSTERED ([id] ASC) +); + +INSERT INTO [dbo].[groups]([name],[enabled],[created_at]) +VALUES + ('coders', 1, '2012-02-01 21:17:50'), + ('jazzman', 0, '2012-02-01 21:18:40'); + + +CREATE TABLE [dbo].[users] ( + [id] INT NOT NULL IDENTITY(1,1), + [uuid] BINARY(16) NULL, + [name] VARCHAR(30) NULL, + [email] VARCHAR(255) NULL, + [is_active] BIT NOT NULL CONSTRAINT DF_users_is_active DEFAULT 1, + [created_at] DATETIME NOT NULL CONSTRAINT DF_users_created_at DEFAULT GETDATE(), + CONSTRAINT PK_users PRIMARY KEY CLUSTERED ([id] ASC) +); + +INSERT INTO [dbo].[users]([uuid],[name],[email],[is_active],[created_at]) +VALUES + (0x11edc34b01d972fa9c1d0242ac120006, 'davert', 'davert@mail.ua', 1, '2012-02-01 21:17:04'), + (null, 'nick', 'nick@mail.ua', 1, '2012-02-01 21:17:15'), + (null, 'miles', 'miles@davis.com', 1, '2012-02-01 21:17:25'), + (null, 'bird', 'charlie@parker.com', 0, '2012-02-01 21:17:39'); + + +CREATE TABLE [dbo].[permissions] ( + [id] INT NOT NULL IDENTITY(1,1), + [user_id] INT NULL, + [group_id] INT NULL, + [role] VARCHAR(30) NULL, + CONSTRAINT PK_permissions PRIMARY KEY CLUSTERED ([id] ASC), + CONSTRAINT FK_permissions FOREIGN KEY ([group_id]) REFERENCES [dbo].[groups] ([id]) ON DELETE CASCADE, + CONSTRAINT FK_users FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([id]) ON DELETE CASCADE +); + +INSERT INTO [dbo].[permissions]([user_id],[group_id],[role]) +VALUES + (1,1,'member'), + (2,1,'member'), + (3,2,'member'), + (4,2,'admin'); + + +CREATE TABLE [dbo].[order] ( + [id] INT NOT NULL IDENTITY(1,1), + [name] VARCHAR(255) NOT NULL, + [status] VARCHAR(255) NOT NULL, + CONSTRAINT PK_order PRIMARY KEY CLUSTERED ([id] ASC) +); + +INSERT INTO [dbo].[order]([name],[status]) VALUES ('main', 'open'); + + +CREATE TABLE [dbo].[composite_pk] ( + [group_id] INT NOT NULL, + [id] INT NOT NULL, + [status] VARCHAR(255) NOT NULL, + CONSTRAINT PK_composite_pk PRIMARY KEY CLUSTERED ([group_id] ASC, [id] ASC) +); + +CREATE TABLE [dbo].[no_pk] ( + [status] varchar(255) NOT NULL +); + +CREATE TABLE [dbo].[empty_table] ( + [id] int NOT NULL IDENTITY(1,1), + [field] varchar(255), + CONSTRAINT [PK_empty_table] PRIMARY KEY CLUSTERED ([id]) +); diff --git a/tests/data/scripts/mssql.sh b/tests/data/scripts/mssql.sh new file mode 100755 index 00000000..84434578 --- /dev/null +++ b/tests/data/scripts/mssql.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +/opt/mssql/bin/sqlservr & +/scripts/wait-for-it.sh 127.0.0.1:1433 + +for i in {1..50}; +do + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -Q "CREATE DATABASE $MSSQL_DATABASE;" + if [ $? -eq 0 ] + then + echo "database created" + break + else + echo "not ready yet..." + sleep 1 + fi +done + +sleep infinity # Keep the container running forever diff --git a/tests/data/scripts/wait-for-it.sh b/tests/data/scripts/wait-for-it.sh new file mode 100644 index 00000000..44768829 --- /dev/null +++ b/tests/data/scripts/wait-for-it.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +#Source: https://github.com/vishnubob/wait-for-it/blob/81b1373f17855a4dc21156cfe1694c31d7d1792e/wait-for-it.sh + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/tests/unit/Codeception/Lib/Driver/MysqlTest.php b/tests/unit/Codeception/Lib/Driver/MysqlTest.php index eda908ae..94ea59f6 100644 --- a/tests/unit/Codeception/Lib/Driver/MysqlTest.php +++ b/tests/unit/Codeception/Lib/Driver/MysqlTest.php @@ -25,9 +25,15 @@ final class MysqlTest extends Unit public static function _setUpBeforeClass() { - $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost'; - self::$config['dsn'] = 'mysql:host='.$host.';dbname=codeception_test'; - self::$config['password'] = getenv('MYSQL_PASSWORD') ? getenv('MYSQL_PASSWORD') : ''; + $host = getenv('MYSQL_HOST') ?: 'localhost'; + $user = getenv('MYSQL_USER') ?: 'root'; + $password = getenv('MYSQL_PASSWORD') ?: ''; + $database = getenv('MYSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MYSQL_DSN') ?: 'mysql:host=' . $host . ';dbname=' . $database; + + self::$config['dsn'] = $dsn; + self::$config['user'] = $user; + self::$config['password'] = $password; $sql = file_get_contents(\Codeception\Configuration::dataDir() . '/dumps/mysql.sql'); $sql = preg_replace('#/\*(?:(?!\*/).)*\*/#s', "", $sql); diff --git a/tests/unit/Codeception/Lib/Driver/PostgresTest.php b/tests/unit/Codeception/Lib/Driver/PostgresTest.php index 7e66826f..b0982021 100644 --- a/tests/unit/Codeception/Lib/Driver/PostgresTest.php +++ b/tests/unit/Codeception/Lib/Driver/PostgresTest.php @@ -27,10 +27,26 @@ public static function _setUpBeforeClass() if (!function_exists('pg_connect')) { return; } - self::$config['password'] = getenv('PGPASSWORD') ? getenv('PGPASSWORD') : null; - $sql = file_get_contents(codecept_data_dir('dumps/postgres.sql')); + + $host = getenv('PG_HOST') ?: 'localhost'; + $user = getenv('PG_USER') ?: 'postgres'; + $password = getenv('PG_PASSWORD') ?: null; + $database = getenv('PG_DB') ?: 'codeception_test'; + $dsn = getenv('PG_DSN') ?: 'pgsql:host=' . $host . ';dbname=' . $database; + + self::$config['dsn'] = $dsn; + self::$config['user'] = $user; + self::$config['password'] = $password; + + $sql = file_get_contents(\Codeception\Configuration::dataDir() . '/dumps/postgres.sql'); $sql = preg_replace('#/\*(?:(?!\*/).)*\*/#s', '', $sql); self::$sql = explode("\n", $sql); + + try { + $postgres = Db::create(self::$config['dsn'], self::$config['user'], self::$config['password']); + $postgres->cleanup(); + } catch (Exception $e) { + } } public function _setUp() diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index f880866b..81ad0942 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -66,6 +66,13 @@ public function testConnectionIsKeptForTheWholeSuite() public function testSeeInDatabaseWithBinary() { + if ( + $this instanceof MssqlSqlSrvDbTest + || $this instanceof MssqlDblibDbTest + ) { + $this->markTestSkipped('Filter to binary field does not supported by SqlSrv driver'); + } + $this->module->seeInDatabase('users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); } @@ -76,18 +83,40 @@ public function testSeeInDatabase() public function testCountInDatabase() { - $this->module->seeNumRecords(1, 'users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); $this->module->seeNumRecords(1, 'users', ['name' => 'davert']); $this->module->seeNumRecords(0, 'users', ['name' => 'davert', 'email' => 'xxx@yyy.zz']); $this->module->seeNumRecords(0, 'users', ['name' => 'user1']); } + public function testCountInDatabaseWithBinary() + { + if ( + $this instanceof MssqlSqlSrvDbTest + || $this instanceof MssqlDblibDbTest + ) { + $this->markTestSkipped('Filter to binary field does not supported by SqlSrv driver'); + } + + $this->module->seeNumRecords(1, 'users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); + } + public function testDontSeeInDatabase() { - $this->module->dontSeeInDatabase('users', ['uuid' => hex2bin('ffffffffffffffffffffffffffffffff')]); $this->module->dontSeeInDatabase('users', ['name' => 'user1']); } + public function testDontSeeInDatabaseWithBinary() + { + if ( + $this instanceof MssqlSqlSrvDbTest + || $this instanceof MssqlDblibDbTest + ) { + $this->markTestSkipped('Filter to binary field does not supported by SqlSrv driver'); + } + + $this->module->dontSeeInDatabase('users', ['uuid' => hex2bin('ffffffffffffffffffffffffffffffff')]); + } + public function testDontSeeInDatabaseWithEmptyTable() { $this->module->dontSeeInDatabase('empty_table'); diff --git a/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php new file mode 100644 index 00000000..e9b46fff --- /dev/null +++ b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php @@ -0,0 +1,39 @@ +getConfig(); + + return sprintf('/opt/mssql-tools/bin/sqlcmd -S $host -U $user -P $password -d $dbname -i %s', $config['dump']); + } + + public function getConfig(): array + { + $host = getenv('MSSQL_HOST') ?: 'localhost'; + $user = getenv('MSSQL_USER') ?: 'sa'; + $password = getenv('MSSQL_PASSWORD') ?: ''; + $database = getenv('MSSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MSSQL_DSN') ?: 'dblib:host=' . $host . ';dbname=' . $database; + + return [ + 'dsn' => $dsn, + 'user' => $user, + 'password' => $password, + 'dump' => 'tests/data/dumps/mssql.sql', + 'reconnect' => true, + 'cleanup' => true, + 'populate' => true, + ]; + } +} diff --git a/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php new file mode 100644 index 00000000..58776042 --- /dev/null +++ b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php @@ -0,0 +1,39 @@ +getConfig(); + + return sprintf('/opt/mssql-tools/bin/sqlcmd -S $Server -U $user -P $password -d $Database -i %s', $config['dump']); + } + + public function getConfig(): array + { + $host = getenv('MSSQL_HOST') ?: 'localhost'; + $user = getenv('MSSQL_USER') ?: 'sa'; + $password = getenv('MSSQL_PASSWORD') ?: ''; + $database = getenv('MSSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MSSQL_DSN') ?: 'sqlsrv:Server=' . $host . ';Database=' . $database; + + return [ + 'dsn' => $dsn, + 'user' => $user, + 'password' => $password, + 'dump' => 'tests/data/dumps/mssql.sql', + 'reconnect' => true, + 'cleanup' => true, + 'populate' => true, + ]; + } +} diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 058aa4f5..11dcfee0 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -22,10 +22,11 @@ public function getPopulator(): string public function getConfig(): array { - $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost'; - $user = getenv('MYSQL_USER') ? getenv('MYSQL_USER') : 'root'; - $password = getenv('MYSQL_PASSWORD') ? getenv('MYSQL_PASSWORD') : ''; - $dsn = getenv('MYSQL_DSN') ? getenv('MYSQL_DSN') : 'mysql:host='.$host.';dbname=codeception_test'; + $host = getenv('MYSQL_HOST') ?: 'localhost'; + $user = getenv('MYSQL_USER') ?: 'root'; + $password = getenv('MYSQL_PASSWORD') ?: ''; + $database = getenv('MYSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MYSQL_DSN') ?: 'mysql:host=' . $host . ';dbname=' . $database; return [ 'dsn' => $dsn, diff --git a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php index fbdae270..9ebf1dd3 100644 --- a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php @@ -13,7 +13,9 @@ final class PostgreSqlDbTest extends AbstractDbTest { public function getPopulator(): string { - return "psql -h localhost -d codeception_test -U postgres < tests/data/dumps/postgres.sql"; + $config = $this->getConfig(); + + return sprintf('psql -h $host -d $dbname -U $user < %s', $config['dump']); } public function getConfig(): array @@ -22,11 +24,15 @@ public function getConfig(): array $this->markTestSkipped(); } - $password = getenv('PGPASSWORD') ? getenv('PGPASSWORD') : null; + $host = getenv('PG_HOST') ?: 'localhost'; + $user = getenv('PG_USER') ?: 'postgres'; + $password = getenv('PG_PASSWORD') ?: null; + $database = getenv('PG_DB') ?: 'codeception_test'; + $dsn = getenv('PG_DSN') ?: 'pgsql:host=' . $host . ';dbname=' . $database; return [ - 'dsn' => 'pgsql:host=localhost;dbname=codeception_test', - 'user' => 'postgres', + 'dsn' => $dsn, + 'user' => $user, 'password' => $password, 'dump' => 'tests/data/dumps/postgres.sql', 'reconnect' => true,