Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
448 lines
17 KiB
PHP
448 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\Console;
|
|
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\Driver\InMemoryCache;
|
|
use App\Framework\Cache\GeneralCache;
|
|
use App\Framework\Config\AppConfig;
|
|
use App\Framework\Console\ConsoleApplication;
|
|
use App\Framework\Console\DemoCommand;
|
|
use App\Framework\Console\ExitCode;
|
|
use App\Framework\Context\ExecutionContext;
|
|
use App\Framework\Core\PathProvider;
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\DateTime\Timezone;
|
|
use App\Framework\DI\Container;
|
|
use App\Framework\DI\DefaultContainer;
|
|
use App\Framework\Discovery\InitializerProcessor;
|
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
|
use App\Framework\ReflectionLegacy\CachedReflectionProvider;
|
|
use App\Framework\ReflectionLegacy\ReflectionProvider;
|
|
use App\Framework\Serializer\Php\PhpSerializer;
|
|
use App\Framework\Serializer\Php\PhpSerializerConfig;
|
|
|
|
describe('ConsoleApplication Integration', function () {
|
|
beforeEach(function () {
|
|
// Create fresh container and dependencies for each test
|
|
$this->container = new DefaultContainer();
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->pathProvider = new PathProvider('/home/michael/dev/michaelschiemer');
|
|
|
|
// Register required dependencies
|
|
$this->container->singleton(Cache::class, $this->cache);
|
|
$this->container->singleton(Clock::class, $this->clock);
|
|
$this->container->singleton(PathProvider::class, $this->pathProvider);
|
|
|
|
// Register ReflectionProvider and ExecutionContext dependencies
|
|
$reflectionProvider = new CachedReflectionProvider();
|
|
$executionContext = ExecutionContext::detect();
|
|
$this->container->singleton(ReflectionProvider::class, $reflectionProvider);
|
|
$this->container->singleton(ExecutionContext::class, $executionContext);
|
|
|
|
$this->container->singleton(InitializerProcessor::class, fn ($c) => new InitializerProcessor(
|
|
$c,
|
|
$c->get(ReflectionProvider::class),
|
|
$c->get(ExecutionContext::class)
|
|
));
|
|
|
|
// Register DemoCommand
|
|
$this->container->singleton(DemoCommand::class, fn () => new DemoCommand());
|
|
|
|
// Create a simple AppConfig for testing
|
|
$appConfig = new AppConfig(
|
|
environment: 'testing',
|
|
debug: true,
|
|
timezone: Timezone::UTC,
|
|
locale: 'en'
|
|
);
|
|
$this->container->singleton(AppConfig::class, $appConfig);
|
|
|
|
// Clear cache
|
|
$this->cache->clear();
|
|
|
|
// Create a test output to capture console output
|
|
$this->output = new class () implements \App\Framework\Console\ConsoleOutputInterface {
|
|
public array $capturedLines = [];
|
|
|
|
public function write(string $message, null|\App\Framework\Console\ConsoleColor|\App\Framework\Console\ConsoleStyle $style = null): void
|
|
{
|
|
$this->capturedLines[] = $message;
|
|
}
|
|
|
|
public function writeLine(string $message = '', ?\App\Framework\Console\ConsoleColor $color = null): void
|
|
{
|
|
$this->capturedLines[] = $message;
|
|
}
|
|
|
|
public function writeError(string $message): void
|
|
{
|
|
$this->capturedLines[] = "ERROR: $message";
|
|
}
|
|
|
|
public function writeSuccess(string $message): void
|
|
{
|
|
$this->capturedLines[] = "SUCCESS: $message";
|
|
}
|
|
|
|
public function writeWarning(string $message): void
|
|
{
|
|
$this->capturedLines[] = "WARNING: $message";
|
|
}
|
|
|
|
public function writeInfo(string $message): void
|
|
{
|
|
$this->capturedLines[] = "INFO: $message";
|
|
}
|
|
|
|
public function newLine(int $count = 1): void
|
|
{
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$this->capturedLines[] = '';
|
|
}
|
|
}
|
|
|
|
public function askQuestion(string $question, ?string $default = null): string
|
|
{
|
|
return $default ?? '';
|
|
}
|
|
|
|
public function confirm(string $question, bool $default = false): bool
|
|
{
|
|
return $default;
|
|
}
|
|
|
|
public function writeWindowTitle(string $title, int $mode = 0): void
|
|
{
|
|
// No-op for testing
|
|
}
|
|
};
|
|
});
|
|
|
|
it('properly initializes CommandRegistry with discovery', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Should not throw exception during initialization
|
|
expect($app)->toBeInstanceOf(ConsoleApplication::class);
|
|
|
|
// Initialization should complete successfully (no exceptions thrown)
|
|
// The fact that we got here means discovery and command registry setup worked
|
|
});
|
|
|
|
it('handles empty command list with fallback discovery', function () {
|
|
// First create a scenario where no commands would be found initially
|
|
$emptyRegistry = new DiscoveryRegistry(
|
|
attributes: new \App\Framework\Discovery\Results\AttributeRegistry([]),
|
|
interfaces: new \App\Framework\Discovery\Results\InterfaceRegistry([]),
|
|
templates: new \App\Framework\Discovery\Results\TemplateRegistry([])
|
|
);
|
|
|
|
// Register the empty registry initially
|
|
$this->container->singleton(DiscoveryRegistry::class, $emptyRegistry);
|
|
|
|
// ConsoleApplication should trigger fallback discovery
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Should have attempted fallback discovery
|
|
$loggedOutput = implode(' ', $this->output->capturedLines);
|
|
|
|
// Either it found commands through fallback or handled the empty case gracefully
|
|
expect($app)->toBeInstanceOf(ConsoleApplication::class);
|
|
});
|
|
|
|
it('can execute demo commands successfully', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test demo:hello command
|
|
$argv = ['test-console', 'demo:hello', 'TestName'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS->value);
|
|
|
|
// Should have captured the hello output
|
|
$output = implode(' ', $this->output->capturedLines);
|
|
expect($output)->toContain('Hallo, TestName!');
|
|
});
|
|
|
|
it('shows help when no command is provided', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Run with no command
|
|
$argv = ['test-console'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS->value);
|
|
|
|
// Should show help/available commands
|
|
$output = implode(' ', $this->output->capturedLines);
|
|
expect($output)->toContain('Verfügbare Kommandos');
|
|
});
|
|
|
|
it('handles unknown commands gracefully', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Try to run non-existent command
|
|
$argv = ['test-console', 'nonexistent:command'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
expect($exitCode)->toBe(ExitCode::COMMAND_NOT_FOUND->value);
|
|
|
|
// Should show error message
|
|
$output = implode(' ', $this->output->capturedLines);
|
|
expect($output)->toContain('nicht gefunden');
|
|
});
|
|
|
|
it('suggests similar commands for typos', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Try command with typo (demo:helo instead of demo:hello)
|
|
$argv = ['test-console', 'demo:helo'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
expect($exitCode)->toBe(ExitCode::COMMAND_NOT_FOUND->value);
|
|
|
|
// Should suggest similar commands
|
|
$output = implode(' ', $this->output->capturedLines);
|
|
expect($output)->toContain('Meinten Sie vielleicht');
|
|
});
|
|
|
|
it('handles help command', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test various help invocations
|
|
$helpCommands = ['help', '--help', '-h'];
|
|
|
|
foreach ($helpCommands as $helpCmd) {
|
|
$this->output->capturedLines = []; // Clear previous output
|
|
|
|
$argv = ['test-console', $helpCmd];
|
|
$exitCode = $app->run($argv);
|
|
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS->value);
|
|
|
|
$output = implode(' ', $this->output->capturedLines);
|
|
expect($output)->toContain('Verfügbare Kommandos');
|
|
}
|
|
});
|
|
|
|
it('finds expected number of commands after discovery', function () {
|
|
// Force fresh discovery by clearing cache
|
|
$this->cache->clear();
|
|
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Run help to see available commands
|
|
$argv = ['test-console', 'help'];
|
|
$app->run($argv);
|
|
|
|
$output = implode(' ', $this->output->capturedLines);
|
|
|
|
// Should find demo commands
|
|
expect($output)->toContain('demo:hello');
|
|
expect($output)->toContain('demo:colors');
|
|
expect($output)->toContain('demo:interactive');
|
|
|
|
// Should not show "Keine Kommandos verfügbar"
|
|
expect($output)->not->toContain('Keine Kommandos verfügbar');
|
|
});
|
|
|
|
it('validates and sanitizes input arguments', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test with various argument types
|
|
$argv = ['test-console', 'demo:hello', 'ValidName', '--option=value'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS->value);
|
|
|
|
// Should have processed arguments without error
|
|
$output = implode(' ', $this->output->capturedLines);
|
|
expect($output)->toContain('Hallo, ValidName!');
|
|
});
|
|
|
|
it('handles empty argv gracefully', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test with empty argv (should not happen in practice, but test robustness)
|
|
try {
|
|
$exitCode = $app->run([]);
|
|
// If it doesn't throw, should return an error code
|
|
expect($exitCode)->toBeInt();
|
|
} catch (\InvalidArgumentException $e) {
|
|
// Expected behavior for invalid input
|
|
expect($e->getMessage())->toContain('No arguments provided');
|
|
}
|
|
});
|
|
|
|
it('shows command usage for invalid arguments', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// This test depends on actual command implementation
|
|
// For now, just ensure the app can handle commands
|
|
$argv = ['test-console', 'demo:hello'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
// Should succeed or fail gracefully
|
|
expect($exitCode)->toBeInt();
|
|
expect($exitCode)->toBeGreaterThanOrEqual(0);
|
|
expect($exitCode)->toBeLessThan(256);
|
|
});
|
|
});
|
|
|
|
describe('ConsoleApplication Error Handling', function () {
|
|
beforeEach(function () {
|
|
$this->container = new DefaultContainer();
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->pathProvider = new PathProvider('/home/michael/dev/michaelschiemer');
|
|
|
|
$this->container->singleton(Cache::class, $this->cache);
|
|
$this->container->singleton(Clock::class, $this->clock);
|
|
$this->container->singleton(PathProvider::class, $this->pathProvider);
|
|
|
|
// Register ReflectionProvider and ExecutionContext dependencies
|
|
$reflectionProvider = new CachedReflectionProvider();
|
|
$executionContext = ExecutionContext::detect();
|
|
$this->container->singleton(ReflectionProvider::class, $reflectionProvider);
|
|
$this->container->singleton(ExecutionContext::class, $executionContext);
|
|
|
|
$this->container->singleton(InitializerProcessor::class, fn ($c) => new InitializerProcessor(
|
|
$c,
|
|
$c->get(ReflectionProvider::class),
|
|
$c->get(ExecutionContext::class)
|
|
));
|
|
$this->container->singleton(DemoCommand::class, fn () => new DemoCommand());
|
|
|
|
$appConfig = new AppConfig(
|
|
environment: 'testing',
|
|
debug: true,
|
|
timezone: Timezone::UTC,
|
|
locale: 'en'
|
|
);
|
|
$this->container->singleton(AppConfig::class, $appConfig);
|
|
|
|
$this->output = new class () implements \App\Framework\Console\ConsoleOutputInterface {
|
|
public array $capturedLines = [];
|
|
|
|
public function write(string $message, null|\App\Framework\Console\ConsoleColor|\App\Framework\Console\ConsoleStyle $style = null): void
|
|
{
|
|
$this->capturedLines[] = $message;
|
|
}
|
|
|
|
public function writeLine(string $message = '', ?\App\Framework\Console\ConsoleColor $color = null): void
|
|
{
|
|
$this->capturedLines[] = $message;
|
|
}
|
|
|
|
public function writeError(string $message): void
|
|
{
|
|
$this->capturedLines[] = "ERROR: $message";
|
|
}
|
|
|
|
public function writeSuccess(string $message): void
|
|
{
|
|
$this->capturedLines[] = "SUCCESS: $message";
|
|
}
|
|
|
|
public function writeWarning(string $message): void
|
|
{
|
|
$this->capturedLines[] = "WARNING: $message";
|
|
}
|
|
|
|
public function writeInfo(string $message): void
|
|
{
|
|
$this->capturedLines[] = "INFO: $message";
|
|
}
|
|
|
|
public function newLine(int $count = 1): void
|
|
{
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$this->capturedLines[] = '';
|
|
}
|
|
}
|
|
|
|
public function askQuestion(string $question, ?string $default = null): string
|
|
{
|
|
return $default ?? '';
|
|
}
|
|
|
|
public function confirm(string $question, bool $default = false): bool
|
|
{
|
|
return $default;
|
|
}
|
|
|
|
public function writeWindowTitle(string $title, int $mode = 0): void
|
|
{
|
|
// No-op for testing
|
|
}
|
|
};
|
|
});
|
|
|
|
it('handles framework exceptions gracefully', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test with valid command to ensure basic functionality
|
|
$argv = ['test-console', 'demo:hello'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
// Should handle any framework exceptions gracefully
|
|
expect($exitCode)->toBeInt();
|
|
expect($exitCode)->toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('provides appropriate exit codes for different error types', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test command not found
|
|
$argv = ['test-console', 'nonexistent:command'];
|
|
$exitCode = $app->run($argv);
|
|
expect($exitCode)->toBe(ExitCode::COMMAND_NOT_FOUND->value);
|
|
|
|
// Test successful command
|
|
$argv = ['test-console', 'demo:hello'];
|
|
$exitCode = $app->run($argv);
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS->value);
|
|
});
|
|
|
|
it('shows debug information in development mode', function () {
|
|
// AppConfig is already set to debug mode in beforeEach
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test with any command
|
|
$argv = ['test-console', 'demo:hello'];
|
|
$exitCode = $app->run($argv);
|
|
|
|
// Should complete without throwing exceptions
|
|
expect($exitCode)->toBeInt();
|
|
});
|
|
|
|
it('handles malformed arguments gracefully', function () {
|
|
$app = new ConsoleApplication($this->container, 'test-console', 'Test Console', $this->output);
|
|
|
|
// Test with various potentially problematic arguments
|
|
$testCases = [
|
|
['test-console', 'demo:hello', ''], // Empty argument
|
|
['test-console', 'demo:hello', 'normal-arg'], // Normal case
|
|
];
|
|
|
|
foreach ($testCases as $argv) {
|
|
$this->output->capturedLines = [];
|
|
|
|
$exitCode = $app->run($argv);
|
|
|
|
// Should not crash, return valid exit code
|
|
expect($exitCode)->toBeInt();
|
|
expect($exitCode)->toBeGreaterThanOrEqual(0);
|
|
expect($exitCode)->toBeLessThan(256);
|
|
}
|
|
});
|
|
});
|