diff --git a/.crontab b/.crontab index 80d263ed..4a2155fc 100644 --- a/.crontab +++ b/.crontab @@ -38,7 +38,3 @@ INDEX_PATH='/home/bmxfeed/aggro/current/public/index.php' ## aggro youtube ## Look for new YouTube videos every 5 minutes */5 * * * * $PHP_PATH $INDEX_PATH aggro youtube - -## aggro watch -## Show next video in the watch queue every week (Monday at 00:00) -0 0 * * 1 $PHP_PATH $INDEX_PATH aggro watch diff --git a/.docksal/commands/admin b/.docksal/commands/admin deleted file mode 100755 index f924178b..00000000 --- a/.docksal/commands/admin +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -## Run application tasks -## Usage: fin admin - -cd "$PROJECT_ROOT" || exit - -echo -e "\\n\\033[1;33mClearing aggro logs...\\033[0m" -fin fire aggro log-clean - -echo -e "\\n\\033[1;33mClearing error logs...\\033[0m" -fin fire aggro log-error-clean - -echo -e "\\n\\033[1;33mClearing news cache...\\033[0m" -fin fire aggro news-cache - -echo -e "\\n\\033[1;33mClearing news stories...\\033[0m" -fin fire aggro news-clean - -echo -e "\\n\\033[1;33mUpdating news...\\033[0m" -fin fire aggro news - -echo -e "\\n\\033[1;33mUpdating Vimeo...\\033[0m" -fin fire aggro vimeo - -echo -e "\\n\\033[1;33mUpdating YouTube...\\033[0m" -fin fire aggro youtube - -echo -e "\\n\\033[1;33mCleaning up...\\033[0m" -fin fire aggro sweep - -echo -e "\\n\\033[1;33mShow aggro log...\\033[0m" -fin fire aggro log - -echo -e "\\n\\033[1;33mShow error log...\\033[0m" -fin fire aggro log-error diff --git a/.docksal/commands/clicheck b/.docksal/commands/clicheck new file mode 100755 index 00000000..50a910a4 --- /dev/null +++ b/.docksal/commands/clicheck @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +## Validate CLI routes are accessible (and run them in the process) +## Usage: fin clicheck + +#: exec_target = cli + +# Abort if anything fails +set -e + +echo -e "\n\033[1;33mValidating CLI routes...\033[0m" + +# CLI routes from crontab that should be accessible +ROUTES=( + "aggro log" + "aggro log-error" + "aggro news" + "aggro news-clean" + "aggro news-cache" + "aggro sweep" + "aggro duration" + "aggro vimeo" + "aggro youtube" + "aggro log-clean" + "aggro log-error-clean" +) + +FAILED=0 + +for route in "${ROUTES[@]}"; do + echo -n " Testing $route... " + + # Run the CLI route and capture output + # shellcheck disable=SC2086 # Intentional word splitting for route args + OUTPUT=$(/usr/local/bin/php /var/www/public/index.php $route 2>&1) || true + + # Check for 404 response (CI4 error page contains

404

) + if echo "$OUTPUT" | grep -q "

404

"; then + echo -e "\033[1;31mFAILED (404)\033[0m" + echo -e "\033[0;33mOutput:\033[0m" + echo "$OUTPUT" + FAILED=1 + elif echo "$OUTPUT" | grep -q "Unable to locate"; then + echo -e "\033[1;31mFAILED (Route not found)\033[0m" + echo -e "\033[0;33mOutput:\033[0m" + echo "$OUTPUT" + FAILED=1 + else + echo -e "\033[1;32mOK\033[0m" + fi +done + +if [ $FAILED -eq 1 ]; then + echo -e "\n\033[1;31mCLI route validation failed!\033[0m" + echo "Ensure all routes have corresponding \$routes->cli() definitions in app/Config/Routes.php" + exit 1 +fi + +echo -e "\n\033[1;32mAll CLI routes accessible.\033[0m" diff --git a/.docksal/commands/maintain b/.docksal/commands/maintain index 4cdcd865..25e282f5 100755 --- a/.docksal/commands/maintain +++ b/.docksal/commands/maintain @@ -6,8 +6,4 @@ cd "$PROJECT_ROOT" || exit fin upgrade -fin init -fin check -fin sniff -fin shellcheck -fin admin +fin test diff --git a/.docksal/commands/sniff b/.docksal/commands/sniff deleted file mode 100755 index c2eca876..00000000 --- a/.docksal/commands/sniff +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -## Run static PHP analysis -## Usage: fin sniff - - -cd "$PROJECT_ROOT" || exit -fin composer lint diff --git a/.docksal/commands/test b/.docksal/commands/test index aaa546db..e9b096da 100755 --- a/.docksal/commands/test +++ b/.docksal/commands/test @@ -6,7 +6,7 @@ cd "$PROJECT_ROOT" || exit fin init -fin sniff -fin shellcheck +fin composer lint fin composer test -fin admin +fin shellcheck +fin clicheck diff --git a/README.md b/README.md index ebcd09ff..462e3301 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Performance note — Xdebug may slow down the application. If you experience per Aggro includes several custom Docksal commands to help with development: -- `fin admin` — Run application maintenance tasks +- `fin clicheck` — Run application maintenance tasks - `fin deploy [env]` — Deploy to specified environment - `fin frontend` — Run front-end build process - `fin maintain` — Run upgrades and tests @@ -197,11 +197,9 @@ Note — Coverage reporting requires Xdebug to be enabled. Use `XDEBUG_MODE=cove ### Code Quality Checks ```bash -# Run all linting and static analysis -fin lint - # Run specific checks -fin sniff # PHP CodeSniffer +fin composer lint # PHP linting, static analysis +fin composer test # PHP unit tests fin shellcheck # Shell script linting # Individual quality tools diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 268f67f8..2e425e24 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -33,11 +33,19 @@ $routes->post('aggro/news-cache', 'Aggro::getNewsCache'); $routes->post('aggro/news-clean', 'Aggro::getNewsClean'); $routes->post('aggro/sweep', 'Aggro::getSweep'); +$routes->cli('aggro/log', 'Aggro::getLog'); +$routes->cli('aggro/log-error', 'Aggro::getLogError'); $routes->cli('aggro/log-clean', 'Aggro::getLogClean'); $routes->cli('aggro/log-error-clean', 'Aggro::getLogErrorClean'); $routes->cli('aggro/news-cache', 'Aggro::getNewsCache'); $routes->cli('aggro/news-clean', 'Aggro::getNewsClean'); $routes->cli('aggro/sweep', 'Aggro::getSweep'); +$routes->cli('aggro/news', 'Aggro::getNews'); +$routes->cli('aggro/vimeo', 'Aggro::getVimeo'); +$routes->cli('aggro/vimeo/(:segment)', 'Aggro::getVimeo/$1'); +$routes->cli('aggro/youtube', 'Aggro::getYoutube'); +$routes->cli('aggro/youtube/(:segment)', 'Aggro::getYoutube/$1'); +$routes->cli('aggro/duration', 'Aggro::getYouTubeDuration'); // Set 404 override $routes->set404Override('App\Controllers\Front::getError404'); diff --git a/composer.json b/composer.json index fa2c86c6..16693859 100644 --- a/composer.json +++ b/composer.json @@ -43,10 +43,10 @@ } }, "scripts": { - "test": "phpunit --no-coverage", - "test:unit": "phpunit tests/unit", - "test:feature": "phpunit tests/feature", - "test:coverage": "phpunit --coverage-html build/logs/html", + "test": "phpunit --testdox", + "test:unit": "phpunit tests/unit --testdox", + "test:feature": "phpunit tests/feature --testdox", + "test:coverage": "XDEBUG_MODE=coverage phpunit --coverage-html build/logs/html --coverage-clover build/logs/clover.xml --testdox", "phpcs": "phpcs --standard=phpcs.xml -v --colors", "phpcbf": "phpcbf --standard=phpcs.xml -v --colors --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer,$($COMPOSER_BINARY config vendor-dir)/sirbrillig/phpcs-variable-analysis,$($COMPOSER_BINARY config vendor-dir)/slevomat/coding-standard", "phpmd": [ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9948d6cd..519093e0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,14 +18,6 @@ ./app/Config/Routes.php - - - - - - - - ./tests diff --git a/tests/unit/CliRouteRegressionTest.php b/tests/unit/CliRouteRegressionTest.php new file mode 100644 index 00000000..417c4938 --- /dev/null +++ b/tests/unit/CliRouteRegressionTest.php @@ -0,0 +1,161 @@ +add() to + * $routes->get()), CLI routes need explicit $routes->cli() definitions. + * This test ensures all cron jobs have corresponding CLI route definitions. + * + * @internal + */ +final class CliRouteRegressionTest extends CIUnitTestCase +{ + private string $crontabPath; + private string $routesPath; + + protected function setUp(): void + { + parent::setUp(); + $this->crontabPath = ROOTPATH . '.crontab'; + $this->routesPath = APPPATH . 'Config/Routes.php'; + } + + /** + * Extract route paths from crontab entries. + * + * @return list Array of route paths (e.g., ['aggro/news', 'aggro/sweep']) + */ + private function getCrontabRoutes(): array + { + $this->assertFileExists($this->crontabPath, 'Crontab file not found'); + + $content = file_get_contents($this->crontabPath); + $lines = explode("\n", $content); + $routes = []; + + foreach ($lines as $line) { + $line = trim($line); + + // Skip empty lines and comments + if ($line === '' || str_starts_with($line, '#')) { + continue; + } + + // Skip variable definitions and DreamHost block + if (str_contains($line, '=') && ! str_contains($line, '$')) { + continue; + } + + // Match cron entries that invoke index.php with a route + // Pattern: $PHP_PATH $INDEX_PATH controller method [optional args] + // Routes in crontab are space-separated: "aggro news" becomes "aggro/news" + if (preg_match('/\$INDEX_PATH\s+(\S+)\s+(\S+)/', $line, $matches)) { + $routes[] = $matches[1] . '/' . $matches[2]; + } + } + + return array_unique($routes); + } + + /** + * Extract CLI route definitions from Routes.php. + * + * @return list Array of CLI route paths + */ + private function getCliRouteDefinitions(): array + { + $this->assertFileExists($this->routesPath, 'Routes.php file not found'); + + $content = file_get_contents($this->routesPath); + $routes = []; + + // Match $routes->cli('path', ...) patterns + if (preg_match_all('/\$routes->cli\([\'"]([^\'"]+)[\'"]/', $content, $matches)) { + $routes = $matches[1]; + } + + return $routes; + } + + /** + * Test that all crontab routes have corresponding CLI route definitions. + */ + public function testAllCrontabRoutesHaveCliDefinitions(): void + { + $crontabRoutes = $this->getCrontabRoutes(); + $cliRoutes = $this->getCliRouteDefinitions(); + + $this->assertNotEmpty($crontabRoutes, 'No routes found in crontab'); + $this->assertNotEmpty($cliRoutes, 'No CLI routes found in Routes.php'); + + $missingRoutes = []; + + foreach ($crontabRoutes as $crontabRoute) { + // Check if this route or a parameterized version exists + $routeFound = false; + + foreach ($cliRoutes as $cliRoute) { + // Direct match + if ($cliRoute === $crontabRoute) { + $routeFound = true; + + break; + } + + // Check for parameterized route (e.g., aggro/vimeo matches aggro/vimeo/(:segment)) + $baseRoute = preg_replace('/\/\([^)]+\)$/', '', $cliRoute); + if ($baseRoute === $crontabRoute) { + $routeFound = true; + + break; + } + } + + if (! $routeFound) { + $missingRoutes[] = $crontabRoute; + } + } + + $this->assertEmpty( + $missingRoutes, + sprintf( + "Missing CLI route definitions in Routes.php for crontab entries:\n- %s\n\n" + . 'Add $routes->cli() definitions for these routes to prevent CLI access failures.', + implode("\n- ", $missingRoutes), + ), + ); + } + + /** + * Test that Routes.php contains CLI route definitions. + */ + public function testRoutesFileHasCliDefinitions(): void + { + $this->assertFileExists($this->routesPath); + + $content = file_get_contents($this->routesPath); + + $this->assertStringContainsString( + '$routes->cli(', + $content, + 'Routes.php should contain CLI route definitions', + ); + } + + /** + * Test crontab file is readable and has expected structure. + */ + public function testCrontabFileStructure(): void + { + $this->assertFileExists($this->crontabPath); + + $content = file_get_contents($this->crontabPath); + + // Check for required variable definitions + $this->assertStringContainsString('PHP_PATH=', $content, 'Crontab should define PHP_PATH'); + $this->assertStringContainsString('INDEX_PATH=', $content, 'Crontab should define INDEX_PATH'); + } +} diff --git a/tests/unit/PhpVersionConsistencyTest.php b/tests/unit/PhpVersionConsistencyTest.php new file mode 100644 index 00000000..01155565 --- /dev/null +++ b/tests/unit/PhpVersionConsistencyTest.php @@ -0,0 +1,183 @@ +=8.4, 8.4.* + if (preg_match('/(\d+\.\d+)/', $constraint, $matches)) { + return $matches[1]; + } + } + + return null; + } + + /** + * Test that PHP version is consistent across all configuration files. + */ + public function testPhpVersionConsistency(): void + { + $versions = [ + '.crontab' => $this->getCrontabPhpVersion(), + '.github/workflows/deploy.yml' => $this->getDeployPhpVersion(), + '.docksal/docksal.env' => $this->getDocksalPhpVersion(), + 'composer.json' => $this->getComposerPhpVersion(), + ]; + + // Filter out null values (missing files) + $foundVersions = array_filter($versions, static fn ($v) => $v !== null); + + $this->assertNotEmpty($foundVersions, 'No PHP version found in any configuration file'); + + $uniqueVersions = array_unique($foundVersions); + + if (count($uniqueVersions) > 1) { + $message = "PHP version mismatch detected:\n"; + + foreach ($foundVersions as $file => $version) { + $message .= sprintf(" - %s: %s\n", $file, $version); + } + $message .= "\nAll configuration files should specify the same PHP version."; + + $this->fail($message); + } + + $this->assertCount(1, $uniqueVersions, 'PHP versions should be consistent'); + } + + /** + * Test that required configuration files exist. + */ + public function testRequiredConfigFilesExist(): void + { + $requiredFiles = [ + '.crontab' => ROOTPATH . '.crontab', + '.github/workflows/deploy.yml' => ROOTPATH . '.github/workflows/deploy.yml', + '.docksal/docksal.env' => ROOTPATH . '.docksal/docksal.env', + 'composer.json' => ROOTPATH . 'composer.json', + ]; + + foreach ($requiredFiles as $name => $path) { + $this->assertFileExists($path, sprintf('%s should exist', $name)); + } + } + + /** + * Test that PHP version can be extracted from each file. + */ + public function testPhpVersionExtractable(): void + { + $this->assertNotNull( + $this->getCrontabPhpVersion(), + 'Could not extract PHP version from .crontab', + ); + + $this->assertNotNull( + $this->getDeployPhpVersion(), + 'Could not extract PHP version from deploy.yml', + ); + + $this->assertNotNull( + $this->getDocksalPhpVersion(), + 'Could not extract PHP version from docksal.env', + ); + + $this->assertNotNull( + $this->getComposerPhpVersion(), + 'Could not extract PHP version from composer.json', + ); + } +}