diff --git a/.docker/velox.toml b/.docker/velox.toml index 832a17b..902d57f 100644 --- a/.docker/velox.toml +++ b/.docker/velox.toml @@ -93,9 +93,4 @@ repository = "tcp" [github.plugins.smtp-server] ref = "2.0.0" owner = "buggregator" -repository = "smtp-server" - -[github.plugins.var-dumper-server] -ref = "1.0.0" -owner = "buggregator" -repository = "var-dumper-server" \ No newline at end of file +repository = "smtp-server" \ No newline at end of file diff --git a/.rr-prod.yaml b/.rr-prod.yaml index e232742..250033d 100644 --- a/.rr-prod.yaml +++ b/.rr-prod.yaml @@ -13,26 +13,32 @@ server: command: "php app.php register:modules" logs: + # Logging mode can be "development", "production" or "raw". + # Do not forget to change this value for production environment. mode: ${RR_LOG_MODE:-production} + # Encoding format can be "console" or "json" (last is preferred for production usage). encoding: ${RR_LOG_ENCODING:-json} + # Logging level can be "panic", "error", "warn", "info", "debug". level: ${RR_LOG_LEVEL:-warn} channels: http: + # HTTP plugin logging level can be "panic", "error", "warn", "info", "debug". level: ${RR_LOG_HTTP_LEVEL:-warn} tcp: + # TCP plugin logging level can be "panic", "error", "warn", "info", "debug". level: ${RR_LOG_TCP_LEVEL:-warn} jobs: + # JOBS plugin logging level can be "panic", "error", "warn", "info", "debug". level: ${RR_LOG_JOBS_LEVEL:-warn} centrifuge: + # Centrifuge plugin logging level can be "panic", "error", "warn", "info", "debug". level: ${RR_LOG_CENTRIFUGE_LEVEL:-warn} server: + # Server logging level can be "panic", "error", "warn", "info", "debug". level: ${RR_LOG_SERVER_LEVEL:-warn} service: + # Service logging level can be "panic", "error", "warn", "info", "debug". level: ${RR_LOG_SERVICE_LEVEL:-warn} - smtp: - level: ${RR_LOG_SMTP_LEVEL:-warn} - var-dumper: - level: ${RR_LOG_VAR_DUMPER_LEVEL:-warn} http: address: 127.0.0.1:8082 @@ -51,11 +57,20 @@ http: tcp: servers: monolog: + # Address to listen. addr: ${RR_TCP_MONOLOG_ADDR:-:9913} delimiter: "\n" + var-dumper: + # Address to listen. + addr: ${RR_TCP_VAR_DUMPER_ADDR:-:9912} + delimiter: "\n" + # Chunks that RR uses to read the data. In bytes. + # If you expect big payloads on a TCP server, to reduce `read` syscalls, + # would be a good practice to use a fairly big enough buffer. + # Default: 1024 * 1024 * 50 (50MB) read_buf_size: ${RR_TCP_READ_BUF_SIZE:-50485760} pool: - num_workers: ${RR_TCP_NUM_WORKERS:-1} + num_workers: ${RR_TCP_NUM_WORKERS:-2} kv: local: @@ -64,9 +79,9 @@ kv: jobs: consume: - - events + - smtp pipelines: - events: + smtp: driver: memory config: priority: 10 @@ -78,13 +93,7 @@ smtp: addr: ${RR_SMTP_ADDR:-:1025} hostname: "buggregator.local" jobs: - pipeline: events - -var-dumper: - addr: ${RR_VAR_DUMPER_ADDR:-:9912} - max_message_size: ${RR_TCP_READ_BUF_SIZE:-50485760} - jobs: - pipeline: events + pipeline: smtp service: nginx: diff --git a/app/config/queue.php b/app/config/queue.php index 1b4f41a..84dca51 100644 --- a/app/config/queue.php +++ b/app/config/queue.php @@ -3,7 +3,6 @@ declare(strict_types=1); use Modules\Smtp\Interfaces\Jobs\EmailHandler; -use Modules\VarDumper\Interfaces\Jobs\DumpHandler; use Modules\Webhooks\Interfaces\Job\WebhookHandler; use Spiral\Queue\Driver\SyncDriver; use Spiral\RoadRunner\Jobs\Queue\MemoryCreateInfo; @@ -40,7 +39,6 @@ 'registry' => [ 'handlers' => [ 'smtp.email' => EmailHandler::class, - 'vardumper.dump' => DumpHandler::class, ], 'serializers' => [ WebhookHandler::class => 'symfony-json', diff --git a/app/config/tcp.php b/app/config/tcp.php index 8ef903f..9f01607 100644 --- a/app/config/tcp.php +++ b/app/config/tcp.php @@ -3,10 +3,12 @@ declare(strict_types=1); use App\Application\TCP\ExceptionHandlerInterceptor; +use Modules\VarDumper\Interfaces\TCP\Service as VarDumperService; use Modules\Monolog\Interfaces\TCP\Service as MonologService; return [ 'services' => [ + 'var-dumper' => VarDumperService::class, 'monolog' => MonologService::class, ], diff --git a/app/modules/VarDumper/Interfaces/Jobs/DumpHandler.php b/app/modules/VarDumper/Interfaces/TCP/Service.php similarity index 69% rename from app/modules/VarDumper/Interfaces/Jobs/DumpHandler.php rename to app/modules/VarDumper/Interfaces/TCP/Service.php index 595e25e..5dbd224 100644 --- a/app/modules/VarDumper/Interfaces/Jobs/DumpHandler.php +++ b/app/modules/VarDumper/Interfaces/TCP/Service.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Modules\VarDumper\Interfaces\Jobs; +namespace Modules\VarDumper\Interfaces\TCP; use App\Application\Commands\HandleReceivedEvent; use Modules\VarDumper\Application\Dump\BodyInterface; @@ -12,32 +12,41 @@ use Modules\VarDumper\Application\Dump\MessageParser; use Modules\VarDumper\Application\Dump\ParsedPayload; use Modules\VarDumper\Application\Dump\PrimitiveBody; -use Spiral\Core\InvokerInterface; use Spiral\Cqrs\CommandBusInterface; -use Spiral\Queue\JobHandler; +use Spiral\RoadRunner\Tcp\Request; +use Spiral\RoadRunner\Tcp\TcpEvent; +use Spiral\RoadRunnerBridge\Tcp\Response\ContinueRead; +use Spiral\RoadRunnerBridge\Tcp\Response\ResponseInterface; +use Spiral\RoadRunnerBridge\Tcp\Service\ServiceInterface; use Symfony\Component\VarDumper\Cloner\Data; -final class DumpHandler extends JobHandler +final readonly class Service implements ServiceInterface { public function __construct( - private readonly CommandBusInterface $bus, - private readonly DumpIdGeneratorInterface $dumpId, - InvokerInterface $invoker, - ) { - parent::__construct($invoker); - } + private CommandBusInterface $commandBus, + private DumpIdGeneratorInterface $dumpId, + ) {} - public function invoke(mixed $payload): void + public function handle(Request $request): ResponseInterface { - $this->fireEvent( - (new MessageParser())->parse($payload), - ); - } + if ($request->event === TcpEvent::Connected) { + return new ContinueRead(); + } + + $messages = \array_filter(\explode("\n", $request->body)); + foreach ($messages as $message) { + $payload = (new MessageParser())->parse($message); + + $this->fireEvent($payload); + } + + return new ContinueRead(); + } private function fireEvent(ParsedPayload $payload): void { - $this->bus->dispatch( + $this->commandBus->dispatch( new HandleReceivedEvent( type: 'var-dump', payload: [ @@ -87,5 +96,4 @@ private function prepareContent(ParsedPayload $payload): array return $payloadContent; } - } diff --git a/docker-compose.yaml b/docker-compose.yaml index 241ec3e..5f32c78 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -31,7 +31,7 @@ services: args: GH_TOKEN: "${GH_TOKEN}" environment: - RR_LOG_LEVEL: debug +# RR_LOG_LEVEL: debug # RR_LOG_TCP_LEVEL: debug # RR_LOG_JOBS_LEVEL: debug # RR_LOG_SERVER_LEVEL: debug diff --git a/tests/Feature/Interfaces/TCP/TCPTestCase.php b/tests/Feature/Interfaces/TCP/TCPTestCase.php index df0c3e0..5d4375f 100644 --- a/tests/Feature/Interfaces/TCP/TCPTestCase.php +++ b/tests/Feature/Interfaces/TCP/TCPTestCase.php @@ -5,9 +5,15 @@ namespace Tests\Feature\Interfaces\TCP; use Modules\Monolog\Interfaces\TCP\Service as MonologService; +use Modules\VarDumper\Interfaces\TCP\Service as VarDumperService; +use Modules\Smtp\Interfaces\TCP\Service as SmtpService; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; use Spiral\RoadRunner\Tcp\Request; use Spiral\RoadRunner\Tcp\TcpEvent; use Spiral\RoadRunnerBridge\Tcp\Response\ResponseInterface; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; +use Tests\App\Smtp\FakeStream; use Tests\DatabaseTestCase; abstract class TCPTestCase extends DatabaseTestCase @@ -19,6 +25,20 @@ public function handleMonologRequest(string $message): ResponseInterface ->handle($this->buildRequest(message: $message)); } + public function handleVarDumperRequest(string $message): ResponseInterface + { + return $this + ->get(VarDumperService::class) + ->handle($this->buildRequest(message: $message)); + } + + public function handleSmtpRequest(string $message, TcpEvent $event = TcpEvent::Data): ResponseInterface + { + return $this + ->get(SmtpService::class) + ->handle($this->buildRequest(message: $message, event: $event)); + } + private function buildRequest(string $message, TcpEvent $event = TcpEvent::Data): Request { return new Request( @@ -29,4 +49,19 @@ private function buildRequest(string $message, TcpEvent $event = TcpEvent::Data) server: 'localhost', ); } + + protected function buildSmtpClient(string $username = 'homestead', ?UuidInterface $uuid = null): EsmtpTransport + { + $client = new EsmtpTransport( + stream: new FakeStream( + service: $this->get(SmtpService::class), + uuid: (string) $uuid ?? Uuid::uuid7(), + ), + ); + + $client->setUsername($username); + $client->setPassword('password'); + + return $client; + } } diff --git a/tests/Feature/Interfaces/TCP/VarDumper/SymfonyV6Test.php b/tests/Feature/Interfaces/TCP/VarDumper/SymfonyV6Test.php new file mode 100644 index 0000000..1614802 --- /dev/null +++ b/tests/Feature/Interfaces/TCP/VarDumper/SymfonyV6Test.php @@ -0,0 +1,35 @@ +handleVarDumperRequest($payload); + + $this->broadcastig->assertPushed(new EventsChannel('default'), function (array $data) { + $this->assertSame('event.received', $data['event']); + $this->assertSame('var-dump', $data['data']['type']); + + $this->assertSame([ + 'type' => 'string', + 'value' => 'foo', + 'label' => null, + ], $data['data']['payload']['payload']); + + $this->assertNotEmpty($data['data']['uuid']); + $this->assertNotEmpty($data['data']['timestamp']); + + + return true; + }); + } +} diff --git a/tests/Feature/Interfaces/TCP/VarDumper/SymfonyV7Test.php b/tests/Feature/Interfaces/TCP/VarDumper/SymfonyV7Test.php new file mode 100644 index 0000000..774222f --- /dev/null +++ b/tests/Feature/Interfaces/TCP/VarDumper/SymfonyV7Test.php @@ -0,0 +1,148 @@ + ['foo', 'string', 'foo']; + yield 'true' => [true, 'boolean', '1']; + yield 'false' => [false, 'boolean', '0']; + yield 'int' => [1, 'integer', '1']; + yield 'float' => [1.1, 'double', '1.1']; + yield 'array' => [ + ['foo' => 'bar'], + 'array', + <<<'HTML' +
Some label array:1 [
+  "foo" => "bar"
+]
+
+ +HTML + , + ]; + yield 'object' => [ + (object) ['type' => 'string', 'value' => 'foo'], + 'stdClass', + <<Some label {#%s + +"type": "string" + +"value": "foo" +} + + +HTML + , + ]; + } + + #[DataProvider('variablesDataProvider')] + public function testSendDump(mixed $value, string $type, mixed $expected): void + { + $generator = $this->mockContainer(DumpIdGeneratorInterface::class); + $generator->shouldReceive('generate')->andReturn('sf-dump-730421088'); + + $message = $this->buildPayload(var: $value); + $this->handleVarDumperRequest($message); + + if (\is_object($value)) { + $expected = \sprintf($expected, \spl_object_id($value)); + } + + $this->broadcastig->assertPushed(new EventsChannel('default'), function (array $data) use ($value, $type, $expected) { + $this->assertSame('event.received', $data['event']); + $this->assertSame('var-dump', $data['data']['type']); + + $this->assertSame([ + 'type' => $type, + 'value' => $expected, + 'label' => 'Some label', + ], $data['data']['payload']['payload']); + + $this->assertNotEmpty($data['data']['uuid']); + $this->assertNotEmpty($data['data']['timestamp']); + + return true; + }); + } + + public function testSendDumpWithCodeHighlighting(): void + { + $message = $this->buildPayload(var: 'foo', context: ['language' => 'php']); + $this->handleVarDumperRequest($message); + + $this->broadcastig->assertPushed(new EventsChannel('default'), function (array $data) { + $this->assertSame('event.received', $data['event']); + $this->assertSame('var-dump', $data['data']['type']); + + $this->assertSame([ + 'type' => 'code', + 'value' => 'foo', + 'label' => 'Some label', + 'language' => 'php', + ], $data['data']['payload']['payload']); + + return true; + }); + } + + public function testSendDumpWithProject(): void + { + $this->createProject('foo'); + $message = $this->buildPayload(project: 'foo'); + $this->handleVarDumperRequest($message); + + $this->broadcastig->assertPushed(new EventsChannel('foo'), function (array $data) { + $this->assertSame('foo', $data['data']['project']); + return true; + }); + } + + public function testSendDumpWithNonExistsProject(): void + { + $message = $this->buildPayload(project: 'foo'); + $this->handleVarDumperRequest($message); + + $this->broadcastig->assertNotPushed(new EventsChannel('foo')); + $this->broadcastig->assertPushed(new EventsChannel('default'), function (array $data) { + $this->assertSame('default', $data['data']['project']); + return true; + }); + } + + public function testSendInvalidDump(): void + { + $this->expectException(InvalidPayloadException::class); + $this->expectExceptionMessage('Unable to decode the message.'); + + $this->handleVarDumperRequest('invalid'); + } + + private function buildPayload(mixed $var = 'string', ?string $project = null, array $context = []): string + { + $cloner = new VarCloner(); + $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); + + $data = $cloner->cloneVar($var); + + if ($project !== null) { + $context['project'] = $project; + } + + $context['label'] = 'Some label'; + + return \base64_encode(\serialize([$data->withContext($context), []])) . "\n"; + } +}