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/.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/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/"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..a40c234f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +version: "3.9" + +services: + php74: + image: codeception-module-db-php74:2.2.0 + build: + context: . + dockerfile: ./php74.Dockerfile + environment: + 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" + + php81: + image: codeception-module-db-php81:2.2.0 + build: + context: . + dockerfile: ./php81.Dockerfile + environment: + 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" + + mysql: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: codeception + MYSQL_DATABASE: codeception + ports: + - "3306:3306" + + postgres: + image: postgres + environment: + POSTGRES_PASSWORD: codeception + POSTGRES_DB: codeception + ports: + - "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 new file mode 100644 index 00000000..45a1db86 --- /dev/null +++ b/php74.Dockerfile @@ -0,0 +1,29 @@ +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 \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + 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 + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/php81.Dockerfile b/php81.Dockerfile new file mode 100644 index 00000000..ca24ccde --- /dev/null +++ b/php81.Dockerfile @@ -0,0 +1,29 @@ +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 \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + 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 + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index b69017ef..2f7385ec 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(); } @@ -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 @@ -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/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']; } } diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 8d41bba9..2464bda0 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, ]; /** @@ -527,8 +529,7 @@ private function readSql($databaseKey = null, $databaseConfig = null): void } /** - * @return bool|null|string|string[] - * @throws ModuleConfigException + * @throws ModuleConfigException|ModuleException */ private function readSqlFile(string $filePath): ?string { @@ -544,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 @@ -635,6 +645,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(); @@ -718,7 +737,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; } @@ -748,7 +767,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 * _getDriver()->getPrimaryKey($table); $primary = []; if ($primaryKey !== []) { - if ($id && count($primaryKey) === 1) { - $primary [$primaryKey[0]] = $id; + $filledKeys = array_intersect($primaryKey, array_keys($row)); + $missingPrimaryKeyColumns = array_diff_key($primaryKey, $filledKeys); + + if (count($missingPrimaryKeyColumns) === 0) { + $primary = array_intersect_key($row, array_flip($primaryKey)); + } elseif (count($missingPrimaryKeyColumns) === 1) { + $primary = array_intersect_key($row, array_flip($primaryKey)); + $missingColumn = reset($missingPrimaryKeyColumns); + $primary[$missingColumn] = $id; } else { foreach ($primaryKey as $column) { if (isset($row[$column])) { @@ -818,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 ); } @@ -844,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 ) ); @@ -856,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 ); } @@ -937,6 +964,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/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/dumps/mysql.sql b/tests/data/dumps/mysql.sql index 4102f7ef..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'); @@ -94,8 +95,25 @@ 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 `auto_increment_on_composite_pk` ( + `id` int(11) NOT NULL, + `counter` int(11) AUTO_INCREMENT NOT NULL, +PRIMARY KEY (`id`, `counter`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + 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/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/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/data/sqlite.db b/tests/data/sqlite.db index 6c7c70e4..b8b0d252 100644 Binary files a/tests/data/sqlite.db and b/tests/data/sqlite.db differ 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 9f4ccfe9..81ad0942 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -64,6 +64,18 @@ public function testConnectionIsKeptForTheWholeSuite() $this->module->_afterSuite(); } + 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')]); + } + public function testSeeInDatabase() { $this->module->seeInDatabase('users', ['name' => 'davert']); @@ -76,11 +88,35 @@ public function testCountInDatabase() $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', ['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'); @@ -175,7 +211,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/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 7fb56397..11dcfee0 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -22,12 +22,15 @@ public function getPopulator(): string public function getConfig(): array { - $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost'; - $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; return [ - 'dsn' => 'mysql:host='.$host.';dbname=codeception_test', - 'user' => 'root', + 'dsn' => $dsn, + 'user' => $user, 'password' => $password, 'dump' => 'tests/data/dumps/mysql.sql', 'reconnect' => true, @@ -51,7 +54,7 @@ public function testConnectionIsResetOnEveryTestWhenReconnectIsTrue() // Simulate a test that runs $this->module->_before($testCase1); - + $connection1 = $this->module->dbh->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); $this->module->_after($testCase1); @@ -83,7 +86,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); @@ -91,6 +94,7 @@ public function testInitialQueriesAreExecuted() public function testGrabColumnFromDatabase() { + $this->module->_beforeSuite(); $emails = $this->module->grabColumnFromDatabase('users', 'email'); $this->assertSame( [ @@ -101,4 +105,77 @@ 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 = [ + '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); + } + + 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); + } } 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,