From 9b4b881cbf899236b337c4efd705b76a84813ed3 Mon Sep 17 00:00:00 2001 From: Jonathan Massuchetti Date: Thu, 1 Dec 2022 19:27:46 +0100 Subject: [PATCH 1/5] 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 0563bb4a..be5f4752 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 24a5e95a7f01db9f36787f42ef27fb04d4d61606 Mon Sep 17 00:00:00 2001 From: Jonathan Massuchetti Date: Sat, 3 Dec 2022 00:38:52 +0100 Subject: [PATCH 2/5] 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 be5f4752..53094094 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 72b6313f9a8cf601d4cda2af5564bc6cc74646e9 Mon Sep 17 00:00:00 2001 From: Jesus The Hun Date: Sat, 3 Dec 2022 11:49:19 +0200 Subject: [PATCH 3/5] 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 53094094..0dff9175 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 = []): array + { + $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 = []): array + { + $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 298150cb18d4191f41ee2d5e956a2171a8e40015 Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Sat, 3 Dec 2022 11:51:05 +0200 Subject: [PATCH 4/5] 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 0dff9175..42c58bca 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 1f659bdfdb0654a94a051e904867016f531b2a23 Mon Sep 17 00:00:00 2001 From: Jesus The Hun Date: Sat, 3 Dec 2022 11:52:18 +0200 Subject: [PATCH 5/5] 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 | 26 +++++++++++++++ php81.Dockerfile | 33 +++++++++++++++++++ .../Codeception/Module/Db/MySqlDbTest.php | 6 ++-- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 docker-compose.yml 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..b164309f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: "3.9" + +services: + 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/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,