Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .crontab
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 0 additions & 36 deletions .docksal/commands/admin

This file was deleted.

59 changes: 59 additions & 0 deletions .docksal/commands/clicheck
Original file line number Diff line number Diff line change
@@ -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 <h1>404</h1>)
if echo "$OUTPUT" | grep -q "<h1>404</h1>"; 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"
6 changes: 1 addition & 5 deletions .docksal/commands/maintain
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,4 @@

cd "$PROJECT_ROOT" || exit
fin upgrade
fin init
fin check
fin sniff
fin shellcheck
fin admin
fin test
8 changes: 0 additions & 8 deletions .docksal/commands/sniff

This file was deleted.

6 changes: 3 additions & 3 deletions .docksal/commands/test
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions app/Config/Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
8 changes: 0 additions & 8 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@
<file>./app/Config/Routes.php</file>
</exclude>
</source>
<coverage includeUncoveredFiles="true">
<report>
<clover outputFile="build/logs/clover.xml"/>
<html outputDirectory="build/logs/html"/>
<php outputFile="build/logs/coverage.serialized"/>
<text outputFile="php://stdout" showUncoveredFiles="false"/>
</report>
</coverage>
<testsuites>
<testsuite name="App">
<directory>./tests</directory>
Expand Down
161 changes: 161 additions & 0 deletions tests/unit/CliRouteRegressionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

use CodeIgniter\Test\CIUnitTestCase;

/**
* Tests to prevent CLI route regressions.
*
* When HTTP method restrictions are added (e.g., changing $routes->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<string> 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<string> 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');
}
}
Loading