* SPDX-License-Identifier: GPL-3.0-or-later */ declare(strict_types=1); namespace MokoStandards\Tests\Unit; use MokoEnterprise\CliFramework; use PHPUnit\Framework\TestCase; /** * Unit tests for CliFramework base class. * * @covers \MokoEnterprise\CliFramework */ class CliFrameworkTest extends TestCase { // ── Exit code constants ────────────────────────────────────────────── public function testExitCodeConstants(): void { $this->assertSame(0, CliFramework::EXIT_SUCCESS); $this->assertSame(1, CliFramework::EXIT_FAILURE); $this->assertSame(2, CliFramework::EXIT_USAGE); $this->assertSame(3, CliFramework::EXIT_NOT_FOUND); $this->assertSame(4, CliFramework::EXIT_PERMISSION); } // ── JSON output ───────────────────────────────────────────────────── public function testJsonOutputProducesValidJson(): void { $tool = $this->createTool(function ($self) { return $self->jsonOutput('pass', ['key' => 'value'], [], ['soft warning']); }); $output = $this->captureOutput($tool); $decoded = json_decode($output, true); $this->assertNotNull($decoded, 'jsonOutput must produce valid JSON'); $this->assertSame('pass', $decoded['status']); $this->assertSame(0, $decoded['exit_code']); $this->assertSame(['key' => 'value'], $decoded['data']); $this->assertSame([], $decoded['errors']); $this->assertSame(['soft warning'], $decoded['warnings']); $this->assertArrayHasKey('duration_ms', $decoded['metadata']); $this->assertArrayHasKey('timestamp', $decoded['metadata']); } public function testJsonOutputFailStatus(): void { $tool = $this->createTool(function ($self) { return $self->jsonOutput('fail', null, ['something broke']); }); $output = $this->captureOutput($tool); $decoded = json_decode($output, true); $this->assertSame('fail', $decoded['status']); $this->assertSame(1, $decoded['exit_code']); $this->assertSame(['something broke'], $decoded['errors']); } public function testJsonOutputCustomExitCode(): void { $tool = $this->createTool(function ($self) { return $self->jsonOutput('error', null, ['not found'], [], 3); }); $output = $this->captureOutput($tool); $decoded = json_decode($output, true); $this->assertSame(3, $decoded['exit_code']); } // ── Table rendering ───────────────────────────────────────────────── public function testTableRendersHeadersAndRows(): void { $tool = $this->createTool(function ($self) { $self->table(['Name', 'Status'], [ ['foo', 'ok'], ['bar', 'fail'], ]); return 0; }); // Table output is suppressed by --quiet, so run without it. $_SERVER['argv'] = ['test', '--no-color']; ob_start(); $tool->execute(); $output = ob_get_clean() ?: ''; $this->assertStringContainsString('Name', $output); $this->assertStringContainsString('Status', $output); $this->assertStringContainsString('foo', $output); $this->assertStringContainsString('bar', $output); $this->assertStringContainsString('+', $output); // separator } // ── Helpers ────────────────────────────────────────────────────────── /** * Create a testable CliFramework subclass with a custom run() callback. */ private function createTool(callable $runCallback): CliFramework { return new class ($runCallback) extends CliFramework { private $callback; public function __construct(callable $callback) { $this->callback = $callback; parent::__construct('test-tool', '1.0.0'); } protected function configure(): void { $this->setDescription('Test tool'); } protected function run(): int { return ($this->callback)($this); } // Expose protected methods for testing. public function jsonOutput( string $status, mixed $data = null, array $errors = [], array $warnings = [], int $exitCode = -1 ): int { return parent::jsonOutput($status, $data, $errors, $warnings, $exitCode); } public function table(array $headers, array $rows): void { parent::table($headers, $rows); } }; } /** * Capture stdout from a tool's run method via execute(). * * The banner and other output is included, so JSON tests should * extract the JSON portion (last complete JSON object) from the output. */ private function captureOutput(CliFramework $tool): string { // Suppress banner by simulating --quiet, but jsonOutput still writes. $_SERVER['argv'] = ['test', '--quiet']; ob_start(); $tool->execute(); $output = ob_get_clean() ?: ''; // Extract JSON object from output (may have log lines before it). if (preg_match('/\{[\s\S]*\}\s*$/m', $output, $m)) { return $m[0]; } return $output; } }