diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index c1c41c3..a01d989 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -26,6 +26,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php }}" + extensions: swoole coverage: pcov ini-values: assert.exception=1, zend.assertions=1, error_reporting=-1, log_errors_max_len=0, display_errors=On tools: composer:v2, cs2pr diff --git a/.laminas-ci.json b/.laminas-ci.json index 64be8c2..af5962f 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -1,8 +1,10 @@ { "additional_composer_arguments": [ - "--no-scripts", - "--no-plugins" + "--no-scripts" + ], + "extensions": [ + "redis" ], "ignore_php_platform_requirements": { } -} \ No newline at end of file +} diff --git a/.laminas-ci/pre-run.sh b/.laminas-ci/pre-run.sh index 8b8528d..b7ba4af 100755 --- a/.laminas-ci/pre-run.sh +++ b/.laminas-ci/pre-run.sh @@ -1,5 +1,5 @@ -#!/bin/bash +JOB=$3 +PHP_VERSION=$(echo "${JOB}" | jq -r '.php') -# Due to the fact that we are disabling plugins when installing/updating/downgrading composer dependencies -# we have to manually enable the coding standard here. -composer enable-codestandard +apt update +apt install -y "php${PHP_VERSION}-swoole" diff --git a/composer.json b/composer.json index f532977..bb71273 100644 --- a/composer.json +++ b/composer.json @@ -84,7 +84,8 @@ }, "autoload-dev": { "psr-4": { - "DotTest\\Mail\\": "test/" + "QueueTest\\App\\": "test/App/", + "QueueTest\\Swoole\\": "test/Swoole/" } }, "scripts": { diff --git a/config/autoload/log.local.php.dist b/config/autoload/log.local.php.dist index 1422cc8..913af79 100644 --- a/config/autoload/log.local.php.dist +++ b/config/autoload/log.local.php.dist @@ -10,7 +10,7 @@ return [ 'writers' => [ 'FileWriter' => [ 'name' => 'stream', - 'level' => \Dot\Log\Logger::ALERT, // this is equal to 1 + 'priority' => \Dot\Log\Logger::ALERT, // this is equal to 1 'options' => [ 'stream' => __DIR__ . '/../../log/queue-log.log', 'formatter' => [ diff --git a/config/autoload/mail.global.php b/config/autoload/mail.global.php new file mode 100644 index 0000000..ad0711d --- /dev/null +++ b/config/autoload/mail.global.php @@ -0,0 +1,86 @@ + [ + //the key is the mail service name, this is the default one, which does not extend any configuration + 'default' => [ + //message configuration + 'message_options' => [ + //from email address of the email + 'from' => '', + //from name to be displayed instead of from address + 'from_name' => '', + //reply-to email address of the email + 'reply_to' => '', + //replyTo name to be displayed instead of the address + 'reply_to_name' => '', + //destination email address as string or a list of email addresses + 'to' => [], + //copy destination addresses + 'cc' => [], + //hidden copy destination addresses + 'bcc' => [], + //email subject + 'subject' => '', + //body options - content can be plain text, HTML + 'body' => [ + 'content' => '', + 'charset' => 'utf-8', + ], + //attachments config + 'attachments' => [ + 'files' => [], + 'dir' => [ + 'iterate' => false, + 'path' => 'data/mail/attachments', + 'recursive' => false, + ], + ], + ], + /** + * the mail transport to use can be any class implementing + * Symfony\Component\Mailer\Transport\TransportInterface + * + * for standard mail transports, you can use these aliases: + * - sendmail => Symfony\Component\Mailer\Transport\SendmailTransport + * - esmtp => Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport + * + * defaults to sendmail + **/ + 'transport' => 'sendmail', + //options that will be used only if esmtp adapter is used + 'smtp_options' => [ + //hostname or IP address of the mail server + 'host' => '', + //port of the mail server - 587 or 465 for secure connections + 'port' => 587, + 'connection_config' => [ + //the smtp authentication identity + 'username' => '', + //the smtp authentication credential + 'password' => '', + /** + * tls will run by default on this component, use: + * + * null - to avoid interfering with automatic encryption + * false - to disable automatic encryption + * + * It's not recommended to disable TLS while connecting to an SMTP server over the Internet + **/ + 'tls' => null, + ], + ], + ], + // option to log the SENT emails + 'log' => [ + 'sent' => getcwd() . '/log/mail/sent.log', + ], + ], +]; diff --git a/src/App/ConfigProvider.php b/src/App/ConfigProvider.php index 9d6963d..882f20f 100644 --- a/src/App/ConfigProvider.php +++ b/src/App/ConfigProvider.php @@ -10,8 +10,8 @@ use Netglue\PsrContainer\Messenger\Container\Middleware\MessageHandlerMiddlewareStaticFactory; use Netglue\PsrContainer\Messenger\Container\Middleware\MessageSenderMiddlewareStaticFactory; use Netglue\PsrContainer\Messenger\HandlerLocator\OneToManyFqcnContainerHandlerLocator; -use Queue\App\Message\ExampleMessage; -use Queue\App\Message\ExampleMessageHandler; +use Queue\App\Message\Message; +use Queue\App\Message\MessageHandler; use Symfony\Component\Messenger\MessageBusInterface; class ConfigProvider @@ -37,7 +37,7 @@ private function getDependencies(): array "message_bus_stamp_middleware" => [BusNameStampMiddlewareStaticFactory::class, "message_bus"], "message_bus_sender_middleware" => [MessageSenderMiddlewareStaticFactory::class, "message_bus"], "message_bus_handler_middleware" => [MessageHandlerMiddlewareStaticFactory::class, "message_bus"], - ExampleMessageHandler::class => AttributedServiceFactory::class, + MessageHandler::class => AttributedServiceFactory::class, ], "aliases" => [ MessageBusInterface::class => "message_bus", @@ -81,7 +81,7 @@ private function busConfig(): array */ 'handler_locator' => OneToManyFqcnContainerHandlerLocator::class, 'handlers' => [ - ExampleMessage::class => [ExampleMessageHandler::class], + Message::class => [MessageHandler::class], ], /** @@ -97,7 +97,7 @@ private function busConfig(): array * Route specific messages to specific transports by using the message name as the key. */ 'routes' => [ - ExampleMessage::class => ["redis_transport"], + Message::class => ["redis_transport"], ], ], ]; diff --git a/src/App/Message/ExampleMessage.php b/src/App/Message/Message.php similarity index 91% rename from src/App/Message/ExampleMessage.php rename to src/App/Message/Message.php index d094f4d..aeee893 100644 --- a/src/App/Message/ExampleMessage.php +++ b/src/App/Message/Message.php @@ -4,7 +4,7 @@ namespace Queue\App\Message; -class ExampleMessage +class Message { public function __construct( private array $payload, diff --git a/src/App/Message/ExampleMessageHandler.php b/src/App/Message/MessageHandler.php similarity index 85% rename from src/App/Message/ExampleMessageHandler.php rename to src/App/Message/MessageHandler.php index 237a641..f96f8d9 100644 --- a/src/App/Message/ExampleMessageHandler.php +++ b/src/App/Message/MessageHandler.php @@ -15,7 +15,7 @@ use function json_decode; -class ExampleMessageHandler +class MessageHandler { protected array $args = []; @@ -35,18 +35,19 @@ public function __construct( ) { } - public function __invoke(ExampleMessage $message): void + public function __invoke(Message $message): void { $payload = json_decode($message->getPayload()['foo'], true); if ($payload !== null && isset($payload['userUuid'])) { $this->logger->info("message: " . $payload['userUuid']); $this->args = $payload; - } - try { - $this->perform(); - } catch (Exception $exception) { + try { + $this->perform(); + } catch (Exception $exception) { + $this->logger->err("message: " . $exception->getMessage()); + } } } @@ -64,7 +65,7 @@ public function perform(): void public function sendWelcomeMail(): bool { $user = $this->userRepository->find($this->args['userUuid']); - $this->mailService->getMessage()->addTo('sergiubota@rospace.com', 'sergiu'); + $this->mailService->getMessage()->addTo($user->getEmail(), $user->getName()); $this->mailService->setSubject('Welcome to ' . $this->config['application']['name']); $body = $this->templateRenderer->render('notification-email::welcome', [ 'user' => $user, diff --git a/src/Swoole/Delegators/TCPServerDelegator.php b/src/Swoole/Delegators/TCPServerDelegator.php index a5e6a48..4302f43 100644 --- a/src/Swoole/Delegators/TCPServerDelegator.php +++ b/src/Swoole/Delegators/TCPServerDelegator.php @@ -5,7 +5,7 @@ namespace Queue\Swoole\Delegators; use Psr\Container\ContainerInterface; -use Queue\App\Message\ExampleMessage; +use Queue\App\Message\Message; use Swoole\Server as TCPSwooleServer; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\DelayStamp; @@ -28,8 +28,8 @@ public function __invoke(ContainerInterface $container, string $serviceName, cal // Register the function for the event `receive` $server->on('receive', function ($server, $fd, $fromId, $data) use ($logger, $bus) { - $bus->dispatch(new ExampleMessage(["foo" => $data])); - $bus->dispatch(new ExampleMessage(["foo" => "with 5 seconds delay"]), [ + $bus->dispatch(new Message(["foo" => $data])); + $bus->dispatch(new Message(["foo" => "with 5 seconds delay"]), [ new DelayStamp(5000), ]); diff --git a/test/App/AppConfigProviderTest.php b/test/App/AppConfigProviderTest.php new file mode 100644 index 0000000..62590b6 --- /dev/null +++ b/test/App/AppConfigProviderTest.php @@ -0,0 +1,23 @@ +config = (new ConfigProvider())(); + } + + public function testHasDependencies(): void + { + $this->assertArrayHasKey('dependencies', $this->config); + } +} diff --git a/test/App/Message/ExampleMessageHandlerTest.php b/test/App/Message/ExampleMessageHandlerTest.php new file mode 100644 index 0000000..f956ab9 --- /dev/null +++ b/test/App/Message/ExampleMessageHandlerTest.php @@ -0,0 +1,200 @@ +mailService = $this->createMock(MailService::class); + $this->renderer = $this->createMock(TemplateRendererInterface::class); + $this->userRepository = $this->createMock(UserRepository::class); + $this->logger = new Logger([ + 'writers' => [ + 'FileWriter' => [ + 'name' => 'null', + 'priority' => Logger::ALERT, + ], + ], + ]); + $this->config = [ + 'notification' => [ + 'server' => [ + 'protocol' => 'tcp', + 'host' => 'localhost', + 'port' => '8556', + 'eof' => "\n", + ], + ], + 'application' => [ + 'name' => 'dotkernel', + ], + ]; + + $this->handler = new MessageHandler( + $this->mailService, + $this->renderer, + $this->userRepository, + $this->logger, + $this->config + ); + } + + /** + * @throws \PHPUnit\Framework\MockObject\Exception + */ + public function testInvokeHandlesExceptionThrownByPerform(): void + { + $uuid = '1234'; + + $message = $this->createMock(Message::class); + $message->method('getPayload')->willReturn([ + 'foo' => json_encode(['userUuid' => $uuid]), + ]); + + $handlerMock = $this->getMockBuilder(MessageHandler::class) + ->setConstructorArgs([ + $this->mailService, + $this->renderer, + $this->userRepository, + $this->logger, + $this->config, + ]) + ->onlyMethods(['perform']) + ->getMock(); + + $handlerMock + ->expects($this->once()) + ->method('perform') + ->willThrowException(new \RuntimeException('Test exception')); + + $handlerMock->__invoke($message); + } + + /** + * @throws \PHPUnit\Framework\MockObject\Exception + */ + public function testInvokeWithValidPayload(): void + { + $uuid = '1245'; + $email = 'test@dotkernel.com'; + $name = 'dotkernel'; + + $message = $this->createMock(Message::class); + $message->method('getPayload')->willReturn([ + 'foo' => json_encode(['userUuid' => $uuid]), + ]); + + $user = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getEmail', 'getName']) + ->getMock(); + $user->method('getEmail')->willReturn($email); + $user->method('getName')->willReturn($name); + + $this->userRepository + ->expects($this->once()) + ->method('find') + ->with($uuid) + ->willReturn($user); + + $mailMessage = $this->createMock(Email::class); + $mailMessage->expects($this->once()) + ->method('addTo') + ->with($email, $name); + + $this + ->mailService + ->method('getMessage') + ->willReturn($mailMessage); + + $this + ->mailService + ->expects($this->once()) + ->method('setSubject') + ->with('Welcome to dotkernel'); + + $this->renderer->method('render')->willReturn('Rendered email body'); + + $this->mailService->expects($this->once()) + ->method('setBody') + ->with('Rendered email body'); + + $this->handler->__invoke($message); + } + + /** + * @throws MailException + * @throws \PHPUnit\Framework\MockObject\Exception + */ + public function testSendWelcomeMailHandlesMailException(): void + { + $uuid = '1234'; + + $reflection = new \ReflectionClass($this->handler); + $argsProperty = $reflection->getProperty('args'); + $argsProperty->setValue($this->handler, ['userUuid' => $uuid]); + + $user = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getEmail', 'getName']) + ->getMock(); + $user->method('getEmail')->willReturn('test@dotkernel.com'); + $user->method('getName')->willReturn('dotkernel'); + + $this->userRepository->method('find')->willReturn($user); + $this->mailService->method('getMessage')->willReturn($this->createMock(Email::class)); + $this->mailService->method('setSubject'); + $this->renderer->method('render')->willReturn('Rendered content'); + $this->mailService->method('setBody'); + + $this->mailService->method('send') + ->willThrowException($this->createMock(TransportExceptionInterface::class)); + + $result = $this->handler->sendWelcomeMail(); + $this->assertFalse($result); + } + + /** + * @throws \PHPUnit\Framework\MockObject\Exception + */ + public function testInvokeWithInvalidJsonSkipsPerform(): void + { + $message = $this->createMock(Message::class); + $message->method('getPayload')->willReturn([ + 'foo' => '{"userUuid":', + ]); + + $this->userRepository->expects($this->never())->method('find'); + $this->mailService->expects($this->never())->method('send'); + + $this->handler->__invoke($message); + } +} diff --git a/test/App/Message/ExampleMessageTest.php b/test/App/Message/ExampleMessageTest.php new file mode 100644 index 0000000..15cbb36 --- /dev/null +++ b/test/App/Message/ExampleMessageTest.php @@ -0,0 +1,17 @@ + "test message payload"]); + $this->assertSame(["payload" => "test message payload"], $admin->getPayload()); + } +} diff --git a/test/Swoole/Command/Factory/StartCommandFactoryTest.php b/test/Swoole/Command/Factory/StartCommandFactoryTest.php new file mode 100644 index 0000000..40e63a0 --- /dev/null +++ b/test/Swoole/Command/Factory/StartCommandFactoryTest.php @@ -0,0 +1,27 @@ +createMock(ContainerInterface::class); + + $factory = new StartCommandFactory(); + $command = $factory($container); + + $this->assertContainsOnlyInstancesOf(StartCommand::class, [$command]); + } +} diff --git a/test/Swoole/Command/Factory/StopCommandFactoryTest.php b/test/Swoole/Command/Factory/StopCommandFactoryTest.php new file mode 100644 index 0000000..a2ca055 --- /dev/null +++ b/test/Swoole/Command/Factory/StopCommandFactoryTest.php @@ -0,0 +1,35 @@ +createMock(PidManager::class); + + $container = $this->createMock(ContainerInterface::class); + $container->expects($this->once()) + ->method('get') + ->with(PidManager::class) + ->willReturn($pidManager); + + $factory = new StopCommandFactory(); + + $command = $factory($container); + + $this->assertContainsOnlyInstancesOf(StopCommand::class, [$command]); + } +} diff --git a/test/Swoole/Command/IsRunningTraitTest.php b/test/Swoole/Command/IsRunningTraitTest.php new file mode 100644 index 0000000..ef59e0d --- /dev/null +++ b/test/Swoole/Command/IsRunningTraitTest.php @@ -0,0 +1,41 @@ +traitUser = new class { + use IsRunningTrait; + + public PidManager $pidManager; + }; + + $this->traitUser->pidManager = $this->createMock(PidManager::class); + } + + public function testIsRunningReturnsFalseWhenNoPids(): void + { + $this->traitUser->pidManager->method('read')->willReturn([]); + $this->assertFalse($this->traitUser->isRunning()); + } + + public function testIsRunningReturnsFalseWhenPidsAreZero(): void + { + $this->traitUser->pidManager->method('read')->willReturn([0, 0]); + $this->assertFalse($this->traitUser->isRunning()); + } +} diff --git a/test/Swoole/Command/StartCommandTest.php b/test/Swoole/Command/StartCommandTest.php new file mode 100644 index 0000000..246976b --- /dev/null +++ b/test/Swoole/Command/StartCommandTest.php @@ -0,0 +1,92 @@ +createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $pidManager = $this->createMock(PidManager::class); + $server = $this->createMock(Server::class); + + $pidManager->method('read')->willReturn([]); + + $server->master_pid = 1234; + $server->manager_pid = 4321; + + $server->expects($this->once())->method('on'); + $server->expects($this->once())->method('start'); + + $config = [ + 'dotkernel-queue-swoole' => [ + 'swoole-server' => [ + 'process-name' => 'test-process', + ], + ], + ]; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->willReturnCallback(function (string $id) use ($pidManager, $server, $config) { + return match ($id) { + PidManager::class => $pidManager, + Server::class => $server, + 'config' => $config, + default => null, + }; + }); + + $command = new StartCommand($container); + $statusCode = $command->run($input, $output); + + $this->assertSame(0, $statusCode); + } + + /** + * @throws ExceptionInterface + * @throws Exception + */ + public function testExecuteWhenServerIsAlreadyRunning(): void + { + $container = $this->createMock(ContainerInterface::class); + $pidManager = $this->createMock(PidManager::class); + $container->method('get') + ->with(PidManager::class) + ->willReturn($pidManager); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $output->expects($this->once()) + ->method('writeln') + ->with('Server is already running!'); + + $command = $this->getMockBuilder(StartCommand::class) + ->setConstructorArgs([$container]) + ->onlyMethods(['isRunning']) + ->getMock(); + + $command->method('isRunning')->willReturn(true); + + $exitCode = $command->run($input, $output); + + $this->assertSame(1, $exitCode); + } +} diff --git a/test/Swoole/Command/StopCommandTest.php b/test/Swoole/Command/StopCommandTest.php new file mode 100644 index 0000000..dbdcca2 --- /dev/null +++ b/test/Swoole/Command/StopCommandTest.php @@ -0,0 +1,90 @@ +createMock(PidManager::class); + + $command = $this->getMockBuilder(StopCommand::class) + ->setConstructorArgs([$pidManager]) + ->onlyMethods(['isRunning']) + ->getMock(); + + $command->method('isRunning')->willReturn(false); + + $tester = new CommandTester($command); + $exitCode = $tester->execute([]); + + $this->assertSame(0, $exitCode); + $this->assertStringContainsString('Server is not running', $tester->getDisplay()); + } + + /** + * @throws Exception + */ + public function testExecuteWhenServerStopsSuccessfully(): void + { + $pidManager = $this->createMock(PidManager::class); + $pidManager->method('read')->willReturn(['1234']); + $pidManager->expects($this->once())->method('delete'); + + $command = $this->getMockBuilder(StopCommand::class) + ->setConstructorArgs([$pidManager]) + ->onlyMethods(['isRunning']) + ->getMock(); + + $command->method('isRunning')->willReturn(true); + + $command->killProcess = function (int $pid, ?int $signal = null): bool { + return true; + }; + + $tester = new CommandTester($command); + $exitCode = $tester->execute([]); + + $this->assertSame(0, $exitCode); + $this->assertStringContainsString('Server stopped', $tester->getDisplay()); + } + + /** + * @throws Exception + */ + public function testExecuteWhenServerFailsToStop(): void + { + $pidManager = $this->createMock(PidManager::class); + $pidManager->method('read')->willReturn(['1234']); + $pidManager->expects($this->never())->method('delete'); + + $command = $this->getMockBuilder(StopCommand::class) + ->setConstructorArgs([$pidManager]) + ->onlyMethods(['isRunning']) + ->getMock(); + + $command->method('isRunning')->willReturn(true); + $command->waitThreshold = 1; + + $command->killProcess = function (int $pid, ?int $signal = null): bool { + return $signal === 0; + }; + + $tester = new CommandTester($command); + $exitCode = $tester->execute([]); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Error stopping server', $tester->getDisplay()); + } +} diff --git a/test/Swoole/Delegators/DummySwooleServer.php b/test/Swoole/Delegators/DummySwooleServer.php new file mode 100644 index 0000000..248e6af --- /dev/null +++ b/test/Swoole/Delegators/DummySwooleServer.php @@ -0,0 +1,38 @@ + */ + public array $callbacks = []; + + public function __construct() + { + parent::__construct('127.0.0.1', 0); + } + + /** + * @param string $eventName + * @param callable $callback + */ + public function on($eventName, $callback): bool + { + $this->callbacks[$eventName] = $callback; + return true; + } + + /** + * @param int|string $fd + * @param string $data + * @param int $serverSocket + */ + public function send($fd, $data, $serverSocket = -1): bool + { + return true; + } +} diff --git a/test/Swoole/Delegators/TCPServerDelegatorTest.php b/test/Swoole/Delegators/TCPServerDelegatorTest.php new file mode 100644 index 0000000..6ff7953 --- /dev/null +++ b/test/Swoole/Delegators/TCPServerDelegatorTest.php @@ -0,0 +1,93 @@ +createMock(LoggerInterface::class); + $bus = $this->createMock(MessageBusInterface::class); + + $server = new DummySwooleServer(); + $callback = fn (): Server => $server; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->willReturnMap([ + [MessageBusInterface::class, $bus], + ['dot-log.queue-log', $logger], + ]); + + $delegator = new TCPServerDelegator(); + $result = $delegator($container, 'tcp-server', $callback); + + $this->assertContainsOnlyInstancesOf(Server::class, [$result]); + $this->assertArrayHasKey('Connect', $server->callbacks); + $this->assertArrayHasKey('receive', $server->callbacks); + $this->assertArrayHasKey('Close', $server->callbacks); + + foreach (['Connect', 'receive', 'Close'] as $event) { + $this->assertIsCallable($server->callbacks[$event]); + } + } + + /** + * @throws Exception + */ + #[RunInSeparateProcess] + public function testReceiveCallbackDispatchesMessagesAndLogs(): void + { + $dispatched = []; + + $bus = $this->createMock(MessageBusInterface::class); + $bus->method('dispatch')->willReturnCallback(function ($message) use (&$dispatched) { + $dispatched[] = $message; + return new Envelope($message); + }); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once()) + ->method('notice') + ->with( + $this->equalTo("Request received on receive"), + $this->arrayHasKey('fd') + ); + + $server = new DummySwooleServer(); + $callback = fn (): Server => $server; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->willReturnMap([ + [MessageBusInterface::class, $bus], + ['dot-log.queue-log', $logger], + ]); + + $delegator = new TCPServerDelegator(); + $result = $delegator($container, 'tcp-server', $callback); + + $this->assertContainsOnlyInstancesOf(Server::class, [$result]); + + $receive = $server->callbacks['receive'] ?? null; + $this->assertIsCallable($receive); + + $receive($server, 1, 1, 'hello'); + + $this->assertCount(2, $dispatched); + } +} diff --git a/test/Swoole/Exception/InvalidStaticResourceMiddlewareExceptionTest.php b/test/Swoole/Exception/InvalidStaticResourceMiddlewareExceptionTest.php new file mode 100644 index 0000000..461edda --- /dev/null +++ b/test/Swoole/Exception/InvalidStaticResourceMiddlewareExceptionTest.php @@ -0,0 +1,32 @@ +assertContainsOnlyInstancesOf(InvalidStaticResourceMiddlewareException::class, [$exception]); + + $expectedMessage = sprintf( + 'Static resource middleware must be callable; received middleware of type "%s" in position %s', + get_debug_type($middleware), + $position + ); + + $this->assertSame($expectedMessage, $exception->getMessage()); + } +} diff --git a/test/Swoole/PidManagerFactoryTest.php b/test/Swoole/PidManagerFactoryTest.php new file mode 100644 index 0000000..b5e52db --- /dev/null +++ b/test/Swoole/PidManagerFactoryTest.php @@ -0,0 +1,54 @@ + [ + 'swoole-tcp-server' => [ + 'options' => [ + 'pid_file' => $expectedPath, + ], + ], + ], + ]; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get') + ->with('config') + ->willReturn($config); + + $factory = new PidManagerFactory(); + $pidManager = $factory($container); + + $pidFilePath = $this->getPrivateProperty($pidManager); + $this->assertSame($expectedPath, $pidFilePath); + } + + /** + * @throws ReflectionException + */ + private function getPrivateProperty(object $object): mixed + { + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty('pidFile'); + return $property->getValue($object); + } +} diff --git a/test/Swoole/PidManagerTest.php b/test/Swoole/PidManagerTest.php new file mode 100644 index 0000000..f6c22e4 --- /dev/null +++ b/test/Swoole/PidManagerTest.php @@ -0,0 +1,92 @@ +tempPidFile = sys_get_temp_dir() . '/test.pid'; + if (file_exists($this->tempPidFile)) { + unlink($this->tempPidFile); + } + } + + protected function tearDown(): void + { + if (file_exists($this->tempPidFile)) { + unlink($this->tempPidFile); + } + } + + public function testWriteAndReadPids(): void + { + $manager = new PidManager($this->tempPidFile); + + $manager->write(12345, 67890); + + $result = $manager->read(); + + $this->assertSame(['12345', '67890'], $result); + $this->assertFileExists($this->tempPidFile); + } + + public function testDeleteRemovesPidFile(): void + { + file_put_contents($this->tempPidFile, 'dummyData'); + + $manager = new PidManager($this->tempPidFile); + $deleted = $manager->delete(); + + $this->assertTrue($deleted); + $this->assertFileDoesNotExist($this->tempPidFile); + } + + public function testDeleteReturnsFalseIfFileNotWritable(): void + { + file_put_contents($this->tempPidFile, 'dummyData'); + chmod($this->tempPidFile, 0444); + + $manager = new PidManager($this->tempPidFile); + $result = $manager->delete(); + + $this->assertFalse($result); + + chmod($this->tempPidFile, 0644); + } + + public function testWriteThrowsWhenFileNotWritable(): void + { + $unwritableDir = sys_get_temp_dir() . '/unwritable_dir'; + mkdir($unwritableDir, 0444); + $unwritableFile = $unwritableDir . '/file.pid'; + + $manager = new PidManager($unwritableFile); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/not writable/'); + + try { + $manager->write(1, 2); + } finally { + chmod($unwritableDir, 0755); + rmdir($unwritableDir); + } + } +} diff --git a/test/Swoole/ServerFactoryTest.php b/test/Swoole/ServerFactoryTest.php new file mode 100644 index 0000000..007ea40 --- /dev/null +++ b/test/Swoole/ServerFactoryTest.php @@ -0,0 +1,143 @@ +factory = new ServerFactory(); + } + + /** + * @throws Exception + */ + #[RunInSeparateProcess] + public function testInvokeWithMinimalValidConfig(): void + { + $config = [ + 'dotkernel-queue-swoole' => [ + 'swoole-tcp-server' => [], + ], + ]; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->with('config')->willReturn($config); + + $server = $this->factory->__invoke($container); + + $this->assertContainsOnlyInstancesOf(Server::class, [$server]); + } + + /** + * @throws Exception + */ + #[RunInSeparateProcess] + public function testInvokeWithCustomValidConfig(): void + { + $config = [ + 'dotkernel-queue-swoole' => [ + 'enable_coroutine' => true, + 'swoole-tcp-server' => [ + 'host' => '127.0.0.1', + 'port' => 9502, + 'mode' => SWOOLE_BASE, + 'protocol' => SWOOLE_SOCK_TCP, + 'options' => [ + 'worker_num' => 1, + ], + ], + ], + ]; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->with('config')->willReturn($config); + + $server = $this->factory->__invoke($container); + + $this->assertContainsOnlyInstancesOf(Server::class, [$server]); + } + + /** + * @throws Exception + */ + public function testThrowsOnInvalidPort(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid port'); + + $config = [ + 'dotkernel-queue-swoole' => [ + 'swoole-tcp-server' => [ + 'port' => 70000, + ], + ], + ]; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->with('config')->willReturn($config); + + $this->factory->__invoke($container); + } + + /** + * @throws Exception + */ + public function testThrowsOnInvalidMode(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid server mode'); + + $config = [ + 'dotkernel-queue-swoole' => [ + 'swoole-tcp-server' => [ + 'mode' => -1, + ], + ], + ]; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->with('config')->willReturn($config); + + $this->factory->__invoke($container); + } + + /** + * @throws Exception + */ + public function testThrowsOnInvalidProtocol(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid server protocol'); + + $config = [ + 'dotkernel-queue-swoole' => [ + 'swoole-tcp-server' => [ + 'protocol' => -99, + ], + ], + ]; + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->with('config')->willReturn($config); + + $this->factory->__invoke($container); + } +} diff --git a/test/SwooleConfigProviderTest.php b/test/Swoole/SwooleConfigProviderTest.php similarity index 93% rename from test/SwooleConfigProviderTest.php rename to test/Swoole/SwooleConfigProviderTest.php index 8647eb6..9c479ca 100644 --- a/test/SwooleConfigProviderTest.php +++ b/test/Swoole/SwooleConfigProviderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace DotTest\Mail; +namespace QueueTest\Swoole; use PHPUnit\Framework\TestCase; use Queue\Swoole\ConfigProvider;