PHPStan (level 6 + symfony extension) and PHP CS Fixer (Symfony + PHP83Migration ruleset) configs at framework/php/. composer.json exposes phpstan / cs:check / cs:fix / phpunit / quality scripts. PHPStan-clean across the bundle; cs:check is happy after auto-fix applied @Symfony idioms (yoda, leading-backslash JSON_*, blank-line before return). Test mocks consolidated into a HubSpy helper to keep PHPStan happy about by-ref captures. Skeleton's Makefile target `quality` chains `composer quality` (in framework/php/) with cmake's all_qmllint target. Local run is green — 11 tests / 32 assertions, no PHPStan errors, cs-fixer clean, qmllint emits advisory warnings only. Layout fix in skeleton's Main.qml: status-dot Rectangles inside RowLayout now use Layout.preferredWidth/Height instead of width/height to satisfy Quick.layout-positioning checks. .gitea/workflows/ci.yml replaces the placeholder with a real `quality` job: setup-php, composer install (cached), the four PHP checks, Qt 6 via install-qt-action (cached), QML module build, qmllint via the all_qmllint CMake target. Workflow exists from this commit onward even if a runner isn't provisioned yet. bridge:doctor lost the Publisher dependency since it was only used as a "service is wired" marker — the command being injectable already proves that. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,60 @@ jobs:
|
|||||||
quality:
|
quality:
|
||||||
name: Quality
|
name: Quality
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Quality (placeholder)
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: curl, json, mbstring
|
||||||
|
tools: composer:v2
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Cache Composer
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/composer
|
||||||
|
key: composer-${{ hashFiles('framework/php/composer.json') }}
|
||||||
|
|
||||||
|
- name: Install bundle dependencies
|
||||||
|
working-directory: framework/php
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: PHPStan
|
||||||
|
working-directory: framework/php
|
||||||
|
run: composer phpstan
|
||||||
|
|
||||||
|
- name: php-cs-fixer (check)
|
||||||
|
working-directory: framework/php
|
||||||
|
run: composer cs:check
|
||||||
|
|
||||||
|
- name: PHPUnit
|
||||||
|
working-directory: framework/php
|
||||||
|
run: composer phpunit
|
||||||
|
|
||||||
|
- name: Setup Qt 6
|
||||||
|
uses: jurplel/install-qt-action@v4
|
||||||
|
with:
|
||||||
|
version: '6.5.*'
|
||||||
|
modules: 'qtquickcontrols2'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Cache CMake build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: framework/skeleton/build
|
||||||
|
key: cmake-${{ runner.os }}-${{ hashFiles('framework/qml/**', 'framework/skeleton/qml/**') }}
|
||||||
|
|
||||||
|
- name: Build QML module + skeleton
|
||||||
|
working-directory: framework/skeleton
|
||||||
run: |
|
run: |
|
||||||
echo "quality job — populated in Phase 1 sub-commit 7"
|
cmake -S qml -B build/qml
|
||||||
echo "will run: PHPStan, php-cs-fixer (check), PHPUnit, qmllint"
|
cmake --build build/qml --parallel
|
||||||
|
|
||||||
|
- name: qmllint
|
||||||
|
working-directory: framework/skeleton
|
||||||
|
run: cmake --build build/qml --target all_qmllint
|
||||||
|
|||||||
17
framework/php/.php-cs-fixer.dist.php
Normal file
17
framework/php/.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$finder = (new PhpCsFixer\Finder())
|
||||||
|
->in([__DIR__ . '/src', __DIR__ . '/tests']);
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config())
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules([
|
||||||
|
'@Symfony' => true,
|
||||||
|
'@Symfony:risky' => true,
|
||||||
|
'@PHP83Migration' => true,
|
||||||
|
'declare_strict_types' => true,
|
||||||
|
'native_function_invocation' => false,
|
||||||
|
'phpdoc_align' => false,
|
||||||
|
'binary_operator_spaces' => ['default' => 'align_single_space_minimal'],
|
||||||
|
])
|
||||||
|
->setFinder($finder);
|
||||||
@@ -31,5 +31,16 @@
|
|||||||
"PhpQml\\Bridge\\Tests\\": "tests/"
|
"PhpQml\\Bridge\\Tests\\": "tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"phpstan": "phpstan analyse --memory-limit=512M",
|
||||||
|
"cs:check": "php-cs-fixer check --diff --allow-unsupported-php-version=yes",
|
||||||
|
"cs:fix": "php-cs-fixer fix --allow-unsupported-php-version=yes",
|
||||||
|
"phpunit": "phpunit",
|
||||||
|
"quality": [
|
||||||
|
"@phpstan",
|
||||||
|
"@cs:check",
|
||||||
|
"@phpunit"
|
||||||
|
]
|
||||||
|
},
|
||||||
"minimum-stability": "stable"
|
"minimum-stability": "stable"
|
||||||
}
|
}
|
||||||
|
|||||||
10
framework/php/phpstan.neon.dist
Normal file
10
framework/php/phpstan.neon.dist
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
includes:
|
||||||
|
- vendor/phpstan/phpstan-symfony/extension.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: 6
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests
|
||||||
|
excludePaths:
|
||||||
|
- vendor/*
|
||||||
@@ -11,9 +11,12 @@ use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
|
|||||||
|
|
||||||
final class BridgeBundle extends AbstractBundle
|
final class BridgeBundle extends AbstractBundle
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $config
|
||||||
|
*/
|
||||||
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
|
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
|
||||||
{
|
{
|
||||||
$container->import(__DIR__ . '/../config/services.yaml');
|
$container->import(__DIR__.'/../config/services.yaml');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure(DefinitionConfigurator $definition): void
|
public function configure(DefinitionConfigurator $definition): void
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace PhpQml\Bridge\Command;
|
namespace PhpQml\Bridge\Command;
|
||||||
|
|
||||||
use PhpQml\Bridge\Publisher;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -23,7 +22,6 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|||||||
final class BridgeDoctorCommand extends Command
|
final class BridgeDoctorCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Publisher $publisher,
|
|
||||||
#[Autowire('%env(default::BRIDGE_TOKEN)%')]
|
#[Autowire('%env(default::BRIDGE_TOKEN)%')]
|
||||||
private readonly string $bridgeToken,
|
private readonly string $bridgeToken,
|
||||||
#[Autowire('%env(default::MERCURE_URL)%')]
|
#[Autowire('%env(default::MERCURE_URL)%')]
|
||||||
@@ -47,30 +45,26 @@ final class BridgeDoctorCommand extends Command
|
|||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$io->title('php-qml bridge — readiness checks');
|
$io->title('php-qml bridge — readiness checks');
|
||||||
|
|
||||||
|
// PHP version + extensions are enforced by composer, so we don't
|
||||||
|
// re-check them runtime — would always be true on a working install.
|
||||||
$checks = [
|
$checks = [
|
||||||
['PHP version >= 8.3',
|
|
||||||
PHP_VERSION_ID >= 80300,
|
|
||||||
'Upgrade PHP to 8.3 or newer; the bundle requires it.'],
|
|
||||||
['ext-curl available',
|
['ext-curl available',
|
||||||
extension_loaded('curl'),
|
extension_loaded('curl'),
|
||||||
'Install the PHP curl extension.'],
|
'Install the PHP curl extension.'],
|
||||||
['ext-json available',
|
['ext-json available',
|
||||||
extension_loaded('json'),
|
extension_loaded('json'),
|
||||||
'Install the PHP json extension.'],
|
'Install the PHP json extension.'],
|
||||||
['Publisher service wired',
|
|
||||||
$this->publisher instanceof Publisher,
|
|
||||||
'Bundle services failed to load — register PhpQml\\Bridge\\BridgeBundle in config/bundles.php.'],
|
|
||||||
['BRIDGE_TOKEN env set',
|
['BRIDGE_TOKEN env set',
|
||||||
$this->bridgeToken !== '',
|
'' !== $this->bridgeToken,
|
||||||
'Set BRIDGE_TOKEN in .env.local; the Qt host expects this as the bearer token.'],
|
'Set BRIDGE_TOKEN in .env.local; the Qt host expects this as the bearer token.'],
|
||||||
['MERCURE_URL env set',
|
['MERCURE_URL env set',
|
||||||
$this->mercureUrl !== '',
|
'' !== $this->mercureUrl,
|
||||||
'Set MERCURE_URL in .env, e.g. http://127.0.0.1:8765/.well-known/mercure.'],
|
'Set MERCURE_URL in .env, e.g. http://127.0.0.1:8765/.well-known/mercure.'],
|
||||||
['MERCURE_PUBLISHER_JWT_KEY env set',
|
['MERCURE_PUBLISHER_JWT_KEY env set',
|
||||||
$this->mercurePublisherKey !== '',
|
'' !== $this->mercurePublisherKey,
|
||||||
'Set MERCURE_PUBLISHER_JWT_KEY in .env.local; mercure-bundle uses it to sign publish requests.'],
|
'Set MERCURE_PUBLISHER_JWT_KEY in .env.local; mercure-bundle uses it to sign publish requests.'],
|
||||||
['MERCURE_SUBSCRIBER_JWT_KEY env set',
|
['MERCURE_SUBSCRIBER_JWT_KEY env set',
|
||||||
$this->mercureSubscriberKey !== '',
|
'' !== $this->mercureSubscriberKey,
|
||||||
'Set MERCURE_SUBSCRIBER_JWT_KEY in .env.local; or rely on the Caddy `anonymous` directive in dev mode.'],
|
'Set MERCURE_SUBSCRIBER_JWT_KEY in .env.local; or rely on the Caddy `anonymous` directive in dev mode.'],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -100,10 +94,12 @@ final class BridgeDoctorCommand extends Command
|
|||||||
|
|
||||||
if ($allPass) {
|
if ($allPass) {
|
||||||
$io->success('All checks passed. Bridge is ready.');
|
$io->success('All checks passed. Bridge is ready.');
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
$io->warning('Some checks failed — fix the items above before running the bridge.');
|
$io->warning('Some checks failed — fix the items above before running the bridge.');
|
||||||
|
|
||||||
return Command::FAILURE;
|
return Command::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,17 +116,19 @@ final class BridgeDoctorCommand extends Command
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$body = @file_get_contents($url, false, $ctx);
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
if ($body === false) {
|
if (false === $body) {
|
||||||
$err = error_get_last()['message'] ?? 'unknown error';
|
$err = error_get_last()['message'] ?? 'unknown error';
|
||||||
|
|
||||||
return ['ok' => false, 'detail' => $err];
|
return ['ok' => false, 'detail' => $err];
|
||||||
}
|
}
|
||||||
$statusLine = $http_response_header[0] ?? '';
|
$statusLine = $http_response_header[0] ?? '';
|
||||||
preg_match('#HTTP/\S+\s+(\d+)#', $statusLine, $m);
|
preg_match('#HTTP/\S+\s+(\d+)#', $statusLine, $m);
|
||||||
$status = (int) ($m[1] ?? 0);
|
$status = (int) ($m[1] ?? 0);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => $status === 200,
|
'ok' => 200 === $status,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'detail' => $status === 200 ? '' : "expected 200, got {$status}",
|
'detail' => 200 === $status ? '' : "expected 200, got {$status}",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ final readonly class Publisher
|
|||||||
{
|
{
|
||||||
return $this->hub->publish(new Update(
|
return $this->hub->publish(new Update(
|
||||||
$topic,
|
$topic,
|
||||||
json_encode($envelope, JSON_THROW_ON_ERROR),
|
json_encode($envelope, \JSON_THROW_ON_ERROR),
|
||||||
$private,
|
$private,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ final class SessionAuthenticator extends AbstractAuthenticator
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports(Request $request): ?bool
|
public function supports(Request $request): bool
|
||||||
{
|
{
|
||||||
return $request->headers->has('Authorization');
|
return $request->headers->has('Authorization');
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ final class SessionAuthenticator extends AbstractAuthenticator
|
|||||||
}
|
}
|
||||||
|
|
||||||
$token = substr($header, 7);
|
$token = substr($header, 7);
|
||||||
if ($this->expectedToken === '' || !hash_equals($this->expectedToken, $token)) {
|
if ('' === $this->expectedToken || !hash_equals($this->expectedToken, $token)) {
|
||||||
throw new AuthenticationException('Bearer token invalid.');
|
throw new AuthenticationException('Bearer token invalid.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ final class SessionAuthenticator extends AbstractAuthenticator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||||
{
|
{
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace PhpQml\Bridge\Tests\Command;
|
namespace PhpQml\Bridge\Tests\Command;
|
||||||
|
|
||||||
use PhpQml\Bridge\Command\BridgeDoctorCommand;
|
use PhpQml\Bridge\Command\BridgeDoctorCommand;
|
||||||
use PhpQml\Bridge\Publisher;
|
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Symfony\Component\Mercure\HubInterface;
|
|
||||||
use Symfony\Component\Mercure\Update;
|
|
||||||
|
|
||||||
#[CoversClass(BridgeDoctorCommand::class)]
|
#[CoversClass(BridgeDoctorCommand::class)]
|
||||||
final class BridgeDoctorCommandTest extends TestCase
|
final class BridgeDoctorCommandTest extends TestCase
|
||||||
@@ -18,7 +15,6 @@ final class BridgeDoctorCommandTest extends TestCase
|
|||||||
public function testAllPassesWhenEnvIsFullyConfigured(): void
|
public function testAllPassesWhenEnvIsFullyConfigured(): void
|
||||||
{
|
{
|
||||||
$command = new BridgeDoctorCommand(
|
$command = new BridgeDoctorCommand(
|
||||||
publisher: $this->makePublisher(),
|
|
||||||
bridgeToken: 'devtoken',
|
bridgeToken: 'devtoken',
|
||||||
mercureUrl: 'http://127.0.0.1:8765/.well-known/mercure',
|
mercureUrl: 'http://127.0.0.1:8765/.well-known/mercure',
|
||||||
mercurePublisherKey: 'devkey',
|
mercurePublisherKey: 'devkey',
|
||||||
@@ -37,7 +33,6 @@ final class BridgeDoctorCommandTest extends TestCase
|
|||||||
public function testFailsAndShowsHintsWhenEnvIsMissing(): void
|
public function testFailsAndShowsHintsWhenEnvIsMissing(): void
|
||||||
{
|
{
|
||||||
$command = new BridgeDoctorCommand(
|
$command = new BridgeDoctorCommand(
|
||||||
publisher: $this->makePublisher(),
|
|
||||||
bridgeToken: '',
|
bridgeToken: '',
|
||||||
mercureUrl: '',
|
mercureUrl: '',
|
||||||
mercurePublisherKey: '',
|
mercurePublisherKey: '',
|
||||||
@@ -58,7 +53,6 @@ final class BridgeDoctorCommandTest extends TestCase
|
|||||||
public function testConnectOptionFailsClosedAgainstAnUnreachableUrl(): void
|
public function testConnectOptionFailsClosedAgainstAnUnreachableUrl(): void
|
||||||
{
|
{
|
||||||
$command = new BridgeDoctorCommand(
|
$command = new BridgeDoctorCommand(
|
||||||
publisher: $this->makePublisher(),
|
|
||||||
bridgeToken: 'devtoken',
|
bridgeToken: 'devtoken',
|
||||||
mercureUrl: 'http://127.0.0.1:8765/.well-known/mercure',
|
mercureUrl: 'http://127.0.0.1:8765/.well-known/mercure',
|
||||||
mercurePublisherKey: 'devkey',
|
mercurePublisherKey: 'devkey',
|
||||||
@@ -72,17 +66,4 @@ final class BridgeDoctorCommandTest extends TestCase
|
|||||||
self::assertSame(1, $exit);
|
self::assertSame(1, $exit);
|
||||||
self::assertStringContainsString('Backend probe failed', $tester->getDisplay());
|
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 ''; }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
50
framework/php/tests/Helper/HubSpy.php
Normal file
50
framework/php/tests/Helper/HubSpy.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpQml\Bridge\Tests\Helper;
|
||||||
|
|
||||||
|
use Symfony\Component\Mercure\HubInterface;
|
||||||
|
use Symfony\Component\Mercure\Jwt\TokenFactoryInterface;
|
||||||
|
use Symfony\Component\Mercure\Jwt\TokenProviderInterface;
|
||||||
|
use Symfony\Component\Mercure\Update;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal HubInterface fake that records the last published Update.
|
||||||
|
* Used by Publisher / BridgeDoctorCommand tests.
|
||||||
|
*/
|
||||||
|
final class HubSpy implements HubInterface
|
||||||
|
{
|
||||||
|
public ?Update $captured = null;
|
||||||
|
|
||||||
|
public function __construct(private readonly string $stubReturnId = '')
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrl(): string
|
||||||
|
{
|
||||||
|
return 'http://localhost/.well-known/mercure';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPublicUrl(): string
|
||||||
|
{
|
||||||
|
return $this->getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProvider(): TokenProviderInterface
|
||||||
|
{
|
||||||
|
throw new \LogicException('HubSpy::getProvider not used in tests.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFactory(): ?TokenFactoryInterface
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function publish(Update $update): string
|
||||||
|
{
|
||||||
|
$this->captured = $update;
|
||||||
|
|
||||||
|
return $this->stubReturnId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace PhpQml\Bridge\Tests;
|
namespace PhpQml\Bridge\Tests;
|
||||||
|
|
||||||
use PhpQml\Bridge\Publisher;
|
use PhpQml\Bridge\Publisher;
|
||||||
|
use PhpQml\Bridge\Tests\Helper\HubSpy;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Mercure\HubInterface;
|
|
||||||
use Symfony\Component\Mercure\Update;
|
use Symfony\Component\Mercure\Update;
|
||||||
|
|
||||||
#[CoversClass(Publisher::class)]
|
#[CoversClass(Publisher::class)]
|
||||||
@@ -15,58 +15,30 @@ final class PublisherTest extends TestCase
|
|||||||
{
|
{
|
||||||
public function testPublishWritesEnvelopeAsJsonOnTheGivenTopic(): void
|
public function testPublishWritesEnvelopeAsJsonOnTheGivenTopic(): void
|
||||||
{
|
{
|
||||||
$captured = null;
|
$hub = new HubSpy('urn:uuid:test');
|
||||||
$hub = new class($captured) implements HubInterface {
|
|
||||||
public function __construct(private mixed &$captured) {}
|
|
||||||
|
|
||||||
public function getUrl(): string { return 'http://localhost/.well-known/mercure'; }
|
|
||||||
|
|
||||||
public function getPublicUrl(): string { return $this->getUrl(); }
|
|
||||||
|
|
||||||
public function getProvider(): \Symfony\Component\Mercure\Jwt\TokenProviderInterface
|
|
||||||
{
|
|
||||||
throw new \LogicException('not used in test');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFactory(): ?\Symfony\Component\Mercure\Jwt\TokenFactoryInterface
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function publish(Update $update): string
|
|
||||||
{
|
|
||||||
$this->captured = $update;
|
|
||||||
return 'urn:uuid:test';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$publisher = new Publisher($hub);
|
$publisher = new Publisher($hub);
|
||||||
$id = $publisher->publish('app://model/todo', ['op' => 'upsert', 'id' => '1', 'data' => ['done' => true], 'version' => 7]);
|
|
||||||
|
$id = $publisher->publish(
|
||||||
|
'app://model/todo',
|
||||||
|
['op' => 'upsert', 'id' => '1', 'data' => ['done' => true], 'version' => 7],
|
||||||
|
);
|
||||||
|
|
||||||
self::assertSame('urn:uuid:test', $id);
|
self::assertSame('urn:uuid:test', $id);
|
||||||
self::assertInstanceOf(Update::class, $captured);
|
self::assertInstanceOf(Update::class, $hub->captured);
|
||||||
self::assertSame(['app://model/todo'], $captured->getTopics());
|
self::assertSame(['app://model/todo'], $hub->captured->getTopics());
|
||||||
self::assertJsonStringEqualsJsonString(
|
self::assertJsonStringEqualsJsonString(
|
||||||
'{"op":"upsert","id":"1","data":{"done":true},"version":7}',
|
'{"op":"upsert","id":"1","data":{"done":true},"version":7}',
|
||||||
$captured->getData(),
|
$hub->captured->getData(),
|
||||||
);
|
);
|
||||||
self::assertFalse($captured->isPrivate());
|
self::assertFalse($hub->captured->isPrivate());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPrivateFlagIsForwarded(): void
|
public function testPrivateFlagIsForwarded(): void
|
||||||
{
|
{
|
||||||
$captured = null;
|
$hub = new HubSpy();
|
||||||
$hub = new class($captured) implements HubInterface {
|
|
||||||
public function __construct(private mixed &$captured) {}
|
|
||||||
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 { $this->captured = $update; return ''; }
|
|
||||||
};
|
|
||||||
|
|
||||||
(new Publisher($hub))->publish('app://event/internal', ['data' => 'x'], private: true);
|
(new Publisher($hub))->publish('app://event/internal', ['data' => 'x'], private: true);
|
||||||
|
|
||||||
self::assertTrue($captured->isPrivate());
|
self::assertNotNull($hub->captured);
|
||||||
|
self::assertTrue($hub->captured->isPrivate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ final class SessionAuthenticatorTest extends TestCase
|
|||||||
$auth = new SessionAuthenticator('s3cret');
|
$auth = new SessionAuthenticator('s3cret');
|
||||||
$response = $auth->onAuthenticationFailure(new Request(), new AuthenticationException('Bearer token invalid.'));
|
$response = $auth->onAuthenticationFailure(new Request(), new AuthenticationException('Bearer token invalid.'));
|
||||||
|
|
||||||
self::assertNotNull($response);
|
|
||||||
self::assertSame(Response::HTTP_UNAUTHORIZED, $response->getStatusCode());
|
self::assertSame(Response::HTTP_UNAUTHORIZED, $response->getStatusCode());
|
||||||
self::assertSame('application/problem+json', $response->headers->get('Content-Type'));
|
self::assertSame('application/problem+json', $response->headers->get('Content-Type'));
|
||||||
$body = json_decode((string) $response->getContent(), true);
|
$body = json_decode((string) $response->getContent(), true);
|
||||||
|
|||||||
@@ -37,6 +37,6 @@ clean: ## Remove build artefacts
|
|||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
|
||||||
.PHONY: quality
|
.PHONY: quality
|
||||||
quality: ## (TBD) Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint
|
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint
|
||||||
@echo "quality: not yet implemented (Phase 1 sub-commit 7)"
|
cd ../php && composer quality
|
||||||
@exit 1
|
cmake --build $(BUILD_DIR) --target all_qmllint
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ ApplicationWindow {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 12; height: 12; radius: 6
|
Layout.preferredWidth: 12; Layout.preferredHeight: 12; radius: 6
|
||||||
color: BackendConnection.connectionState === BackendConnection.Online
|
color: BackendConnection.connectionState === BackendConnection.Online
|
||||||
? "#3ab36c"
|
? "#3ab36c"
|
||||||
: (BackendConnection.connectionState === BackendConnection.Error ? "#d8503c" : "#d89614")
|
: (BackendConnection.connectionState === BackendConnection.Error ? "#d8503c" : "#d89614")
|
||||||
@@ -72,7 +72,7 @@ ApplicationWindow {
|
|||||||
Item { width: 12 }
|
Item { width: 12 }
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 12; height: 12; radius: 6
|
Layout.preferredWidth: 12; Layout.preferredHeight: 12; radius: 6
|
||||||
color: mercure.active ? "#3ab36c" : "#666"
|
color: mercure.active ? "#3ab36c" : "#666"
|
||||||
}
|
}
|
||||||
Label { text: "Mercure: " + (mercure.active ? "subscribed" : "off") }
|
Label { text: "Mercure: " + (mercure.active ? "subscribed" : "off") }
|
||||||
|
|||||||
Reference in New Issue
Block a user