diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ebcfb6c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,17 @@
+name: CI
+
+on: [push]
+
+jobs:
+ build-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: php-actions/composer@v5
+
+ - name: PHPUnit Tests
+ uses: php-actions/phpunit@v2
+ with:
+ bootstrap: vendor/autoload.php
+ configuration: phpunit.xml.dist
diff --git a/.gitignore b/.gitignore
index d1502b0..bf68767 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
vendor/
composer.lock
+phpunit.xml
+.phpunit.cache/
diff --git a/composer.json b/composer.json
index 935ef91..6882dc7 100644
--- a/composer.json
+++ b/composer.json
@@ -15,12 +15,13 @@
}
},
"suggest": {
- "illuminate/database": "Needed for DatabaseChecker",
+ "ext-pdo": "Needed for DatabaseChecker",
"php-amqplib/php-amqplib": "Needed for RabbitMqChecker",
"influxdb/influxdb-php": "Needed for InfluxDbChecker",
"predis/predis": "Needed for RedisChecker"
},
"require-dev": {
- "squizlabs/php_codesniffer": "^2.7"
+ "squizlabs/php_codesniffer": "^2.7",
+ "phpunit/phpunit": "^9.5"
}
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..21d3ddc
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,26 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
diff --git a/src/Services/DatabaseChecker.php b/src/Services/DatabaseChecker.php
index 535ee05..6bd0e45 100644
--- a/src/Services/DatabaseChecker.php
+++ b/src/Services/DatabaseChecker.php
@@ -3,10 +3,15 @@
namespace Nordsec\StatusChecker\Services;
use Exception;
-use Illuminate\Database\Capsule\Manager as Capsule;
class DatabaseChecker implements StatusCheckerInterface
{
+ protected const DEFAULT_DRIVER = 'mysql';
+ protected const DEFAULT_HOST = 'localhost';
+ protected const DEFAULT_PORT = 3306;
+ protected const DEFAULT_USER = 'root';
+ protected const DEFAULT_PASSWORD = '';
+
private $name;
private $configuration;
private $critical = true;
@@ -41,15 +46,145 @@ public function needsOutput(): bool
public function checkStatus(): string
{
- $capsule = new Capsule();
- $capsule->addConnection($this->configuration);
-
try {
- $capsule->getConnection()->select('select 1');
+ $this->executeSelect('select 1');
} catch (Exception $exception) {
return StatusCheckerInterface::STATUS_FAIL;
}
return StatusCheckerInterface::STATUS_OK;
}
+
+ protected function createConnection(string $dsn, $user = null, $password = null, array $options = []): \PDO
+ {
+ return new \PDO($dsn, $user, $password, $options);
+ }
+
+ private function executeSelect(string $query): void
+ {
+ $config = $this->resolveConfig();
+
+ $dsn = $this->resolveDsn($config);
+ $user = $this->resolveUser($config);
+ $pass = $this->resolvePassword($config);
+
+ $pdo = $this->createConnection($dsn, $user, $pass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
+
+ $pdo->exec($query);
+ }
+
+ private function resolveDsn(array $config): string
+ {
+ if (
+ is_string($this->configuration) &&
+ strpos($this->configuration, ':') !== false &&
+ strpos($this->configuration, ':') !== strpos($this->configuration, '://')
+ ) {
+ return $this->configuration;
+ }
+
+ return $config['dsn'] ?? $this->buildDsn($config);
+ }
+
+ private function resolveConfig(): array
+ {
+ if (
+ is_string($this->configuration) &&
+ strpos($this->configuration, '://') !== false &&
+ strpos($this->configuration, '://') === strpos($this->configuration, ':')
+ ) {
+ return $this->resolveConfigFromUrl($this->configuration);
+ }
+
+ if (is_array($this->configuration)) {
+ if (isset($this->configuration['url'])) {
+ return $this->resolveConfigFromUrl($this->configuration['url']);
+ }
+ if (isset($this->configuration['driver']) || isset($this->configuration['adapter'])) {
+ return $this->configuration;
+ }
+
+ $firstElement = reset($this->configuration);
+ if (isset($firstElement['url'])) {
+ return $this->resolveConfigFromUrl($firstElement['url']);
+ }
+ if (isset($firstElement['driver']) || isset($firstElement['adapter'])) {
+ return $firstElement;
+ }
+ }
+
+ $urlFromEnv = getenv('DATABASE_URL');
+ if (!empty($urlFromEnv)) {
+ return $this->resolveConfigFromUrl($urlFromEnv);
+ }
+
+ return [];
+ }
+
+ private function buildDsn(array $config): string
+ {
+ if (($config['driver'] ?? '') === 'sqlite') {
+ return sprintf('%s:%s', $config['driver'], $config['database'] ?? $config['path']);
+ }
+
+ return sprintf(
+ '%s:host=%s;port=%d;dbname=%s',
+ $this->resolveDriver($config),
+ $config['host'] ?? $config['read']['host'] ?? $config['write']['host'] ?? static::DEFAULT_HOST,
+ $config['port'] ?? static::DEFAULT_PORT,
+ $config['dbname'] ?? $config['database'] ?? null,
+ );
+ }
+
+ private function resolveConfigFromUrl(string $url): array
+ {
+ $details = parse_url($url);
+ $additionalPathChars = 1;
+
+ if ($details === false) {
+ // $url is in format sqlite:///path/to/file
+ preg_match('|^(?\w+)://(?.*)$|', $url, $details);
+ $additionalPathChars = 0;
+ }
+
+ $result = [
+ 'driver' => $details['scheme'],
+ 'user' => $details['user'] ?? '',
+ 'password' => $details['pass'] ?? '',
+ 'database' => substr($details['path'] ?? null, $additionalPathChars),
+ 'host' => $details['host'] ?? '',
+ ];
+
+ if (empty($result['database']) && !empty($result['host']) && $result['driver'] === 'sqlite') {
+ // $url is in format sqlite://file_in_current_dir
+ $result['database'] = $result['host'];
+ $result['host'] = '';
+ }
+
+ return $result;
+ }
+
+ private function resolveUser(array $config): string
+ {
+ return $config['user'] ?? $config['username'] ?? $config['dbuser'] ?? static::DEFAULT_USER;
+ }
+
+ private function resolvePassword(array $config): string
+ {
+ return $config['password'] ?? $config['pass'] ?? $config['dbpass'] ?? static::DEFAULT_PASSWORD;
+ }
+
+ private function resolveDriver(array $config): string
+ {
+ $driver = $config['driver'] ?? $config['adapter'] ?? static::DEFAULT_DRIVER;
+
+ if ($driver === 'mysqli') {
+ $driver = 'mysql';
+ }
+ if (strpos($driver, 'pdo_') !== false) {
+ $driver = str_replace('pdo_', '', $driver);
+ }
+
+ return $driver;
+ }
}
diff --git a/tests/Unit/Services/DatabaseCheckerTest.php b/tests/Unit/Services/DatabaseCheckerTest.php
new file mode 100644
index 0000000..c1ae216
--- /dev/null
+++ b/tests/Unit/Services/DatabaseCheckerTest.php
@@ -0,0 +1,280 @@
+getMockBuilder(DatabaseChecker::class)
+ ->setConstructorArgs($arguments)
+ ->onlyMethods(['createConnection'])
+ ->getMock()
+ ;
+
+ $databaseCheckerMock
+ ->expects(static::once())
+ ->method('createConnection')
+ ->with(...$expectedPdoArgs)
+ ->willReturn($this->createMock(\PDO::class))
+ ;
+
+ $actualResult = $databaseCheckerMock->checkStatus();
+
+ static::assertSame($expectedResult, $actualResult);
+ }
+
+ public function getTestCheckStatusCases(): array
+ {
+ $cases = [];
+
+ $cases['doctrine_mysql_url'] = [
+ 'arguments' => [
+ 'database.mysql_doctrine',
+ [
+ 'url' => 'mysql://user:pass@localhost:3306/testdb',
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=localhost;port=3306;dbname=testdb',
+ 'user',
+ 'pass',
+ [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['eloquent_mysql_url'] = [
+ 'arguments' => [
+ 'database.mysql_eloquent',
+ [
+ 'my_connection' => [
+ 'url' => 'mysql://user2:pass2@127.0.0.1:3306/testdb2',
+ ],
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=127.0.0.1;port=3306;dbname=testdb2',
+ 'user2',
+ 'pass2',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['custom_mysql_url'] = [
+ 'arguments' => [
+ 'database.mysql_custom',
+ 'mysql://user3:pass3@127.0.0.3:3306/testdb3',
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=127.0.0.3;port=3306;dbname=testdb3',
+ 'user3',
+ 'pass3',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['doctrine_mysql_parameters'] = [
+ 'arguments' => [
+ 'database.mysql_doctrine',
+ [
+ 'dbname' => 'database',
+ 'host' => 'localhost',
+ 'port' => '1234',
+ 'user' => 'user',
+ 'password' => 'secret',
+ 'driver' => 'pdo_mysql',
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=localhost;port=1234;dbname=database',
+ 'user',
+ 'secret',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['eloquent_mysql_parameters'] = [
+ 'arguments' => [
+ 'database.mysql_eloquent',
+ [
+ 'mysql' => [
+ 'read' => [
+ 'host' => '192.168.1.1',
+ ],
+ 'write' => [
+ 'host' => '196.168.1.2',
+ ],
+ 'driver' => 'mysql',
+ 'database' => 'database',
+ 'username' => 'root',
+ 'password' => '',
+ 'charset' => 'utf8',
+ 'collation' => 'utf8_unicode_ci',
+ 'prefix' => '',
+ ],
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=192.168.1.1;port=3306;dbname=database',
+ 'root',
+ '',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['propel_mysql_parameters'] = [
+ 'arguments' => [
+ 'database.mysql_propel',
+ [
+ 'bookstore' => [
+ 'adapter' => 'mysql',
+ 'classname' => 'Propel\Runtime\Connection\ConnectionWrapper',
+ 'dsn' => 'mysql:host=localhost;dbname=my_db_name',
+ 'user' => 'my_db_user',
+ 'password' => 's3cr3t',
+ 'attributes' => [],
+ ],
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=localhost;dbname=my_db_name',
+ 'my_db_user',
+ 's3cr3t',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['doctrine_sqlite_url_current_dir'] = [
+ 'arguments' => [
+ 'database.sqlite_doctrine',
+ [
+ 'url' => 'sqlite://testfile.db',
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'sqlite:testfile.db',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['eloquent_sqlite_url_current_dir'] = [
+ 'arguments' => [
+ 'database.sqlite_eloquent',
+ [
+ 'sqlite' => [
+ 'url' => 'sqlite://testfile.db',
+ ],
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'sqlite:testfile.db',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['doctrine_sqlite_url_realpath'] = [
+ 'arguments' => [
+ 'database.sqlite_doctrine',
+ [
+ 'url' => 'sqlite:///var/testfile.db',
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'sqlite:/var/testfile.db',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['eloquent_sqlite_url_realpath'] = [
+ 'arguments' => [
+ 'database.sqlite_eloquent',
+ [
+ 'sqlite' => [
+ 'url' => 'sqlite:///var/testfile.db',
+ ],
+ ],
+ ],
+ 'expectedPdoArgs' => [
+ 'sqlite:/var/testfile.db',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['custom_sqlite_url'] = [
+ 'arguments' => [
+ 'database.sqlite_custom',
+ 'sqlite:///var/testfile.db',
+ ],
+ 'expectedPdoArgs' => [
+ 'sqlite:/var/testfile.db',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['custom_sqlite_dsn'] = [
+ 'arguments' => [
+ 'database.sqlite_custom',
+ 'sqlite:/var/testfile.db',
+ ],
+ 'expectedPdoArgs' => [
+ 'sqlite:/var/testfile.db',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ ];
+
+ $cases['symfony_style_database_url'] = [
+ 'arguments' => [
+ 'database.mysql_symfony',
+ [],
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=localhost;port=3306;dbname=test_sf_db',
+ 'user1',
+ 'pass1',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ 'env' => [
+ 'DATABASE_URL=mysql://user1:pass1@localhost/test_sf_db',
+ ],
+ ];
+
+ $cases['defaults_only'] = [
+ 'arguments' => [
+ 'database.mysql_defaults',
+ [],
+ ],
+ 'expectedPdoArgs' => [
+ 'mysql:host=localhost;port=3306;dbname=',
+ 'root',
+ '',
+ ],
+ 'expectedResult' => DatabaseChecker::STATUS_OK,
+ 'env' => [
+ 'DATABASE_URL=',
+ ],
+ ];
+
+ return $cases;
+ }
+}