Phase 1 sub-commit 3: bridge:doctor console command
All checks were successful
CI / Quality (push) Successful in 5s

Console command bridge:doctor surfaces actionable hints for env / wiring
problems so first-run failures aren't a "connection refused" mystery.
Checks PHP version, ext-curl, ext-json, the Publisher service is wired
(meaning BridgeBundle loaded), and the BRIDGE_TOKEN / MERCURE_URL /
MERCURE_PUBLISHER_JWT_KEY / MERCURE_SUBSCRIBER_JWT_KEY env vars. With
--connect, also probes the configured URL via plain stream context (no
extra dep) and fails the run when unreachable.

CommandTester suite covers green path, missing-env path, and an
unreachable-URL probe — 11 tests, 32 assertions, all green.

Skeleton's Makefile target stays a TBD until sub-commit 6 stands up the
Symfony app the command runs from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 01:08:09 +02:00
parent eafe12b588
commit b3932674dd
2 changed files with 224 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace PhpQml\Bridge\Tests\Command;
use PhpQml\Bridge\Command\BridgeDoctorCommand;
use PhpQml\Bridge\Publisher;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
#[CoversClass(BridgeDoctorCommand::class)]
final class BridgeDoctorCommandTest extends TestCase
{
public function testAllPassesWhenEnvIsFullyConfigured(): void
{
$command = new BridgeDoctorCommand(
publisher: $this->makePublisher(),
bridgeToken: 'devtoken',
mercureUrl: 'http://127.0.0.1:8765/.well-known/mercure',
mercurePublisherKey: 'devkey',
mercureSubscriberKey: 'devkey',
);
$tester = new CommandTester($command);
$exit = $tester->execute([]);
self::assertSame(0, $exit);
$display = $tester->getDisplay();
self::assertStringContainsString('All checks passed', $display);
self::assertStringNotContainsString('FAIL', $display);
}
public function testFailsAndShowsHintsWhenEnvIsMissing(): void
{
$command = new BridgeDoctorCommand(
publisher: $this->makePublisher(),
bridgeToken: '',
mercureUrl: '',
mercurePublisherKey: '',
mercureSubscriberKey: '',
);
$tester = new CommandTester($command);
$exit = $tester->execute([]);
self::assertSame(1, $exit);
$display = $tester->getDisplay();
self::assertStringContainsString('BRIDGE_TOKEN env set', $display);
self::assertStringContainsString('MERCURE_URL env set', $display);
self::assertStringContainsString('Set BRIDGE_TOKEN in .env.local', $display);
self::assertStringContainsString('Some checks failed', $display);
}
public function testConnectOptionFailsClosedAgainstAnUnreachableUrl(): void
{
$command = new BridgeDoctorCommand(
publisher: $this->makePublisher(),
bridgeToken: 'devtoken',
mercureUrl: 'http://127.0.0.1:8765/.well-known/mercure',
mercurePublisherKey: 'devkey',
mercureSubscriberKey: 'devkey',
);
$tester = new CommandTester($command);
// Port 1 is reserved and refuses connections — the probe must fail.
$exit = $tester->execute(['--connect' => true, '--url' => 'http://127.0.0.1:1/healthz']);
self::assertSame(1, $exit);
self::assertStringContainsString('Backend probe failed', $tester->getDisplay());
}
private function makePublisher(): Publisher
{
return new Publisher(new class implements HubInterface {
public function getUrl(): string { return ''; }
public function getPublicUrl(): string { return ''; }
public function getProvider(): \Symfony\Component\Mercure\Jwt\TokenProviderInterface
{ throw new \LogicException(); }
public function getFactory(): ?\Symfony\Component\Mercure\Jwt\TokenFactoryInterface
{ return null; }
public function publish(Update $update): string { return ''; }
});
}
}