Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DependencyInjection/BdfQueueMessengerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/
class BdfQueueMessengerExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('queue_messenger.yaml');
Expand Down
149 changes: 149 additions & 0 deletions Tests/Functional/BundleIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace Bdf\QueueMessengerBundle\Tests\Functional;

use Bdf\QueueMessengerBundle\Tests\Functional\Fixtures\TestKernel;
use Bdf\QueueMessengerBundle\Tests\Functional\Fixtures\TestMessage;
use Bdf\QueueMessengerBundle\Tests\Functional\Fixtures\TestMessageHandler;
use Bdf\QueueMessengerBundle\Transport\Handler\MessageBusHandler;
use Bdf\QueueMessengerBundle\Transport\QueueTransport;
use Bdf\QueueMessengerBundle\Transport\QueueTransportFactory;
use Bdf\QueueMessengerBundle\Transport\Stamp\BdfQueueReceivedStamp;
use Bdf\QueueMessengerBundle\Transport\Stamp\PhpStampsSerializer;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\SentStamp;
use Symfony\Component\Messenger\Worker;

/**
* Boots a real Symfony container with the bundle registered and checks that its
* services are wired and that the messenger transport works end to end against
* an in-memory queue.
*/
class BundleIntegrationTest extends KernelTestCase
{
protected static function getKernelClass(): string
{
return TestKernel::class;
}

public function testTransportFactoryIsRegistered()
{
self::bootKernel();

$factory = self::getContainer()->get('bdf_queue.messenger_transport.factory');

$this->assertInstanceOf(QueueTransportFactory::class, $factory);
}

public function testDefaultStampSerializerIsThePhpOne()
{
self::bootKernel();

$serializer = self::getContainer()->get('bdf_queue.messenger_transport.stamp_serializer');

$this->assertInstanceOf(PhpStampsSerializer::class, $serializer);
}

public function testMessageBusHandlerIsRegistered()
{
self::bootKernel();

$handler = self::getContainer()->get(MessageBusHandler::class);

$this->assertInstanceOf(MessageBusHandler::class, $handler);
}

public function testMessengerBuildsAQueueTransportForBdfQueueDsn()
{
self::bootKernel();

// The transport is built by symfony messenger using the tagged factory
// of the bundle : getting a QueueTransport proves the factory is
// properly tagged as "messenger.transport_factory".
$transport = self::getContainer()->get('messenger.transport.bdf');

$this->assertInstanceOf(QueueTransport::class, $transport);
}

public function testSendAndReceiveRoundTrip()
{
self::bootKernel();

/** @var QueueTransport $transport */
$transport = self::getContainer()->get('messenger.transport.bdf');

$message = new TestMessage('hello');
$transport->send(new Envelope($message));

$received = iterator_to_array($transport->get());

$this->assertCount(1, $received);

/** @var Envelope $envelope */
$envelope = $received[0];
$this->assertEquals($message, $envelope->getMessage());
$this->assertNotNull($envelope->last(BdfQueueReceivedStamp::class));

// Acknowledging must not throw and should leave the queue empty.
$transport->ack($envelope);

$this->assertSame([], iterator_to_array($transport->get()));
}

public function testDispatchThroughMessengerBusRoutesToTransportAndIsHandled()
{
self::bootKernel();

$container = self::getContainer();

/** @var MessageBusInterface $bus */
$bus = $container->get('messenger.default_bus');
/** @var TestMessageHandler $handler */
$handler = $container->get(TestMessageHandler::class);

$message = new TestMessage('via-bus');

// Dispatch through the bus : the routing sends the message to the bdf
// transport asynchronously, so it must NOT be handled in process yet.
$envelope = $bus->dispatch($message);

$this->assertNotNull($envelope->last(SentStamp::class), 'The message should have been sent to a transport');
$this->assertSame([], $handler->handled, 'An async message must not be handled during dispatch');

// Consume the transport with a real worker : it receives the message
// from the queue and dispatches it back to the bus for handling.
$worker = new Worker(
['bdf' => $container->get('messenger.transport.bdf')],
$bus,
$this->workerEventDispatcher()
);
$worker->run(['sleep' => 0]);

$this->assertCount(1, $handler->handled);
$this->assertEquals($message, $handler->handled[0]);
}

/**
* Event dispatcher stopping the worker as soon as one message is handled,
* with a wall-clock deadline as a safety net so the test can never hang.
*/
private function workerEventDispatcher(): EventDispatcher
{
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1));

$deadline = microtime(true) + 5;
$dispatcher->addListener(WorkerRunningEvent::class, function (WorkerRunningEvent $event) use ($deadline) {
if (microtime(true) >= $deadline) {
$event->getWorker()->stop();
}
});

return $dispatcher;
}
}
40 changes: 40 additions & 0 deletions Tests/Functional/Fixtures/DestinationManagerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Bdf\QueueMessengerBundle\Tests\Functional\Fixtures;

use Bdf\Queue\Connection\Factory\CachedConnectionDriverFactory;
use Bdf\Queue\Connection\Factory\ResolverConnectionDriverFactory;
use Bdf\Queue\Connection\Memory\MemoryConnection;
use Bdf\Queue\Destination\ConfigurationDestinationFactory;
use Bdf\Queue\Destination\DestinationManager;
use Bdf\Queue\Destination\DsnDestinationFactory;

/**
* Build a "real" destination manager backed by an in-memory connection.
*
* This service is normally provided by the bdf-queue framework bundle. It is
* redefined here so the messenger bundle can be booted in a real container
* without pulling the whole queue bundle, while still exercising the actual
* destination/consumer stack (no mock).
*/
final class DestinationManagerFactory
{
public static function create(): DestinationManager
{
$connectionFactory = new ResolverConnectionDriverFactory([
'memory-queue' => 'memory://localhost',
]);
$connectionFactory->addDriverResolver('memory', function () {
return new MemoryConnection();
});

// Cache the connections so a message sent by the transport and the
// consumer that reads it back share the same in-memory storage.
$connectionFactory = new CachedConnectionDriverFactory($connectionFactory);

return new DestinationManager(
$connectionFactory,
new ConfigurationDestinationFactory([], new DsnDestinationFactory($connectionFactory))
);
}
}
37 changes: 37 additions & 0 deletions Tests/Functional/Fixtures/PublicServicesPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Bdf\QueueMessengerBundle\Tests\Functional\Fixtures;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* Force some bundle services to stay public so they can be fetched from the
* test container and are not removed as "unused" in the minimal test kernel.
*/
final class PublicServicesPass implements CompilerPassInterface
{
/**
* @var string[]
*/
private $ids;

/**
* @param string[] $ids
*/
public function __construct(array $ids)
{
$this->ids = $ids;
}

public function process(ContainerBuilder $container): void
{
foreach ($this->ids as $id) {
if ($container->hasDefinition($id)) {
$container->getDefinition($id)->setPublic(true);
} elseif ($container->hasAlias($id)) {
$container->getAlias($id)->setPublic(true);
}
}
}
}
98 changes: 98 additions & 0 deletions Tests/Functional/Fixtures/TestKernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace Bdf\QueueMessengerBundle\Tests\Functional\Fixtures;

use Bdf\Queue\Destination\DestinationManager;
use Bdf\QueueMessengerBundle\BdfQueueMessengerBundle;
use Bdf\QueueMessengerBundle\Transport\Handler\MessageBusHandler;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;

/**
* Minimal kernel booting the framework bundle together with the messenger
* bundle under test.
*/
class TestKernel extends Kernel
{
public function registerBundles(): iterable
{
return [
new FrameworkBundle(),
new BdfQueueMessengerBundle(),
];
}

protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(
new PublicServicesPass([
MessageBusHandler::class,
'bdf_queue.messenger_bus_handler',
'messenger.default_bus',
]),
PassConfig::TYPE_BEFORE_REMOVING
);
}

public function registerContainerConfiguration(LoaderInterface $loader): void
{
$loader->load(function (ContainerBuilder $container) {
$config = [
'test' => true,
'secret' => 'test',
'http_method_override' => false,
'php_errors' => ['log' => true],
'messenger' => [
'transports' => [
'bdf' => 'bdfqueue://memory-queue',
],
'routing' => [
TestMessage::class => ['bdf'],
],
],
];

// Introduced in Symfony 6.4, unknown (and thus rejected) on 5.4.
if (Kernel::VERSION_ID >= 60400) {
$config['handle_all_throwables'] = true;
}

$container->loadFromExtension('framework', $config);

// Service usually provided by the bdf-queue framework bundle.
$container
->register('bdf_queue.destination_manager', DestinationManager::class)
->setFactory([DestinationManagerFactory::class, 'create'])
->setPublic(true);

// Handler used to assert the dispatch -> transport -> worker flow.
$container
->register(TestMessageHandler::class, TestMessageHandler::class)
->addTag('messenger.message_handler')
->setPublic(true);
});
}

public function getCacheDir(): string
{
return $this->buildDir().'/cache';
}

public function getBuildDir(): string
{
return $this->buildDir().'/build';
}

public function getLogDir(): string
{
return $this->buildDir().'/log';
}

private function buildDir(): string
{
return sys_get_temp_dir().'/bdf_queue_messenger_bundle_tests/'.$this->environment;
}
}
19 changes: 19 additions & 0 deletions Tests/Functional/Fixtures/TestMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Bdf\QueueMessengerBundle\Tests\Functional\Fixtures;

/**
* Simple serializable message used to check the transport round trip.
*/
final class TestMessage
{
/**
* @var string
*/
public $content;

public function __construct(string $content = '')
{
$this->content = $content;
}
}
20 changes: 20 additions & 0 deletions Tests/Functional/Fixtures/TestMessageHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Bdf\QueueMessengerBundle\Tests\Functional\Fixtures;

/**
* Records the messages it handles so the test can assert the full
* dispatch -> transport -> worker -> handler round trip.
*/
class TestMessageHandler
{
/**
* @var TestMessage[]
*/
public $handled = [];

public function __invoke(TestMessage $message): void
{
$this->handled[] = $message;
}
}
Loading