diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 310d97d..80a5f1f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,9 +2,9 @@ name: Tests on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] schedule: - cron: '0 0 * * *' @@ -26,11 +26,13 @@ jobs: - name: Install dependencies run: composer install - - name: Start server - run: php bin/drumkit --tls-cert=ssl/mercure-router.local.pem --tls-key=ssl/mercure-router.local-key.pem --dev --active-subscriptions & - - - name: Wait for server to be ready - run: sleep 2 - - name: Run PHPUnit tests run: vendor/bin/phpunit + + - name: Logs of the server by test + if: always() + uses: actions/upload-artifact@v4 + with: + name: server-logs + path: | + logs \ No newline at end of file diff --git a/.gitignore b/.gitignore index a56590d..c57abcf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vendor/ /ssl/mercure-router.local.pem .phpunit.result.cache composer.lock +logs \ No newline at end of file diff --git a/composer.json b/composer.json index 59cb1fe..51a8e0f 100644 --- a/composer.json +++ b/composer.json @@ -49,6 +49,7 @@ "amphp/http-client-cookies": "^2.0.0", "amphp/phpunit-util": "^3.0.0", "phpspec/prophecy-phpunit": "^2.2.0", - "dg/bypass-finals": "dev-master" + "dg/bypass-finals": "dev-master", + "symfony/process": "^7.1" } } diff --git a/lib/amphp/http-server-router/src/Router.php b/lib/amphp/http-server-router/src/Router.php index 5986a55..0ed6a8d 100644 --- a/lib/amphp/http-server-router/src/Router.php +++ b/lib/amphp/http-server-router/src/Router.php @@ -76,6 +76,8 @@ public function handleRequest(Request $request): Response $method = $request->getMethod(); $path = $request->getUri()->getPath(); + $this->logger->debug('[Router] Matching path: "' . $path . '" with method '.$method); + $toMatch = "{$method}\0{$path}"; if (null === $match = $this->cache->get($toMatch)) { diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 3aaefb1..f9413d0 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -33,6 +33,9 @@ class RunCommand extends Command private const OPTION_SECURITY_PUBLISHER_KEY = 'security-publisher-key'; private const OPTION_SECURITY_SUBSCRIBER_ALG = 'security-subscriber-algorithm'; private const OPTION_SECURITY_SUBSCRIBER_KEY = 'security-subscriber-key'; + private const OPTION_CORS_ORIGIN_KEY = 'corsOrigin'; + private const OPTION_PORT_HTTP = 'http-port'; + private const OPTION_PORT_HTTPS = 'https-port'; protected static $defaultDescription = 'Start Drumkit (run a Mercure server)'; public function __construct( @@ -124,12 +127,26 @@ function (CompletionInput $input): array { 'Run the server in dev mode (shows more explicit errors, logs & run with xdebug)' ) ->addOption( - 'corsOrigin', + self::OPTION_CORS_ORIGIN_KEY, null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_REQUIRED, 'Specified authorised cors domain', [] ) + ->addOption( + self::OPTION_PORT_HTTP, + null, + InputOption::VALUE_REQUIRED, + 'Port number for HTTP', + 80 + ) + ->addOption( + self::OPTION_PORT_HTTPS, + null, + InputOption::VALUE_REQUIRED, + 'Port number for HTTPS', + 443 + ) ; } @@ -158,13 +175,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $options = OptionsFactory::fromCommandOptions( $tlsCert, $tlsKey, - $input->getOption('corsOrigin'), + $input->getOption(self::OPTION_CORS_ORIGIN_KEY), $input->getOption(self::OPTION_SECURITY_SUBSCRIBER_KEY), $input->getOption(self::OPTION_SECURITY_SUBSCRIBER_ALG), $input->getOption(self::OPTION_SECURITY_PUBLISHER_KEY), $input->getOption(self::OPTION_SECURITY_PUBLISHER_ALG), $input->getOption(self::OPTION_FEATURE_SUBSCRIPTIONS), - devMode: $devMode + devMode: $devMode, + httpPort: (int) $input->getOption(self::OPTION_PORT_HTTP), + httpsPort: (int) $input->getOption(self::OPTION_PORT_HTTPS), ); } else { $output->writeln('You need to provide at least TLS certificates to run the server. Run command `drumkit --help` to learn more.'); diff --git a/src/Configuration/OptionsFactory.php b/src/Configuration/OptionsFactory.php index c3fb786..20bda34 100644 --- a/src/Configuration/OptionsFactory.php +++ b/src/Configuration/OptionsFactory.php @@ -56,7 +56,9 @@ public static function fromCommandOptions( ?string $pubKey, ?string $pubAlg, bool $activeSubscriptions, - bool $devMode + bool $devMode, + int $httpPort, + int $httpsPort, ): Options { $tlsKey = self::resolvePath($tlsKey); $tlsCert = self::resolvePath($tlsCert); @@ -85,6 +87,8 @@ public static function fromCommandOptions( $tlsCert, $tlsKey, new CorsConfiguration($corsOrigin), + tlsPort: $httpsPort, + unsecuredPort: $httpPort, activeSubscriptionEnabled: $activeSubscriptions, devMode: $devMode, subscriberSecurity: $subscriberSecurity, diff --git a/src/Controller/Subscription/GetSubscriptionController.php b/src/Controller/Subscription/GetSubscriptionController.php index 9816abb..5d5a870 100644 --- a/src/Controller/Subscription/GetSubscriptionController.php +++ b/src/Controller/Subscription/GetSubscriptionController.php @@ -16,6 +16,8 @@ use Amp\Http\Server\RequestHandler; use Amp\Http\Server\Response; use Amp\Http\Server\Router; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use SwagIndustries\MercureRouter\Controller\NotFoundController; use SwagIndustries\MercureRouter\Mercure\Hub; use SwagIndustries\MercureRouter\Security\Security; @@ -24,10 +26,15 @@ class GetSubscriptionController implements RequestHandler { use SubscriptionNormalizerTrait; use SubscriptionApiResponseTrait; - public function __construct(private Hub $mercure, private NotFoundController $notFound) {} + public function __construct( + private Hub $mercure, + private NotFoundController $notFound, + private LoggerInterface $logger = new NullLogger() + ) {} public function handleRequest(Request $request): Response { ['topic' => $topicQuery, 'subscriber' => $subscriberId] = $request->getAttribute(Router::class); + $this->logger->debug('[API] Get subscriptions for topic "' . $topicQuery . '" and subscriber "' . $subscriberId . '"'); /** @var array{subscribe: array|string|null, payload?: array} $jwtContent */ $jwtContent = $request->getAttribute(Security::ATTRIBUTE_JWT_PAYLOAD)['mercure'] ?? []; @@ -36,9 +43,7 @@ public function handleRequest(Request $request): Response $validPaths = [ Hub::MERCURE_PATH . '/subscriptions{/topic}{/subscriber}', ]; - dump($allowedTopics); - dump($validPaths); - dump(array_intersect($validPaths, $allowedTopics)); + if (empty(array_intersect($validPaths, $allowedTopics))) { return $this->forbiddenApiResponse(); } diff --git a/src/Controller/Subscription/GetSubscriptionsController.php b/src/Controller/Subscription/GetSubscriptionsController.php index e5d8ca4..9ce9ce4 100644 --- a/src/Controller/Subscription/GetSubscriptionsController.php +++ b/src/Controller/Subscription/GetSubscriptionsController.php @@ -15,6 +15,8 @@ use Amp\Http\Server\Request; use Amp\Http\Server\RequestHandler; use Amp\Http\Server\Response; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use SwagIndustries\MercureRouter\Controller\NotFoundController; use SwagIndustries\MercureRouter\Mercure\Hub; use SwagIndustries\MercureRouter\Security\Security; @@ -23,9 +25,14 @@ class GetSubscriptionsController implements RequestHandler { use SubscriptionNormalizerTrait; use SubscriptionApiResponseTrait; - public function __construct(private Hub $mercure, private NotFoundController $notFound) {} + public function __construct( + private Hub $mercure, + private NotFoundController $notFound, + private LoggerInterface $logger = new NullLogger() + ) {} public function handleRequest(Request $request): Response { + $this->logger->debug('[API] Get all subscriptions'); /** @var array{subscribe: array|string|null, payload?: array} $jwtContent */ $jwtContent = $request->getAttribute(Security::ATTRIBUTE_JWT_PAYLOAD)['mercure'] ?? []; $allowedTopics = (array) ($jwtContent['subscribe'] ?? []); diff --git a/src/Controller/Subscription/GetTopicSubscriptionsController.php b/src/Controller/Subscription/GetTopicSubscriptionsController.php index dfa5fd1..67c09ec 100644 --- a/src/Controller/Subscription/GetTopicSubscriptionsController.php +++ b/src/Controller/Subscription/GetTopicSubscriptionsController.php @@ -16,6 +16,8 @@ use Amp\Http\Server\RequestHandler; use Amp\Http\Server\Response; use Amp\Http\Server\Router; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use SwagIndustries\MercureRouter\Controller\NotFoundController; use SwagIndustries\MercureRouter\Mercure\Hub; use SwagIndustries\MercureRouter\Security\Security; @@ -24,10 +26,15 @@ class GetTopicSubscriptionsController implements RequestHandler { use SubscriptionNormalizerTrait; use SubscriptionApiResponseTrait; - public function __construct(private Hub $mercure, private NotFoundController $notFound) {} + public function __construct( + private Hub $mercure, + private NotFoundController $notFound, + private LoggerInterface $logger = new NullLogger() + ) {} public function handleRequest(Request $request): Response { ['topic' => $topicQuery] = $request->getAttribute(Router::class); + $this->logger->debug('[API] Get topic subscriptions for topic "' . $topicQuery . '"'); /** @var array{subscribe: array|string|null, payload?: array} $jwtContent */ $jwtContent = $request->getAttribute(Security::ATTRIBUTE_JWT_PAYLOAD)['mercure'] ?? []; diff --git a/tests/Functional/AbstractFunctionalTest.php b/tests/Functional/AbstractFunctionalTest.php new file mode 100644 index 0000000..67462ce --- /dev/null +++ b/tests/Functional/AbstractFunctionalTest.php @@ -0,0 +1,45 @@ +process = new Process( + [ + 'bin/drumkit', + '--tls-cert=ssl/ci.mercure-router.local.pem', + '--tls-key=ssl/ci.mercure-router.local-key.pem', + '--dev', + '--active-subscriptions', + '--http-port='.self::UNSECURED_PORT, + '--https-port='.self::TLS_PORT, + ] + ); + + $this->process->start(); + $this->process->waitUntil(function ($type, $buffer) { + return str_contains($buffer, 'Listening on'); + }); + } + + protected function tearDown(): void + { + $outputDir = __DIR__.'/../../logs'; + if (!is_dir($outputDir)) { + mkdir($outputDir); + } + + $file = $this->getName(); + file_put_contents($outputDir.'/'.$file.'.out',$this->process->getOutput()); + file_put_contents($outputDir.'/'.$file.'.err',$this->process->getErrorOutput()); + $this->process->stop(); + } +} diff --git a/tests/Functional/RecoveryTest.php b/tests/Functional/RecoveryTest.php index f728ba6..44bfc68 100644 --- a/tests/Functional/RecoveryTest.php +++ b/tests/Functional/RecoveryTest.php @@ -1,15 +1,25 @@ + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace SwagIndustries\MercureRouter\Test\Functional; use PHPUnit\Framework\TestCase; +use SwagIndustries\MercureRouter\Test\Functional\AbstractFunctionalTest; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestClient; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestSubscriber; use function Amp\async; use function Amp\Future\await; -class RecoveryTest extends TestCase +class RecoveryTest extends AbstractFunctionalTest { public function testItCanRecoverySomeMessagesBefore(): void { diff --git a/tests/Functional/SendUpdateTest.php b/tests/Functional/SendUpdateTest.php index 61b1d6e..23e5f8e 100644 --- a/tests/Functional/SendUpdateTest.php +++ b/tests/Functional/SendUpdateTest.php @@ -12,12 +12,11 @@ namespace SwagIndustries\MercureRouter\Test\Functional; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestClient; -use PHPUnit\Framework\TestCase; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestSubscriber; use function Amp\async; use function Amp\Future\await; -class SendUpdateTest extends TestCase +class SendUpdateTest extends AbstractFunctionalTest { public function testSendUpdate(): void { diff --git a/tests/Functional/SendUpdateUsingSymfonyClientTest.php b/tests/Functional/SendUpdateUsingSymfonyClientTest.php index d5bda05..556434f 100644 --- a/tests/Functional/SendUpdateUsingSymfonyClientTest.php +++ b/tests/Functional/SendUpdateUsingSymfonyClientTest.php @@ -1,8 +1,16 @@ + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + namespace SwagIndustries\MercureRouter\Test\Functional; -use PHPUnit\Framework\TestCase; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestClient; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestSubscriber; use Symfony\Component\HttpClient\HttpClient; @@ -14,7 +22,7 @@ use function Amp\delay; use function Amp\Future\await; -class SendUpdateUsingSymfonyClientTest extends TestCase +class SendUpdateUsingSymfonyClientTest extends AbstractFunctionalTest { public function testSendUpdateUsingSymfonyHttpClient(): void { @@ -27,7 +35,7 @@ public function testSendUpdateUsingSymfonyHttpClient(): void $httpClient = HttpClient::create(['verify_peer' => false, 'verify_host'=> false]); $token = (new LcobucciFactory(TestClient::PASSPHRASE_JWT))->create(); $hub = new Hub( - 'https://127.0.0.1/.well-known/mercure', + 'https://127.0.0.1:'.self::TLS_PORT.'/.well-known/mercure', jwtProvider: new StaticTokenProvider($token), httpClient: $httpClient, ); diff --git a/tests/Functional/SubscriptionsApiTest.php b/tests/Functional/SubscriptionsApiTest.php index 756d3e0..88bce97 100644 --- a/tests/Functional/SubscriptionsApiTest.php +++ b/tests/Functional/SubscriptionsApiTest.php @@ -1,9 +1,17 @@ + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace SwagIndustries\MercureRouter\Test\Functional; use Amp\Http\Client\Response; -use PHPUnit\Framework\TestCase; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestClient; use SwagIndustries\MercureRouter\Test\Functional\Tool\TestSubscriber; use Symfony\Component\Mercure\Jwt\LcobucciFactory; @@ -11,7 +19,7 @@ use function Amp\delay; use function Amp\Future\await; -class SubscriptionsApiTest extends TestCase +class SubscriptionsApiTest extends AbstractFunctionalTest { public const PASSPHRASE_JWT = '!ChangeThisMercureHubJWTSecretKey!'; public function testSubscriptionsList(): void @@ -103,6 +111,7 @@ public function testGetASpecificSubscription(): void [,[$response, $content]] = await([ $subscriber1->subscribe(), async(function () use ($client, $subscriber1) { + delay(1); $topic = urlencode('https://example.com/my-topic'); // Let some time pass for the subscription to be established $res = $client->get( @@ -136,6 +145,7 @@ function (string $content) { ]); $content = json_decode($content, true, flags: JSON_THROW_ON_ERROR); + $this->assertEquals($response->getRequest()->getUri()->getPath(), $content['id']); $this->assertTrue($content['active']); $this->assertEquals('https://example.com/my-topic', $content['topic']); diff --git a/tests/Functional/Tool/TestClient.php b/tests/Functional/Tool/TestClient.php index 1dd0fd2..fc153de 100644 --- a/tests/Functional/Tool/TestClient.php +++ b/tests/Functional/Tool/TestClient.php @@ -22,6 +22,7 @@ use Amp\Socket\ClientTlsContext; use Amp\Socket\ConnectContext; use Nekland\Tools\StringTools; +use SwagIndustries\MercureRouter\Test\Functional\AbstractFunctionalTest; use Symfony\Component\Mercure\Jwt\LcobucciFactory; use function Amp\async; use function Amp\delay; @@ -65,7 +66,7 @@ public function sendUpdate(array $data, bool $isPrivate = false, string $token = $token = (new LcobucciFactory(self::PASSPHRASE_JWT))->create(); } - $request = new Request('https://127.0.0.1/.well-known/mercure', 'POST', $body); + $request = new Request('https://127.0.0.1:'.AbstractFunctionalTest::TLS_PORT.'/.well-known/mercure', 'POST', $body); $request->addHeader('Authorization', 'Bearer '.$token); $response = $this->client->request($request); @@ -85,8 +86,7 @@ public function get(string $url, callable $expectation, ?string $token = null): $url = StringTools::removeStart($url, '/.well-known/mercure'); } - - $request = new Request('https://127.0.0.1/.well-known/mercure'. $url, 'GET'); + $request = new Request('https://127.0.0.1:'.AbstractFunctionalTest::TLS_PORT.'/.well-known/mercure'. $url, 'GET'); if ($token) { $request->addHeader('Authorization', 'Bearer '.$token); } diff --git a/tests/Functional/Tool/TestSubscriber.php b/tests/Functional/Tool/TestSubscriber.php index f2ca235..636c512 100644 --- a/tests/Functional/Tool/TestSubscriber.php +++ b/tests/Functional/Tool/TestSubscriber.php @@ -23,6 +23,7 @@ use Amp\Socket\ClientTlsContext; use Amp\Socket\ConnectContext; use SwagIndustries\MercureRouter\Controller\SubscribeController; +use SwagIndustries\MercureRouter\Test\Functional\AbstractFunctionalTest; use Symfony\Component\Mercure\Jwt\LcobucciFactory; use function Amp\async; use function Amp\delay; @@ -62,7 +63,7 @@ public function subscribe(string $token = null, string $lastEventId = null): Fut } return async(function () use($token, $lastEventId) { - $request = new Request('https://127.0.0.1/.well-known/mercure?topic='.urlencode($this->topic), 'GET'); + $request = new Request('https://127.0.0.1:'.AbstractFunctionalTest::TLS_PORT.'/.well-known/mercure?topic='.urlencode($this->topic), 'GET'); $request->addHeader('Authorization', 'Bearer '.$token); $request->setInactivityTimeout($this->timeout); $request->setTransferTimeout($this->timeout);