From 64664efb4f5f7e7871c00fd7525432b9c70308bb Mon Sep 17 00:00:00 2001 From: Muhammad Faiz Date: Wed, 25 Nov 2020 18:04:49 +0800 Subject: [PATCH] Initial Code. --- .github/workflows/main.yml | 74 ++++++ .gitignore | 6 + composer.json | 48 ++++ config/tail-db.php | 84 +++++++ phpunit.xml.dist | 25 ++ src/DatabaseWatcher.php | 145 +++++++++++ src/TailDatabase.php | 36 +++ src/TailDatabaseCommand.php | 284 ++++++++++++++++++++++ src/TailDatabaseServiceProvider.php | 48 ++++ tests/DatabaseWatcherTest.php | 115 +++++++++ tests/TailDatabaseCommandTest.php | 83 +++++++ tests/TailDatabaseServiceProviderTest.php | 28 +++ tests/TestCase.php | 133 ++++++++++ tests/TestClasses/UserModel.php | 22 ++ 14 files changed, 1131 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 config/tail-db.php create mode 100644 phpunit.xml.dist create mode 100644 src/DatabaseWatcher.php create mode 100644 src/TailDatabase.php create mode 100644 src/TailDatabaseCommand.php create mode 100644 src/TailDatabaseServiceProvider.php create mode 100644 tests/DatabaseWatcherTest.php create mode 100644 tests/TailDatabaseCommandTest.php create mode 100644 tests/TailDatabaseServiceProviderTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/TestClasses/UserModel.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f475156 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,74 @@ +# This is a basic workflow to help you get started with Actions + +name: testing + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_USER: root + MYSQL_DATABASE: test_db + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest] + php: [ 7.4 ] + laravel: [ 6.*, 7.*, 8.* ] + include: + - laravel: 6.* + testbench: 4.* + - laravel: 7.* + testbench: 5.* + - laravel: 8.* + testbench: 6.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --prefer-dist --no-interaction --no-suggest + + - name: Execute tests + env: + DB_CONNECTION: mysql + DB_DATABASE: test_db + DB_PORT: ${{ job.services.mysql.ports[3306] }} + DB_USER: root + DB_HOST: 127.0.0.1 + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc3eee1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor +composer.phar +composer.lock +.phpunit.result.cache +.php_cs.cache +.idea \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..daf2f84 --- /dev/null +++ b/composer.json @@ -0,0 +1,48 @@ +{ + "name": "muhdfaiz/laravel-tail-db", + "description": "Provide artisan command to tail database query. Able to automatically run explain command for each of query received.", + "keywords": [ + "laravel", + "tail", + "log", + "explain", + "query", + "database", + "tail-db" + ], + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Muhammad Faiz", + "email": "muhdfaizhalim@gmail.com" + } + ], + "require": { + "php": "^7.0", + "symfony/process": "^4.0|^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0|^9.0", + "laravel/framework": "^6.0|^7.0|^8.0", + "orchestra/testbench": "^4.0|^5.0|^6.0" + }, + "autoload": { + "psr-4": { + "Muhdfaiz\\LaravelTailDb\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Muhdfaiz\\LaravelTailDb\\Tests\\": "tests" + } + }, + "extra": { + "laravel": { + "providers": [ + "Muhdfaiz\\LaravelTailDb\\TailDatabaseServiceProvider" + ] + } + }, + "prefer-stable": true +} diff --git a/config/tail-db.php b/config/tail-db.php new file mode 100644 index 0000000..59527f7 --- /dev/null +++ b/config/tail-db.php @@ -0,0 +1,84 @@ + env('TAIL_DB_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Duration of time considered slow query. + |-------------------------------------------------------------------------- + | + | This option used to tell Laravel Tail DB if the query slow or not. + | For example, if you specify 2000ms and the last query executed take + | more than 2000ms, Laravel Tail DB will highlight the time with red color. + | If the query below than 2000ms Laravel Tail DB will highlight with green color. + | The value must be in milliseconds. + */ + 'slow_duration' => env('TAIL_DB_SLOW_DURATION', 3000), + + /* + |-------------------------------------------------------------------------- + | Ignore queries + |-------------------------------------------------------------------------- + | + | You can specify the keyword to skip the tailing. + | Laravel Tail DB will check if the query contain those keyword or not. + | If exist, Laravel Tail DB will skip recording to log file. + | The key keyword must be separated by comma. + | Example: alter table}drop table. (Separated by |) + | + */ + 'ignore_query_keyword' => env('TAIL_DB_IGNORE_QUERY_KEYWORD', ''), + + /* + |-------------------------------------------------------------------------- + | Filename to store mysql queries log + |-------------------------------------------------------------------------- + | + | Default filename is database.log + | + */ + 'filename' => env ('TAIL_DB_FILENAME', 'database.log'), + + /* + |-------------------------------------------------------------------------- + | Path to store sql queries log. + |-------------------------------------------------------------------------- + | + | Default path is inside storage/logs. + | + */ + 'path' => env ('TAIL_DB_PATH', storage_path('logs')), + + /* + |-------------------------------------------------------------------------- + | Show explain sql during the tail. + |-------------------------------------------------------------------------- + | + | By default every sql query executed, laravel tail db will run explain + | command. Useful if you want to troubleshooting performance issue. + | If turn off, Laravel Tail DB only show the query executed, the time and + | the location where the query executed. + */ + 'show_explain' => env ('TAIL_DB_SHOW_EXPLAIN', true), + + /* + |-------------------------------------------------------------------------- + | Clear log + |-------------------------------------------------------------------------- + | + | When you end the tail:database command or every time Laravel Tail DB + | received new data, the data in the log will be cleared. + */ + 'clear_log' => env ('TAIL_DB_CLEAR_LOG', true), +]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..e22d946 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + tests + + + + + src/ + + + + + + \ No newline at end of file diff --git a/src/DatabaseWatcher.php b/src/DatabaseWatcher.php new file mode 100644 index 0000000..353660b --- /dev/null +++ b/src/DatabaseWatcher.php @@ -0,0 +1,145 @@ +listen(QueryExecuted::class, [$this, 'recordQuery']); + } + + /** + * Record a query was executed. + * + * @param QueryExecuted $event + * @return void + */ + public function recordQuery(QueryExecuted $event) + { + // Check if the query contain keywords user want to ignore based on the config. + if (config('tail-db.ignore_query_keyword') && preg_match('(' . config('tail-db.ignore_query_keyword') . ')', strtolower($event->sql)) === 1) { + return; + } + + // Need to skip recording if the query received related to migration. + // For example, create table, drop table and alter table. + if ($this->checkIfQueryRelatedToMigration(strtolower($event->sql))) { + return; + }; + + // Get stack trace. + $caller = $this->getCallerFromStackTrace(); + + // Set data before storing in the database log file. + $data = [ + 'connection' => $event->connectionName, + 'bindings' => $event->bindings, + 'sql' => strtolower($this->replaceBindings($event)), + 'time' => number_format($event->time, 2, '.', ''), + 'file' => $caller['file'], + 'line' => $caller['line'], + ]; + + // Store the data in the log file. + Log::channel('taildb')->info(json_encode($data)); + } + + /** + * Find the first frame in the stack trace outside of Laravel vendor. + * + * @return array + */ + protected function getCallerFromStackTrace() + { + $trace = collect(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))->forget(0); + return $trace->first(function ($frame) { + if (! isset($frame['file'])) { + return false; + } + + if (Str::contains($frame['file'],'vendor'.DIRECTORY_SEPARATOR.'laravel')) { + return false; + } + + return $frame['file']; + }); + } + + /** + * Format the given bindings to strings. + * + * @param QueryExecuted $event + * + * @return array + */ + protected function formatBindings($event) + { + return $event->connection->prepareBindings($event->bindings); + } + + /** + * Replace the placeholders with the actual bindings. + * + * @param QueryExecuted $event + * + * @return string + */ + public function replaceBindings($event) + { + $sql = $event->sql; + + foreach ($this->formatBindings($event) as $key => $binding) { + $regex = is_numeric($key) + ? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/" + : "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"; + + if ($binding === null) { + $binding = 'null'; + } elseif (! is_int($binding) && ! is_float($binding)) { + $binding = $event->connection->getPdo()->quote($binding); + } + + $sql = preg_replace($regex, $binding, $sql, 1); + } + + return $sql; + } + + /** + * Check if query related to migration. + * + * @param $query + * + * @return bool + */ + private function checkIfQueryRelatedToMigration($query) + { + $query = strtolower($query); + + // Get ignore query from the config in case user want to ignore other query. + + + // Default query that will skip by Laravel Tail DB. + if (strpos($query, 'explain') !== false || strpos($query, 'alter table') !== false + || strpos($query, 'create table') !== false || strpos($query, 'drop table') !== false + || strpos($query, 'create index') !== false || strpos($query, 'create unique index') !== false + || strpos($query, 'information_schema') !== false + ) { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/TailDatabase.php b/src/TailDatabase.php new file mode 100644 index 0000000..5ece82b --- /dev/null +++ b/src/TailDatabase.php @@ -0,0 +1,36 @@ +make(DatabaseWatcher::class); + + $databaseWatcher->register($app); + } +} \ No newline at end of file diff --git a/src/TailDatabaseCommand.php b/src/TailDatabaseCommand.php new file mode 100644 index 0000000..b417311 --- /dev/null +++ b/src/TailDatabaseCommand.php @@ -0,0 +1,284 @@ +checkStatusEnabled()) { + $this->output->writeln(PHP_EOL); + $this->output->writeln(' Laravel Tail DB has been disabled. Please enabled first before' . + ' running the command. '); + $this->output->writeln(PHP_EOL); + return; + } + + $this->output->writeln('Listening for new query from application.....' . PHP_EOL); + + $tailCommand = $this->generateTailCommand(); + + Process::fromShellCommandline($tailCommand, config('tail-db.path')) + ->setTty(false) + ->setTimeout(null) + ->run(function ($type, $logData) { + $logData = $this->parseLogData($logData); + + $this->outputQueryDetails($logData); + + $databaseDriver = config('database.connections.' . $logData['connection'] . '.driver'); + + // For SQL Server, don't have explain command like SQLite, MySQL and PostgreSQL. + // Need to check if the database using SQL Server or not. + // If using SQL Server, disable the explain command. + if (config('tail-db.show_explain') && $databaseDriver !== 'sqlsrv' + && preg_match('(select|delete|insert|replace|update)', $logData['sql']) === 1) { + $this->outputExplainResultInTableFormat($logData); + } + + if (config('tail-db.clear_log')) { + $this->clearLogFile(); + } + }); + } + + /** + * Check if Laravel Tail DB enabled or not. + * + * @return bool + */ + protected function checkStatusEnabled() + { + return config('tail-db.enabled'); + } + + /** + * Generate tail command. + * + * @return string + */ + public function generateTailCommand() + { + // If running in windows, tail command not available. + // Need to use powershell + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + // Needed this because if using -f option in test, the command will be running forever. + // If the test contain code running tail:db command, the test will not stop. + // Didn't find a way to stop/terminate terminate the command.. + if (config('app.env') === 'testing') { + return 'powershell Get-Content ' . config('tail-db.filename') . ' -Tail 1'; + } else { + return 'powershell Get-Content ' . config('tail-db.filename') . ' -Wait -Tail 0'; + } + } else { + // Needed this because if using -f option in test, the command will be running forever. + // If the test contain code running tail:db command, the test will not stop. + // Didn't find a way to stop/terminate terminate the command.. + if (config('app.env') === 'testing') { + return 'tail -n 1 ' . config('tail-db.filename'); + } else { + return 'tail -f -n 0 ' . config('tail-db.filename'); + } + } + } + + /** + * Parse the log data in JSON into array. + * + * @param string $logData + * + * @return array + */ + protected function parseLogData(string $logData) + { + $needle = config('app.env') . '.INFO: '; + + $databaseQueryJSON = str_replace($needle, '', strstr($logData, $needle)); + $databaseQueryArray = json_decode($databaseQueryJSON, true); + + return $databaseQueryArray; + } + + /** + * Execute explain query based on query receive from tail process. + * + * @param array $logData + * + * @return array + */ + protected function executeExplainQuery(array $logData) + { + $explainQuery = 'explain ' . str_replace('Query: ', '', $logData['sql']); + $explainResult = DB::connection($logData['connection'])->select($explainQuery); + + return $explainResult; + } + + /** + * Output the last sql query executed and time taken to complete the query. + * Also output the file that trigger the last sql query executed. + * + * @param array $logData + */ + protected function outputQueryDetails(array $logData) + { + $timeColor = 'blue'; + + if ($logData['time'] > config('tail-db.slow_duration')) { + $timeColor = 'red'; + } + + $file = 'File: ' . $logData['file'] . ' Line: ' . $logData['line'] . ''; + $query = 'Query: ' . $logData['sql'] . ' Time: ' . $logData['time'] . ' ms'; + + $this->output->writeln($file); + $this->output->writeln($query); + + if (!config('tail-db.show_explain')) { + $this->output->write(PHP_EOL); + } + } + + /** + * Output the data from explain result in the console using table format.. + * + * @param array $logData + */ + protected function outputExplainResultInTableFormat(array $logData) + { + $databaseDriver = config('database.connections.' . $logData['connection'] . '.driver'); + + $explainResult = $this->executeExplainQuery($logData); + + $tableHeaders = $this->getTableHeaderFromExplainResult($explainResult, $databaseDriver); + $tableRows = $this->generateTableRows($explainResult, $databaseDriver); + + $table = new Table($this->output); + + // For postgreSql, the explain result only have 1 columns and 1 rows. + // Need to set max width to support screen with smaller width. + if ($databaseDriver === 'pgsql') { + $table->setColumnMaxWidth(0, 120); + } else if ($databaseDriver === 'mysql') { + $table->setColumnMaxWidth(5, 60); + $table->setColumnMaxWidth(6, 60); + } + + $table->setStyle('box')->setHeaders($tableHeaders) + ->setRows($tableRows)->render(); + + $this->output->write(PHP_EOL); + } + + /** + * Generate table header for the explain result. + * + * @param array $explainResult + * @param string $databaseDriver + * + * @return array + */ + protected function getTableHeaderFromExplainResult(array $explainResult, string $databaseDriver) + { + $headers = array_keys((array) $explainResult[0]); + + if ($databaseDriver === 'mysql') { + unset($headers[3]); + } + + return $headers; + } + + /** + * Generate table header for the explain result. + * + * @param array $explainResult + * @param string $databaseDriver + * + * @return array + */ + protected function generateTableRows(array $explainResult, string $databaseDriver) + { + $rows = []; + + foreach ($explainResult as $key => $result) { + $rows[$key] = array_values((array) $result); + + // Trying to support smaller screen by trying to avoid the text length too long + // by split the string into a few line. + // Only for MySQL. + if ($databaseDriver === 'mysql') { + // Split string into multiple lines. + if (!empty($rows[$key][5])) { + $rows[$key][5] = str_replace(',', PHP_EOL, $rows[$key][5]); + } + + // Split string into multiple lines. + if (!empty($rows[$key][6])) { + $rows[$key][6] = str_replace(',', PHP_EOL, $rows[$key][6]); + } + + // Round filtered value to 2 decimal place. + if (!empty($rows[$key][10])) { + $rows[$key][10] = round($rows[$key][10], 2); + } + + // Split string into multiple lines. + if (!empty($rows[$key][11])) { + $rows[$key][11] = str_replace('; ', PHP_EOL, $rows[$key][11]); + } + + unset($rows[$key][3]); + } + } + + return $rows; + } + + /** + * Clear log file uses to record database query. + */ + protected function clearLogFile() + { + // Clear log file. + $filename = config('tail-db.filename'); + $path = config('tail-db.path'); + + fclose(fopen($path . '/' . $filename,'w')); + } +} diff --git a/src/TailDatabaseServiceProvider.php b/src/TailDatabaseServiceProvider.php new file mode 100644 index 0000000..f1ad429 --- /dev/null +++ b/src/TailDatabaseServiceProvider.php @@ -0,0 +1,48 @@ +app->runningInConsole()) { + $this->commands([ + TailDatabaseCommand::class, + ]); + } + + $this->publishes([ + __DIR__.'/../config/tail-db.php' => config_path('tail-db.php'), + ], 'tail-db-config'); + + if (config('tail-db.enabled')) { + $newLoggingChannel = [ + 'driver' => 'single', + 'path' => config('tail-db.path') . '/' . config('tail-db.filename'), + 'level' => env('LOG_LEVEL', 'debug'), + ]; + + $this->app['config']["logging.channels.taildb"] = $newLoggingChannel; + + TailDatabase::start($this->app); + } + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->mergeConfigFrom(__DIR__.'/../config/tail-db.php', 'tail-db'); + } +} \ No newline at end of file diff --git a/tests/DatabaseWatcherTest.php b/tests/DatabaseWatcherTest.php new file mode 100644 index 0000000..952fbf0 --- /dev/null +++ b/tests/DatabaseWatcherTest.php @@ -0,0 +1,115 @@ +app['events']->getListeners(QueryExecuted::class); + + $this->assertNotEmpty($listeners); + } + + /** + * @test + * @environment-setup useDisableLibrary + */ + public function it_will_not_registered_query_executed_event_when_disabled() + { + $listeners = $this->app['events']->getListeners(QueryExecuted::class); + + $this->assertEmpty($listeners); + } + + /** + * @test + */ + public function it_record_the_query_to_the_log_file() + { + $user = UserModel::create(['name' => 'Test Person', 'email' => 'testuser@gmail.com']); + + $query = UserModel::where('email', $user->email); + $user = $query->first(); + + $filename = config('tail-db.filename'); + $path = config('tail-db.path'); + + // Read last line of the log file. + $process = new Process(['tail', '-n', '1', $path . '/' . $filename]); + $process->run(); + $logContent = $process->getOutput(); + + // Parse the data. + $needle = 'testing.INFO: '; + $databaseQueryJSON = str_replace($needle, '', strstr($logContent, $needle)); + $databaseQueryArray = json_decode($databaseQueryJSON, true); + + $this->assertEquals($user->email, $databaseQueryArray['bindings'][0]); + $this->assertEquals($this->getQueriesWithBindings($query), $databaseQueryArray['sql']); + + $reflector = new \ReflectionClass($this); + $filename = $reflector->getFileName(); + + $this->assertEquals($filename, $databaseQueryArray['file']); + } + + /** + * @test + */ + public function it_will_skip_recording_query_related_with_migration() + { + $this->createDummyData(); + + DB::select('explain select * from users'); + + Schema::dropIfExists('users'); + + Schema::dropIfExists('test_table'); + Schema::create('test_table', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->timestamps(); + }); + + $filename = config('tail-db.filename'); + $path = config('tail-db.path'); + + $logData = file_get_contents($path . '/' . $filename); + + $this->assertFalse(str_contains($logData, 'drop table')); + $this->assertFalse(str_contains($logData, 'create table')); + $this->assertFalse(str_contains($logData, 'alter table')); + $this->assertFalse(str_contains($logData, "create table `test_table`")); + } + + /** @test */ + public function it_will_not_record_query_contains_ignore_keyword() + { + config(['tail-db.enabled' => true]); + config(['tail-db.ignore_query_keyword' => 'select|insert']); + config(['tail-db.clear_log' => false]); + + $this->createDummyData(); + + Artisan::call('tail:db'); + + $filename = config('tail-db.filename'); + $path = config('tail-db.path'); + + $logData = file_get_contents($path . '/' . $filename); + + $this->assertEmpty($logData); + } +} \ No newline at end of file diff --git a/tests/TailDatabaseCommandTest.php b/tests/TailDatabaseCommandTest.php new file mode 100644 index 0000000..6a167ec --- /dev/null +++ b/tests/TailDatabaseCommandTest.php @@ -0,0 +1,83 @@ + false]); + + $expectOutput = ' Laravel Tail DB has been disabled. Please enabled first before running the command. '; + + $this->artisan('tail:db')->expectsOutput($expectOutput); + } + + /** @test */ + public function it_will_run_if_enabled() + { + config(['tail-db.enabled' => true]); + config(['tail-db.show_explain' => true]); + + $this->createDummyData(); + + Artisan::call('tail:db'); + $output = Artisan::output(); + + $this->assertTrue(str_contains($output, 'select_type')); + $this->assertTrue(str_contains($output, 'Extra')); + } + + /** @test */ + public function it_will_not_execute_explain_query_if_show_explain_config_false() + { + config(['tail-db.enabled' => true]); + config(['tail-db.show_explain' => false]); + + $this->createDummyData(); + + Artisan::call('tail:db'); + $output = Artisan::output(); + + $this->assertFalse(str_contains($output, 'select_type')); + $this->assertFalse(str_contains($output, 'Extra')); + } + + /** @test */ + public function it_will_clear_log_if_show_clear_log_config_true() + { + config(['tail-db.enabled' => true]); + config(['tail-db.clear_log' => true]); + + $user = $this->createDummyData(); + + Artisan::call('tail:db'); + + $filename = config('tail-db.filename'); + $path = config('tail-db.path'); + + $logData = file_get_contents($path . '/' . $filename); + $this->assertEmpty($logData); + } + + /** @test */ + public function it_will_not_clear_log_if_show_clear_log_config_false() + { + config(['tail-db.enabled' => true]); + config(['tail-db.clear_log' => false]); + + $this->createDummyData(); + + Artisan::call('tail:db'); + + $filename = config('tail-db.filename'); + $path = config('tail-db.path'); + + $logData = file_get_contents($path . '/' . $filename); + + $this->assertNotEmpty($logData); + } +} diff --git a/tests/TailDatabaseServiceProviderTest.php b/tests/TailDatabaseServiceProviderTest.php new file mode 100644 index 0000000..ee12b07 --- /dev/null +++ b/tests/TailDatabaseServiceProviderTest.php @@ -0,0 +1,28 @@ +assertNotEmpty($tailDBLoggingChannel); + } + + /** + * @test + * @environment-setup useDisableLibrary + */ + public function it_will_not_add_taildb_logging_channel_when_disabled() + { + $tailDBLoggingChannel = config('logging.channels.taildb'); + + $this->assertEmpty($tailDBLoggingChannel); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..3ae53b9 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,133 @@ +setUpDatabase(); + } + + /** + * Setup test environment. + * + * @param Application $app + */ + protected function getEnvironmentSetUp($app) + { + $app['config']->set('app.debug', 'true'); + + $app['config']->set('database.default', 'mysql'); + + $app['config']->set('database.connections.mysql', [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'username' => env('DB_USER', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'database' => env('DB_DATABASE', 'test_db'), + 'port' => env('DB_PORT', '3306'), + 'prefix' => '', + ]); + + // Clear log file. + $filename = config('tail-db.filename'); + $path = config('tail-db.path'); + fclose(fopen($path . '/' . $filename,'w')); + } + + /** + * Load service provider. + * + * @param Application $app + * + * @return array|string[] + */ + protected function getPackageProviders($app) + { + return [ + TailDatabaseServiceProvider::class + ]; + } + + /** + * Setup dummy database. + */ + protected function setUpDatabase() + { + $query = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?"; + $db = DB::select($query, ['test_db']); + + if (empty($db)) { + DB::select('CREATE DATABASE '. 'test_db'); + } + + Schema::dropIfExists('users'); + Schema::create('users', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('name'); + $table->string('email'); + $table->timestamps(); + }); + } + + /** + * Get sql queries with bindings. + * + * @param Builder $builder + * + * @return string + */ + public static function getQueriesWithBindings(Builder $builder) + { + $addSlashes = str_replace('?', "'?'", $builder->toSql()); + return vsprintf(str_replace('?', '%s', $addSlashes), $builder->getBindings()); + } + + /** + * Disable the library. + * + * @param $app + */ + protected function useDisableLibrary($app) + { + $app->config->set('tail-db.enabled', false); + } + + /** + * Enable the library. + * + * @param $app + */ + protected function useEnableLibrary($app) + { + $app->config->set('tail-db.enabled', true); + } + + protected function createDummyData() + { + $input = [ + 'name' => 'Test User', + 'email' => 'testuser@gmail.com', + ]; + + $user = UserModel::create($input); + + $query = UserModel::where('email', $user->email); + return $query->first(); + } +} diff --git a/tests/TestClasses/UserModel.php b/tests/TestClasses/UserModel.php new file mode 100644 index 0000000..d7b4d79 --- /dev/null +++ b/tests/TestClasses/UserModel.php @@ -0,0 +1,22 @@ +