diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 49d03346..4a594055 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -78,9 +78,10 @@ jobs:
uses: docker/build-push-action@v2
with:
context: ./
- file: ./Dockerfile
+ file: ./docker/Dockerfile
push: true
build-args:
APP_VERSION=${{ steps.previoustag.outputs.tag }}
+ FRONTEND_IMAGE_TAG=latest
tags:
${{ secrets.DOCKER_HUB_USERNAME }}/buggregator:latest, ${{ secrets.DOCKER_HUB_USERNAME }}/buggregator:${{ steps.previoustag.outputs.tag }}
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644
index 00000000..075f8fe3
--- /dev/null
+++ b/.github/workflows/phpunit.yml
@@ -0,0 +1,16 @@
+on:
+ pull_request: null
+
+name: phpunit
+
+jobs:
+ phpunit:
+ uses: spiral/gh-actions/.github/workflows/phpunit.yml@master
+ with:
+ install_protoc: true
+ os: >-
+ ['ubuntu-latest']
+ php: >-
+ ['8.1', '8.2']
+ stability: >-
+ ['prefer-stable']
diff --git a/.gitignore b/.gitignore
index 2912f5a0..d408eab6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,6 @@ rr*
protoc-gen-php-grpc*
.env
.phpunit.result.cache
+.php-cs-fixer.cache
.deptrac.cache
composer.lock
diff --git a/.rr.yaml b/.rr.yaml
index 822e037d..26550918 100644
--- a/.rr.yaml
+++ b/.rr.yaml
@@ -1,4 +1,4 @@
-version: '2.7'
+version: '3'
rpc:
listen: tcp://127.0.0.1:6001
diff --git a/README.md b/README.md
index a18fd218..4ec3b5f4 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
[](https://twitter.com/buggregator)
[](https://patreon.com/butschster)
+[](https://github.com/buggregator/server/actions/workflows/phpunit.yml)
**Buggregator is a lightweight, standalone server that offers a range of debugging features for PHP applications. Think of it as a Swiss Army knife for developers. What makes it special is that it offers a range of features that you would usually find in various paid tools, but it's available for free.**
diff --git a/app/config/broadcasting.php b/app/config/broadcasting.php
index 1ba0f0a7..745a477d 100644
--- a/app/config/broadcasting.php
+++ b/app/config/broadcasting.php
@@ -2,6 +2,7 @@
declare(strict_types=1);
+use App\Application\Broadcasting\InMemoryDriver;
use Spiral\Broadcasting\Driver\NullBroadcast;
return [
@@ -14,5 +15,8 @@
'null' => [
'driver' => NullBroadcast::class,
],
+ 'in-memory' => [
+ 'driver' => InMemoryDriver::class,
+ ],
],
];
diff --git a/app/config/cache.php b/app/config/cache.php
index 14cd6e06..2647bce4 100644
--- a/app/config/cache.php
+++ b/app/config/cache.php
@@ -3,18 +3,22 @@
declare(strict_types=1);
use Spiral\Cache\Storage\ArrayStorage;
-use Spiral\Cache\Storage\FileStorage;
+
+$defaultStorage = env('CACHE_DEFAULT_STORAGE', 'roadrunner');
return [
- 'default' => env('CACHE_STORAGE', 'local'),
+ 'default' => env('CACHE_STORAGE', 'roadrunner'),
+ 'aliases' => [
+ 'events' => ['storage' => $defaultStorage, 'prefix' => 'events:'],
+ 'local' => ['storage' => $defaultStorage, 'prefix' => 'local:'],
+ ],
'storages' => [
- 'local' => [
- 'type' => 'roadrunner',
- 'driver' => 'local',
+ 'array' => [
+ 'type' => ArrayStorage::class,
],
- 'events' => [
+ 'roadrunner' => [
'type' => 'roadrunner',
- 'driver' => 'events',
+ 'driver' => 'local',
],
],
];
diff --git a/app/migrations/20221204.005409_0_0_default_create_events.php b/app/migrations/20221204.005409_0_0_default_create_events.php
index 99af059b..5450b2fe 100644
--- a/app/migrations/20221204.005409_0_0_default_create_events.php
+++ b/app/migrations/20221204.005409_0_0_default_create_events.php
@@ -16,7 +16,7 @@ public function up(): void
->addColumn('uuid', 'string', ['nullable' => false, 'default' => null])
->addColumn('type', 'string', ['nullable' => false, 'default' => null])
->addColumn('payload', 'longText', ['nullable' => false, 'default' => null])
- ->addColumn('date', 'datetime', ['nullable' => false, 'default' => null])
+ ->addColumn('timestamp', 'float', ['nullable' => false, 'default' => null])
->addColumn('project_id', 'integer', ['nullable' => true, 'default' => null])
->setPrimaryKeys(['uuid'])
->create();
diff --git a/app/modules/Events/Domain/Event.php b/app/modules/Events/Domain/Event.php
index 00bbdc7c..64be4f07 100644
--- a/app/modules/Events/Domain/Event.php
+++ b/app/modules/Events/Domain/Event.php
@@ -26,8 +26,8 @@ public function __construct(
#[Column(type: 'longText', typecast: 'json')]
private Json $payload,
- #[Column(type: 'datetime')]
- private DateTimeImmutable $date,
+ #[Column(type: 'float')]
+ private float $timestamp,
#[Column(type: 'integer', nullable: true)]
private ?int $projectId,
@@ -49,9 +49,9 @@ public function getPayload(): Json
return $this->payload;
}
- public function getDate(): DateTimeImmutable
+ public function getTimestamp(): float
{
- return $this->date;
+ return $this->timestamp;
}
public function getProjectId(): ?int
diff --git a/app/modules/Events/Domain/EventRepositoryInterface.php b/app/modules/Events/Domain/EventRepositoryInterface.php
index 71bf096a..35ebb9de 100644
--- a/app/modules/Events/Domain/EventRepositoryInterface.php
+++ b/app/modules/Events/Domain/EventRepositoryInterface.php
@@ -7,7 +7,7 @@
use Cycle\ORM\RepositoryInterface;
/**
- * @template TEntity of Event
+ * @extends RepositoryInterface
*/
interface EventRepositoryInterface extends RepositoryInterface
{
diff --git a/app/modules/Events/Domain/Events/EventWasReceived.php b/app/modules/Events/Domain/Events/EventWasReceived.php
index bce5e30f..1e56f60e 100644
--- a/app/modules/Events/Domain/Events/EventWasReceived.php
+++ b/app/modules/Events/Domain/Events/EventWasReceived.php
@@ -14,7 +14,7 @@ public function __construct(
public readonly Uuid $uuid,
public readonly string $type,
public readonly array $payload,
- public readonly int $timestamp,
+ public readonly float $timestamp,
public readonly ?int $projectId = null,
) {
}
diff --git a/app/modules/Events/Application/Commands/ClearEventsHandler.php b/app/modules/Events/Interfaces/Commands/ClearEventsHandler.php
similarity index 85%
rename from app/modules/Events/Application/Commands/ClearEventsHandler.php
rename to app/modules/Events/Interfaces/Commands/ClearEventsHandler.php
index 379a3443..02bc36af 100644
--- a/app/modules/Events/Application/Commands/ClearEventsHandler.php
+++ b/app/modules/Events/Interfaces/Commands/ClearEventsHandler.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Modules\Events\Application\Commands;
+namespace Modules\Events\Interfaces\Commands;
use App\Application\Commands\ClearEvents;
use Modules\Events\Domain\EventRepositoryInterface;
@@ -14,7 +14,7 @@ final class ClearEventsHandler
{
public function __construct(
private readonly EventRepositoryInterface $events,
- private readonly EventDispatcherInterface $dispatcher
+ private readonly EventDispatcherInterface $dispatcher,
) {
}
diff --git a/app/modules/Events/Application/Commands/DeleteEventHandler.php b/app/modules/Events/Interfaces/Commands/DeleteEventHandler.php
similarity index 93%
rename from app/modules/Events/Application/Commands/DeleteEventHandler.php
rename to app/modules/Events/Interfaces/Commands/DeleteEventHandler.php
index 30ce6b3c..d16a70c6 100644
--- a/app/modules/Events/Application/Commands/DeleteEventHandler.php
+++ b/app/modules/Events/Interfaces/Commands/DeleteEventHandler.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Modules\Events\Application\Commands;
+namespace Modules\Events\Interfaces\Commands;
use App\Application\Commands\DeleteEvent;
use Modules\Events\Domain\EventRepositoryInterface;
diff --git a/app/modules/Events/Application/Commands/StoreEventHandler.php b/app/modules/Events/Interfaces/Commands/StoreEventHandler.php
similarity index 86%
rename from app/modules/Events/Application/Commands/StoreEventHandler.php
rename to app/modules/Events/Interfaces/Commands/StoreEventHandler.php
index 820a9a42..62767d3d 100644
--- a/app/modules/Events/Application/Commands/StoreEventHandler.php
+++ b/app/modules/Events/Interfaces/Commands/StoreEventHandler.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Modules\Events\Application\Commands;
+namespace Modules\Events\Interfaces\Commands;
use App\Application\Commands\FindProjectByName;
use App\Application\Commands\HandleReceivedEvent;
@@ -20,7 +20,7 @@ final class StoreEventHandler
public function __construct(
private readonly EventDispatcherInterface $dispatcher,
private readonly EventRepositoryInterface $events,
- private readonly QueryBusInterface $queryBus
+ private readonly QueryBusInterface $queryBus,
) {
}
@@ -37,9 +37,9 @@ public function handle(HandleReceivedEvent $command): void
$command->uuid,
$command->type,
new Json($command->payload),
- Carbon::createFromTimestamp($command->timestamp)->toDateTimeImmutable(),
+ $command->timestamp,
$projectId,
- )
+ ),
);
$this->dispatcher->dispatch(
@@ -49,7 +49,7 @@ public function handle(HandleReceivedEvent $command): void
payload: $command->payload,
timestamp: $command->timestamp,
projectId: $projectId,
- )
+ ),
);
}
}
diff --git a/app/modules/Events/Interfaces/Http/Controllers/ClearEventsAction.php b/app/modules/Events/Interfaces/Http/Controllers/ClearAction.php
similarity index 64%
rename from app/modules/Events/Interfaces/Http/Controllers/ClearEventsAction.php
rename to app/modules/Events/Interfaces/Http/Controllers/ClearAction.php
index 6380231c..ac3b4b91 100644
--- a/app/modules/Events/Interfaces/Http/Controllers/ClearEventsAction.php
+++ b/app/modules/Events/Interfaces/Http/Controllers/ClearAction.php
@@ -5,17 +5,21 @@
namespace Modules\Events\Interfaces\Http\Controllers;
use App\Application\Commands\ClearEvents;
+use App\Application\HTTP\Response\ResourceInterface;
+use App\Application\HTTP\Response\SuccessResource;
use Modules\Events\Interfaces\Http\Request\ClearEventsRequest;
use Spiral\Cqrs\CommandBusInterface;
use Spiral\Router\Annotation\Route;
-final class ClearEventsAction
+final class ClearAction
{
#[Route(route: 'events', name: 'events.clear', methods: 'DELETE', group: 'api')]
- public function __invoke(ClearEventsRequest $request, CommandBusInterface $bus): void
+ public function __invoke(ClearEventsRequest $request, CommandBusInterface $bus): ResourceInterface
{
$bus->dispatch(
- new ClearEvents(type: $request->type)
+ new ClearEvents(type: $request->type),
);
+
+ return new SuccessResource();
}
}
diff --git a/app/modules/Events/Interfaces/Http/Controllers/DeleteEventAction.php b/app/modules/Events/Interfaces/Http/Controllers/DeleteAction.php
similarity index 66%
rename from app/modules/Events/Interfaces/Http/Controllers/DeleteEventAction.php
rename to app/modules/Events/Interfaces/Http/Controllers/DeleteAction.php
index ea696049..3c72df1c 100644
--- a/app/modules/Events/Interfaces/Http/Controllers/DeleteEventAction.php
+++ b/app/modules/Events/Interfaces/Http/Controllers/DeleteAction.php
@@ -6,16 +6,20 @@
use App\Application\Commands\DeleteEvent;
use App\Application\Domain\ValueObjects\Uuid;
+use App\Application\HTTP\Response\ResourceInterface;
+use App\Application\HTTP\Response\SuccessResource;
use Spiral\Cqrs\CommandBusInterface;
use Spiral\Router\Annotation\Route;
-final class DeleteEventAction
+final class DeleteAction
{
#[Route(route: 'event/', name: 'event.delete', methods: 'DELETE', group: 'api')]
- public function __invoke(CommandBusInterface $bus, Uuid $uuid): void
+ public function __invoke(CommandBusInterface $bus, Uuid $uuid): ResourceInterface
{
$bus->dispatch(
- new DeleteEvent($uuid)
+ new DeleteEvent($uuid),
);
+
+ return new SuccessResource();
}
}
diff --git a/app/modules/Events/Interfaces/Http/Controllers/ListAction.php b/app/modules/Events/Interfaces/Http/Controllers/ListAction.php
index d5954244..57427357 100644
--- a/app/modules/Events/Interfaces/Http/Controllers/ListAction.php
+++ b/app/modules/Events/Interfaces/Http/Controllers/ListAction.php
@@ -6,18 +6,19 @@
use App\Application\Commands\FindEvents;
use Modules\Events\Interfaces\Http\Request\EventsRequest;
+use Modules\Events\Interfaces\Http\Resources\EventCollection;
use Spiral\Cqrs\QueryBusInterface;
use Spiral\Router\Annotation\Route;
-class ListAction
+final class ListAction
{
#[Route(route: 'events', name: 'events.list', methods: 'GET', group: 'api')]
public function __invoke(EventsRequest $request, QueryBusInterface $bus): EventCollection
{
return new EventCollection(
$bus->ask(
- new FindEvents(type: $request->type)
- )
+ new FindEvents(type: $request->type),
+ ),
);
}
}
diff --git a/app/modules/Events/Interfaces/Http/Controllers/ShowAction.php b/app/modules/Events/Interfaces/Http/Controllers/ShowAction.php
index cd822d4e..6a53ecca 100644
--- a/app/modules/Events/Interfaces/Http/Controllers/ShowAction.php
+++ b/app/modules/Events/Interfaces/Http/Controllers/ShowAction.php
@@ -7,6 +7,7 @@
use App\Application\Commands\FindEventByUuid;
use App\Application\Domain\ValueObjects\Uuid;
use App\Application\Exception\EntityNotFoundException;
+use Modules\Events\Interfaces\Http\Resources\EventResource;
use Spiral\Cqrs\QueryBusInterface;
use Spiral\Http\Exception\ClientException\NotFoundException;
use Spiral\Router\Annotation\Route;
diff --git a/app/modules/Events/Interfaces/Http/Request/ClearEventsRequest.php b/app/modules/Events/Interfaces/Http/Request/ClearEventsRequest.php
index ba32315d..5b229290 100644
--- a/app/modules/Events/Interfaces/Http/Request/ClearEventsRequest.php
+++ b/app/modules/Events/Interfaces/Http/Request/ClearEventsRequest.php
@@ -4,7 +4,7 @@
namespace Modules\Events\Interfaces\Http\Request;
-use Spiral\Filters\Attribute\Input\Post;
+use Spiral\Filters\Attribute\Input\Data;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
@@ -12,7 +12,7 @@
final class ClearEventsRequest extends Filter implements HasFilterDefinition
{
- #[Post]
+ #[Data]
public ?string $type = null;
public function filterDefinition(): FilterDefinitionInterface
diff --git a/app/modules/Events/Interfaces/Http/Controllers/EventCollection.php b/app/modules/Events/Interfaces/Http/Resources/EventCollection.php
similarity index 83%
rename from app/modules/Events/Interfaces/Http/Controllers/EventCollection.php
rename to app/modules/Events/Interfaces/Http/Resources/EventCollection.php
index aead576a..56884fb0 100644
--- a/app/modules/Events/Interfaces/Http/Controllers/EventCollection.php
+++ b/app/modules/Events/Interfaces/Http/Resources/EventCollection.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Modules\Events\Interfaces\Http\Controllers;
+namespace Modules\Events\Interfaces\Http\Resources;
use App\Application\HTTP\Response\ResourceCollection;
diff --git a/app/modules/Events/Interfaces/Http/Controllers/EventResource.php b/app/modules/Events/Interfaces/Http/Resources/EventResource.php
similarity index 68%
rename from app/modules/Events/Interfaces/Http/Controllers/EventResource.php
rename to app/modules/Events/Interfaces/Http/Resources/EventResource.php
index ac26ee2f..634e137d 100644
--- a/app/modules/Events/Interfaces/Http/Controllers/EventResource.php
+++ b/app/modules/Events/Interfaces/Http/Resources/EventResource.php
@@ -2,11 +2,10 @@
declare(strict_types=1);
-namespace Modules\Events\Interfaces\Http\Controllers;
+namespace Modules\Events\Interfaces\Http\Resources;
use App\Application\HTTP\Response\JsonResource;
use Modules\Events\Domain\Event;
-use Psr\Http\Message\ServerRequestInterface;
/**
* @property-read Event $data
@@ -18,13 +17,13 @@ public function __construct(Event $data)
parent::__construct($data);
}
- protected function mapData(ServerRequestInterface $request): array|\JsonSerializable
+ protected function mapData(): array|\JsonSerializable
{
return [
'uuid' => (string)$this->data->getUuid(),
'type' => $this->data->getType(),
'payload' => $this->data->getPayload(),
- 'timestamp' => $this->data->getDate()->getTimestamp(),
+ 'timestamp' => $this->data->getTimestamp(),
'project_id' => $this->data->getProjectId(),
];
}
diff --git a/app/modules/Events/Application/Queries/CountEventsHandler.php b/app/modules/Events/Interfaces/Queries/CountEventsHandler.php
similarity index 91%
rename from app/modules/Events/Application/Queries/CountEventsHandler.php
rename to app/modules/Events/Interfaces/Queries/CountEventsHandler.php
index 946af6f2..212e70d3 100644
--- a/app/modules/Events/Application/Queries/CountEventsHandler.php
+++ b/app/modules/Events/Interfaces/Queries/CountEventsHandler.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Modules\Events\Application\Queries;
+namespace Modules\Events\Interfaces\Queries;
use App\Application\Commands\CountEvents;
use Modules\Events\Domain\EventRepositoryInterface;
diff --git a/app/modules/Events/Application/Queries/EventsHandler.php b/app/modules/Events/Interfaces/Queries/EventsHandler.php
similarity index 89%
rename from app/modules/Events/Application/Queries/EventsHandler.php
rename to app/modules/Events/Interfaces/Queries/EventsHandler.php
index 5a1de0c8..9c9dde40 100644
--- a/app/modules/Events/Application/Queries/EventsHandler.php
+++ b/app/modules/Events/Interfaces/Queries/EventsHandler.php
@@ -1,6 +1,6 @@
[self::class, 'eventHandler'],
- ];
-
- public function boot(
- HandlerRegistryInterface $registry,
- AnyHttpRequestDump $handler
- ): void {
- $registry->register($handler);
- }
-
- public function eventHandler(ContainerInterface $container): EventHandlerInterface
+ public function defineSingletons(): array
{
- return new EventHandler($container, []);
+ return [
+ EventHandlerInterface::class => static function (ContainerInterface $container): EventHandlerInterface {
+ return new EventHandler($container, []);
+ },
+ ];
}
}
diff --git a/app/modules/HttpDumps/Interfaces/Http/Handler/AnyHttpRequestDump.php b/app/modules/HttpDumps/Interfaces/Http/Handler/AnyHttpRequestDump.php
index 2d70df1c..ca2dbfd2 100644
--- a/app/modules/HttpDumps/Interfaces/Http/Handler/AnyHttpRequestDump.php
+++ b/app/modules/HttpDumps/Interfaces/Http/Handler/AnyHttpRequestDump.php
@@ -51,7 +51,7 @@ public function handle(ServerRequestInterface $request, \Closure $next): Respons
$event = $this->handler->handle($payload);
$this->commands->dispatch(
- new HandleReceivedEvent(type: 'http-dump', payload: $event)
+ new HandleReceivedEvent(type: 'http-dump', payload: $event),
);
return $this->responseWrapper->create(200);
@@ -77,7 +77,7 @@ private function createPayload(ServerRequestInterface $request): array
function (UploadedFileInterface $attachment) use ($id) {
$this->bucket->write(
$filename = $id . '/' . $attachment->getClientFilename(),
- $attachment->getStream()
+ $attachment->getStream(),
);
return [
@@ -88,9 +88,9 @@ function (UploadedFileInterface $attachment) use ($id) {
'mime' => $attachment->getClientMediaType(),
];
},
- $request->getUploadedFiles()
+ $request->getUploadedFiles(),
),
- ]
+ ],
];
}
}
diff --git a/app/modules/Inspector/Application/InspectorBootloader.php b/app/modules/Inspector/Application/InspectorBootloader.php
index 0116788d..a9b7e94e 100644
--- a/app/modules/Inspector/Application/InspectorBootloader.php
+++ b/app/modules/Inspector/Application/InspectorBootloader.php
@@ -4,16 +4,9 @@
namespace Modules\Inspector\Application;
-use App\Application\Service\HttpHandler\HandlerRegistryInterface;
-use Modules\Inspector\Interfaces\Http\Handler\EventHandler;
use Spiral\Boot\Bootloader\Bootloader;
final class InspectorBootloader extends Bootloader
{
- public function boot(
- HandlerRegistryInterface $registry,
- EventHandler $handler
- ): void {
- $registry->register($handler);
- }
+
}
diff --git a/app/modules/Inspector/Interfaces/Http/Handler/EventHandler.php b/app/modules/Inspector/Interfaces/Http/Handler/EventHandler.php
index 451eee16..c0d65d37 100644
--- a/app/modules/Inspector/Interfaces/Http/Handler/EventHandler.php
+++ b/app/modules/Inspector/Interfaces/Http/Handler/EventHandler.php
@@ -35,12 +35,17 @@ public function handle(ServerRequestInterface $request, \Closure $next): Respons
?? throw new ClientException\BadRequestException('Invalid data');
$type = $data[0]['type'] ?? 'unknown';
- if ($type !== 'request') {
- throw new ClientException\BadRequestException('Invalid data');
- }
+
+ $data = match ($type) {
+ 'process',
+ 'request' => $data,
+ default => throw new ClientException\BadRequestException(
+ \sprintf('Invalid type "%s". [%s] expected.', $type, \implode(', ', ['process', 'request'])),
+ ),
+ };
$this->commands->dispatch(
- new HandleReceivedEvent(type: 'inspector', payload: $data)
+ new HandleReceivedEvent(type: 'inspector', payload: $data),
);
return $this->responseWrapper->create(200);
diff --git a/app/modules/Monolog/Interfaces/TCP/Service.php b/app/modules/Monolog/Interfaces/TCP/Service.php
index 66080056..b430a9f2 100644
--- a/app/modules/Monolog/Interfaces/TCP/Service.php
+++ b/app/modules/Monolog/Interfaces/TCP/Service.php
@@ -7,7 +7,6 @@
use App\Application\Commands\HandleReceivedEvent;
use Psr\Log\LoggerInterface;
use Spiral\Cqrs\CommandBusInterface;
-use Spiral\Exceptions\ExceptionReporterInterface;
use Spiral\RoadRunner\Tcp\Request;
use Spiral\RoadRunner\Tcp\TcpEvent;
use Spiral\RoadRunnerBridge\Tcp\Response\CloseConnection;
diff --git a/app/modules/Profiler/Application/ProfilerBootloader.php b/app/modules/Profiler/Application/ProfilerBootloader.php
index b068e0ab..bf61e3b7 100644
--- a/app/modules/Profiler/Application/ProfilerBootloader.php
+++ b/app/modules/Profiler/Application/ProfilerBootloader.php
@@ -4,9 +4,7 @@
namespace Modules\Profiler\Application;
-use App\Application\Service\HttpHandler\HandlerRegistryInterface;
use Modules\Profiler\Application\Handlers\CalculateDiffsBetweenEdges;
-use Modules\Profiler\Interfaces\Http\Handler\EventHandler as HttpEventHandler;
use Modules\Profiler\Application\Handlers\CleanupEvent;
use Modules\Profiler\Application\Handlers\PrepareEdges;
use Modules\Profiler\Application\Handlers\PreparePeaks;
@@ -15,24 +13,17 @@
final class ProfilerBootloader extends Bootloader
{
- protected const SINGLETONS = [
- EventHandlerInterface::class => [self::class, 'eventHandler'],
- ];
-
- public function boot(
- HandlerRegistryInterface $registry,
- HttpEventHandler $handler
- ): void {
- $registry->register($handler);
- }
-
- public function eventHandler(ContainerInterface $container): EventHandlerInterface
+ public function defineSingletons(): array
{
- return new EventHandler($container, [
- PreparePeaks::class,
- CalculateDiffsBetweenEdges::class,
- PrepareEdges::class,
- CleanupEvent::class,
- ]);
+ return [
+ EventHandlerInterface::class => static function (ContainerInterface $container): EventHandlerInterface {
+ return new EventHandler($container, [
+ PreparePeaks::class,
+ CalculateDiffsBetweenEdges::class,
+ PrepareEdges::class,
+ CleanupEvent::class,
+ ]);
+ },
+ ];
}
}
diff --git a/app/modules/Ray/Application/RayBootloader.php b/app/modules/Ray/Application/RayBootloader.php
index 8cada904..ce34ce07 100644
--- a/app/modules/Ray/Application/RayBootloader.php
+++ b/app/modules/Ray/Application/RayBootloader.php
@@ -13,21 +13,21 @@
final class RayBootloader extends Bootloader
{
- protected const SINGLETONS = [
- EventHandlerInterface::class => [self::class, 'eventHandler'],
- ];
+ public function defineSingletons(): array
+ {
+ return [
+ EventHandlerInterface::class => static function (ContainerInterface $container): EventHandlerInterface {
+ return new EventHandler($container, [
+ MergeEventsHandler::class,
+ ]);
+ },
+ ];
+ }
public function boot(
HandlerRegistryInterface $registry,
- HttpEventHandler $handler
+ HttpEventHandler $handler,
): void {
$registry->register($handler);
}
-
- public function eventHandler(ContainerInterface $container): EventHandlerInterface
- {
- return new EventHandler($container, [
- MergeEventsHandler::class,
- ]);
- }
}
diff --git a/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php b/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php
index 4c3973dd..73745f81 100644
--- a/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php
+++ b/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php
@@ -58,14 +58,15 @@ private function handleEvent(ServerRequestInterface $request): ResponseInterface
$this->cache->set($hash, 1, CarbonInterval::minute(5));
} elseif ($type === TypeEnum::ClearAll->value) {
$this->commands->dispatch(new ClearEvents(type: 'ray'));
+ return $this->responseWrapper->create(200);
}
$event = $this->handler->handle($event);
$this->commands->dispatch(
new HandleReceivedEvent(
- type: 'ray', payload: $event, uuid: Uuid::fromString($event['uuid'])
- )
+ type: 'ray', payload: $event, uuid: Uuid::fromString($event['uuid']),
+ ),
);
return $this->responseWrapper->create(200);
@@ -86,9 +87,11 @@ private function handleLocks(ServerRequestInterface $request): ResponseInterface
private function isValidRequest(ServerRequestInterface $request): bool
{
+ $userAgent = $request->getServerParams()['HTTP_USER_AGENT'] ?? '';
+
return $request->getHeaderLine('X-Buggregator-Event') === 'ray'
|| $request->getAttribute('event-type') === 'ray'
- || \str_starts_with($request->getUri()->getPath(), 'Ray')
- || $request->getUri()->getUserInfo() === 'ray';
+ || $request->getUri()->getUserInfo() === 'ray'
+ || \str_starts_with($userAgent, 'Ray');
}
}
diff --git a/app/modules/Sentry/Application/PayloadParser.php b/app/modules/Sentry/Application/PayloadParser.php
new file mode 100644
index 00000000..378bd283
--- /dev/null
+++ b/app/modules/Sentry/Application/PayloadParser.php
@@ -0,0 +1,37 @@
+getHeaderLine('Content-Type') === 'application/x-sentry-envelope' ||
+ \str_contains($request->getHeaderLine('X-Sentry-Auth'), 'sentry.php/4');
+
+ if ($isV4) {
+ if ($request->getHeaderLine('Content-Encoding') === 'gzip') {
+ return \iterator_to_array($this->gzippedStreamFactory->createFromRequest($request)->getPayload());
+ }
+
+ $payloads = \explode("\n", (string)$request->getBody());
+
+ return \array_map(
+ static fn(string $payload): array => \json_decode($payload, true),
+ \array_filter($payloads),
+ );
+ }
+
+ return [$request->getParsedBody()];
+ }
+}
diff --git a/app/modules/Sentry/Application/SentryBootloader.php b/app/modules/Sentry/Application/SentryBootloader.php
index 848d5641..037d5f9f 100644
--- a/app/modules/Sentry/Application/SentryBootloader.php
+++ b/app/modules/Sentry/Application/SentryBootloader.php
@@ -4,30 +4,18 @@
namespace Modules\Sentry\Application;
-use App\Application\Service\HttpHandler\HandlerRegistryInterface;
use Modules\Sentry\EventHandler;
-use Modules\Sentry\Interfaces\Http\Handler\EventHandler as HttpEventHandler;
-use Modules\Sentry\Interfaces\Http\Handler\JsEventHandler;
use Psr\Container\ContainerInterface;
use Spiral\Boot\Bootloader\Bootloader;
final class SentryBootloader extends Bootloader
{
- protected const SINGLETONS = [
- EventHandlerInterface::class => [self::class, 'eventHandler'],
- ];
-
- public function boot(
- HandlerRegistryInterface $registry,
- HttpEventHandler $handler,
- JsEventHandler $jsHandler
- ): void {
- $registry->register($handler);
- $registry->register($jsHandler);
- }
-
- public function eventHandler(ContainerInterface $container): EventHandlerInterface
+ public function defineSingletons(): array
{
- return new EventHandler($container, []);
+ return [
+ EventHandlerInterface::class => static function (ContainerInterface $container): EventHandlerInterface {
+ return new EventHandler($container, []);
+ },
+ ];
}
}
diff --git a/app/modules/Sentry/Interfaces/Http/Handler/EventHandler.php b/app/modules/Sentry/Interfaces/Http/Handler/EventHandler.php
index e24a3e19..15523e4e 100644
--- a/app/modules/Sentry/Interfaces/Http/Handler/EventHandler.php
+++ b/app/modules/Sentry/Interfaces/Http/Handler/EventHandler.php
@@ -5,9 +5,9 @@
namespace Modules\Sentry\Interfaces\Http\Handler;
use App\Application\Commands\HandleReceivedEvent;
-use App\Application\HTTP\GzippedStreamFactory;
use App\Application\Service\HttpHandler\HandlerInterface;
use Modules\Sentry\Application\EventHandlerInterface;
+use Modules\Sentry\Application\PayloadParser;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\Cqrs\CommandBusInterface;
@@ -16,7 +16,7 @@
final class EventHandler implements HandlerInterface
{
public function __construct(
- private readonly GzippedStreamFactory $gzippedStreamFactory,
+ private readonly PayloadParser $payloadParser,
private readonly ResponseWrapper $responseWrapper,
private readonly EventHandlerInterface $handler,
private readonly CommandBusInterface $commands,
@@ -35,7 +35,8 @@ public function handle(ServerRequestInterface $request, \Closure $next): Respons
}
$url = \rtrim($request->getUri()->getPath(), '/');
- $payloads = $this->gzippedStreamFactory->createFromRequest($request)->getPayload();
+
+ $payloads = $this->payloadParser->parse($request);
match (true) {
\str_ends_with($url, '/envelope') => $this->handleEnvelope($payloads),
@@ -46,42 +47,35 @@ public function handle(ServerRequestInterface $request, \Closure $next): Respons
return $this->responseWrapper->create(200);
}
- private function handleEvent(\Traversable $data): void
+ private function handleEvent(array $data): void
{
- $data = \iterator_to_array($data);
-
$event = $this->handler->handle($data[0]);
$this->commands->dispatch(
- new HandleReceivedEvent(type: 'sentry', payload: $event)
+ new HandleReceivedEvent(type: 'sentry', payload: $event),
);
}
/**
* TODO handle sentry transaction and session
*/
- private function handleEnvelope(\Traversable $data): void
+ private function handleEnvelope(array $data): void
{
- $data = \iterator_to_array($data);
-
if (\count($data) == 3) {
match ($data[1]['type']) {
'transaction' => null,
'session' => null,
+ 'event' => $this->handleEvent([$data[2]]),
+ default => null,
};
}
}
private function isValidRequest(ServerRequestInterface $request): bool
{
- if ($request->getHeaderLine('Content-Encoding') !== 'gzip') {
- return false;
- }
-
return $request->getHeaderLine('X-Buggregator-Event') === 'sentry'
|| $request->getAttribute('event-type') === 'sentry'
|| $request->hasHeader('X-Sentry-Auth')
- || $request->getUri()->getUserInfo() === 'sentry'
- || (string)$request->getUri() === 'profiler/store';
+ || $request->getUri()->getUserInfo() === 'sentry';
}
}
diff --git a/app/src/Application/AppDirectories.php b/app/src/Application/AppDirectories.php
index b98dfe6e..ce494282 100644
--- a/app/src/Application/AppDirectories.php
+++ b/app/src/Application/AppDirectories.php
@@ -10,8 +10,7 @@ final class AppDirectories
{
public function __construct(
private readonly DirectoriesInterface $directories
- ) {
- }
+ ) {}
/**
* Application root directory.
diff --git a/app/src/Application/Bootloader/AppBootloader.php b/app/src/Application/Bootloader/AppBootloader.php
index 374440f1..478ca44e 100644
--- a/app/src/Application/Bootloader/AppBootloader.php
+++ b/app/src/Application/Bootloader/AppBootloader.php
@@ -12,13 +12,19 @@
final class AppBootloader extends DomainBootloader
{
- protected const SINGLETONS = [
- CoreInterface::class => [self::class, 'domainCore'],
- ];
+ public function defineSingletons(): array
+ {
+ return [
+ CoreInterface::class => [self::class, 'domainCore'],
+ ];
+ }
- protected const INTERCEPTORS = [
- StringToIntParametersInterceptor::class,
- UuidParametersConverterInterceptor::class,
- JsonResourceInterceptor::class,
- ];
+ protected static function defineInterceptors(): array
+ {
+ return [
+ StringToIntParametersInterceptor::class,
+ UuidParametersConverterInterceptor::class,
+ JsonResourceInterceptor::class,
+ ];
+ }
}
diff --git a/app/src/Application/Bootloader/ExceptionHandlerBootloader.php b/app/src/Application/Bootloader/ExceptionHandlerBootloader.php
index 72bb3be6..22f05cdd 100644
--- a/app/src/Application/Bootloader/ExceptionHandlerBootloader.php
+++ b/app/src/Application/Bootloader/ExceptionHandlerBootloader.php
@@ -19,10 +19,13 @@
final class ExceptionHandlerBootloader extends Bootloader
{
- protected const BINDINGS = [
- SuppressErrorsInterface::class => EnvSuppressErrors::class,
- RendererInterface::class => PlainRenderer::class,
- ];
+ public function defineBindings(): array
+ {
+ return [
+ SuppressErrorsInterface::class => EnvSuppressErrors::class,
+ RendererInterface::class => PlainRenderer::class,
+ ];
+ }
public function init(AbstractKernel $kernel): void
{
diff --git a/app/src/Application/Bootloader/HttpHandlerBootloader.php b/app/src/Application/Bootloader/HttpHandlerBootloader.php
index 9efb9132..ff431db1 100644
--- a/app/src/Application/Bootloader/HttpHandlerBootloader.php
+++ b/app/src/Application/Bootloader/HttpHandlerBootloader.php
@@ -7,28 +7,32 @@
use App\Application\Service\HttpHandler\CoreHandlerInterface;
use App\Application\Service\HttpHandler\HandlerPipeline;
use App\Application\Service\HttpHandler\HandlerRegistryInterface;
-use App\Interfaces\Http\FrontendRequest;
+use App\Interfaces\Http\Handler\FrontendRequest;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Boot\DirectoriesInterface;
+use Spiral\Core\FactoryInterface;
+use Spiral\Tokenizer\TokenizerListenerRegistryInterface;
final class HttpHandlerBootloader extends Bootloader
{
- protected const SINGLETONS = [
- HandlerPipeline::class => [self::class, 'initHandlerPipeline'],
- HandlerRegistryInterface::class => HandlerPipeline::class,
- CoreHandlerInterface::class => HandlerPipeline::class,
- ];
-
- private function initHandlerPipeline(DirectoriesInterface $dirs): HandlerPipeline
+ public function defineSingletons(): array
{
- $pipeline = new HandlerPipeline();
-
- $pipeline->register(
- new FrontendRequest(
- $dirs->get('public')
- )
- );
+ return [
+ HandlerPipeline::class => static function (FactoryInterface $factory): HandlerPipeline {
+ return new HandlerPipeline(factory: $factory);
+ },
+ HandlerRegistryInterface::class => HandlerPipeline::class,
+ CoreHandlerInterface::class => HandlerPipeline::class,
+ FrontendRequest::class => static function (DirectoriesInterface $dirs): FrontendRequest {
+ return new FrontendRequest(
+ $dirs->get('public'),
+ );
+ },
+ ];
+ }
- return $pipeline;
+ public function init(TokenizerListenerRegistryInterface $tokenizerRegistry, HandlerPipeline $pipeline): void
+ {
+ $tokenizerRegistry->addListener($pipeline);
}
}
diff --git a/app/src/Application/Bootloader/MongoDBBootloader.php b/app/src/Application/Bootloader/MongoDBBootloader.php
index 9c105e2c..f81d9c08 100644
--- a/app/src/Application/Bootloader/MongoDBBootloader.php
+++ b/app/src/Application/Bootloader/MongoDBBootloader.php
@@ -6,29 +6,25 @@
use MongoDB\Client;
use MongoDB\Database;
-use MongoDB\Driver\ServerApi;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Boot\EnvironmentInterface;
final class MongoDBBootloader extends Bootloader
{
- protected const SINGLETONS = [
- Client::class => [self::class, 'createClient'],
- Database::class => [self::class, 'selectDatabase'],
- ];
-
- private function createClient(EnvironmentInterface $env): Client
- {
- return new Client(
- $env->get('MONGODB_CONNECTION')
- );
- }
-
- private function selectDatabase(Client $client, EnvironmentInterface $env): Database
+ public function defineSingletons(): array
{
- $database = $client->selectDatabase($env->get('MONGODB_DATABASE'));
- $database->command(['ping' => 1]);
+ return [
+ Client::class => static function (EnvironmentInterface $env): Client {
+ return new Client(
+ $env->get('MONGODB_CONNECTION'),
+ );
+ },
+ Database::class => static function (Client $client, EnvironmentInterface $env): Database {
+ $database = $client->selectDatabase($env->get('MONGODB_DATABASE'));
+ $database->command(['ping' => 1]);
- return $database;
+ return $database;
+ },
+ ];
}
}
diff --git a/app/src/Application/Bootloader/RoutesBootloader.php b/app/src/Application/Bootloader/RoutesBootloader.php
index c55e5ce3..45351b5d 100644
--- a/app/src/Application/Bootloader/RoutesBootloader.php
+++ b/app/src/Application/Bootloader/RoutesBootloader.php
@@ -4,14 +4,11 @@
namespace App\Application\Bootloader;
-use App\Application\Service\HttpHandler\CoreHandlerInterface;
use App\Interfaces\Http\EventHandlerAction;
-use Psr\Container\ContainerInterface;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader;
use Spiral\Filter\ValidationHandlerMiddleware;
use Spiral\Http\Middleware\ErrorHandlerMiddleware;
+use Spiral\Http\Middleware\JsonPayloadMiddleware;
use Spiral\Router\Bootloader\AnnotatedRoutesBootloader;
use Spiral\Router\GroupRegistry;
use Spiral\Router\Loader\Configurator\RoutingConfigurator;
@@ -25,6 +22,7 @@ final class RoutesBootloader extends BaseRoutesBootloader
protected function globalMiddleware(): array
{
return [
+ JsonPayloadMiddleware::class,
ErrorHandlerMiddleware::class,
ValidationHandlerMiddleware::class,
];
diff --git a/app/src/Application/Broadcasting/BroadcastEventInterceptor.php b/app/src/Application/Broadcasting/BroadcastEventInterceptor.php
index f5b698c2..16eaf790 100644
--- a/app/src/Application/Broadcasting/BroadcastEventInterceptor.php
+++ b/app/src/Application/Broadcasting/BroadcastEventInterceptor.php
@@ -12,8 +12,7 @@ final class BroadcastEventInterceptor implements CoreInterceptorInterface
{
public function __construct(
private readonly BroadcastInterface $broadcast
- ) {
- }
+ ) {}
public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed
{
diff --git a/app/src/Application/Broadcasting/Channel/Channel.php b/app/src/Application/Broadcasting/Channel/Channel.php
index a37ca15b..eeca81c6 100644
--- a/app/src/Application/Broadcasting/Channel/Channel.php
+++ b/app/src/Application/Broadcasting/Channel/Channel.php
@@ -8,8 +8,7 @@ class Channel implements \Stringable
{
public function __construct(
public readonly string $name
- ) {
- }
+ ) {}
public function __toString(): string
{
diff --git a/app/src/Application/Broadcasting/InMemoryDriver.php b/app/src/Application/Broadcasting/InMemoryDriver.php
new file mode 100644
index 00000000..3f72511c
--- /dev/null
+++ b/app/src/Application/Broadcasting/InMemoryDriver.php
@@ -0,0 +1,36 @@
+formatTopics($this->toArray($topics));
+
+ foreach ($topics as $topic) {
+ foreach ($this->toArray($messages) as $message) {
+ self::$published[$topic][] = \json_decode($message, true);
+ }
+ }
+ }
+
+ public function published(): array
+ {
+ return self::$published;
+ }
+
+ public function reset(): void
+ {
+ self::$published = [];
+ }
+}
diff --git a/app/src/Application/Commands/AskEvents.php b/app/src/Application/Commands/AskEvents.php
index 4ef6dbef..7762da3d 100644
--- a/app/src/Application/Commands/AskEvents.php
+++ b/app/src/Application/Commands/AskEvents.php
@@ -11,6 +11,5 @@ abstract class AskEvents implements QueryInterface
public function __construct(
public readonly ?string $type = null,
public readonly ?int $projectId = null,
- ) {
- }
+ ) {}
}
diff --git a/app/src/Application/Commands/ClearEvents.php b/app/src/Application/Commands/ClearEvents.php
index b6b38cb8..82f25f5f 100644
--- a/app/src/Application/Commands/ClearEvents.php
+++ b/app/src/Application/Commands/ClearEvents.php
@@ -10,6 +10,5 @@ class ClearEvents implements CommandInterface
{
public function __construct(
public readonly ?string $type = null
- ) {
- }
+ ) {}
}
diff --git a/app/src/Application/Commands/CountEvents.php b/app/src/Application/Commands/CountEvents.php
index fda8a4b8..3bf53e21 100644
--- a/app/src/Application/Commands/CountEvents.php
+++ b/app/src/Application/Commands/CountEvents.php
@@ -4,6 +4,4 @@
namespace App\Application\Commands;
-final class CountEvents extends AskEvents
-{
-}
+final class CountEvents extends AskEvents {}
diff --git a/app/src/Application/Commands/DeleteEvent.php b/app/src/Application/Commands/DeleteEvent.php
index 8839382f..947a1939 100644
--- a/app/src/Application/Commands/DeleteEvent.php
+++ b/app/src/Application/Commands/DeleteEvent.php
@@ -11,6 +11,5 @@ class DeleteEvent implements CommandInterface
{
public function __construct(
public readonly Uuid $uuid
- ) {
- }
+ ) {}
}
diff --git a/app/src/Application/Commands/FinUserByUsername.php b/app/src/Application/Commands/FinUserByUsername.php
index f2b03f3d..8803d1d4 100644
--- a/app/src/Application/Commands/FinUserByUsername.php
+++ b/app/src/Application/Commands/FinUserByUsername.php
@@ -10,6 +10,5 @@ class FinUserByUsername implements QueryInterface
{
public function __construct(
public readonly string $username
- ) {
- }
+ ) {}
}
diff --git a/app/src/Application/Commands/FindAllProjects.php b/app/src/Application/Commands/FindAllProjects.php
index 457b4c41..983bb705 100644
--- a/app/src/Application/Commands/FindAllProjects.php
+++ b/app/src/Application/Commands/FindAllProjects.php
@@ -6,6 +6,4 @@
use Spiral\Cqrs\QueryInterface;
-class FindAllProjects implements QueryInterface
-{
-}
+class FindAllProjects implements QueryInterface {}
diff --git a/app/src/Application/Commands/FindAllTransactions.php b/app/src/Application/Commands/FindAllTransactions.php
index e31dfe7d..e2ffc64e 100644
--- a/app/src/Application/Commands/FindAllTransactions.php
+++ b/app/src/Application/Commands/FindAllTransactions.php
@@ -6,6 +6,4 @@
use Spiral\Cqrs\QueryInterface;
-class FindAllTransactions implements QueryInterface
-{
-}
+class FindAllTransactions implements QueryInterface {}
diff --git a/app/src/Application/Commands/FindEventByUuid.php b/app/src/Application/Commands/FindEventByUuid.php
index 0a60db6d..59734681 100644
--- a/app/src/Application/Commands/FindEventByUuid.php
+++ b/app/src/Application/Commands/FindEventByUuid.php
@@ -11,6 +11,5 @@ class FindEventByUuid implements QueryInterface
{
public function __construct(
public readonly Uuid $uuid
- ) {
- }
+ ) {}
}
diff --git a/app/src/Application/Commands/FindEvents.php b/app/src/Application/Commands/FindEvents.php
index fee8b78b..b01bbb80 100644
--- a/app/src/Application/Commands/FindEvents.php
+++ b/app/src/Application/Commands/FindEvents.php
@@ -4,6 +4,4 @@
namespace App\Application\Commands;
-final class FindEvents extends AskEvents
-{
-}
+final class FindEvents extends AskEvents {}
diff --git a/app/src/Application/Commands/FindProjectByName.php b/app/src/Application/Commands/FindProjectByName.php
index 106d6793..6b145b2e 100644
--- a/app/src/Application/Commands/FindProjectByName.php
+++ b/app/src/Application/Commands/FindProjectByName.php
@@ -10,6 +10,5 @@ class FindProjectByName implements QueryInterface
{
public function __construct(
public readonly string $name
- ) {
- }
+ ) {}
}
diff --git a/app/src/Application/Commands/FindTransactionByName.php b/app/src/Application/Commands/FindTransactionByName.php
index 9f8818bd..c6f3b223 100644
--- a/app/src/Application/Commands/FindTransactionByName.php
+++ b/app/src/Application/Commands/FindTransactionByName.php
@@ -10,6 +10,5 @@ class FindTransactionByName implements QueryInterface
{
public function __construct(
public readonly string $name
- ) {
- }
+ ) {}
}
diff --git a/app/src/Application/Commands/HandleReceivedEvent.php b/app/src/Application/Commands/HandleReceivedEvent.php
index d17997bf..82d885fc 100644
--- a/app/src/Application/Commands/HandleReceivedEvent.php
+++ b/app/src/Application/Commands/HandleReceivedEvent.php
@@ -10,16 +10,16 @@
final class HandleReceivedEvent implements CommandInterface, \JsonSerializable
{
public readonly Uuid $uuid;
- public readonly int $timestamp;
+ public readonly float $timestamp;
public function __construct(
public readonly string $type,
public readonly array $payload,
public readonly ?string $project = null,
- ?Uuid $uuid = null
+ ?Uuid $uuid = null,
) {
$this->uuid = $uuid ?? Uuid::generate();
- $this->timestamp = time();
+ $this->timestamp = microtime(true);
}
public function jsonSerialize(): array
diff --git a/app/src/Application/Domain/Entity/Json.php b/app/src/Application/Domain/Entity/Json.php
index 296b9d37..5243ad85 100644
--- a/app/src/Application/Domain/Entity/Json.php
+++ b/app/src/Application/Domain/Entity/Json.php
@@ -10,8 +10,7 @@ final class Json implements \JsonSerializable
{
public function __construct(
private readonly array $data = []
- ) {
- }
+ ) {}
public static function cast(string $value, DatabaseInterface $db): self
{
diff --git a/app/src/Application/Domain/ValueObjects/Uuid.php b/app/src/Application/Domain/ValueObjects/Uuid.php
index 0ea9ffe8..515d5837 100644
--- a/app/src/Application/Domain/ValueObjects/Uuid.php
+++ b/app/src/Application/Domain/ValueObjects/Uuid.php
@@ -16,7 +16,7 @@ public static function generate(): self
public function __construct(private ?UuidInterface $uuid = null)
{
if (!$uuid) {
- $this->uuid = \Ramsey\Uuid\Uuid::uuid4();
+ $this->uuid = \Ramsey\Uuid\Uuid::uuid7();
}
}
diff --git a/app/src/Application/Exception/EntityNotFoundException.php b/app/src/Application/Exception/EntityNotFoundException.php
index 84801e9c..f742432a 100644
--- a/app/src/Application/Exception/EntityNotFoundException.php
+++ b/app/src/Application/Exception/EntityNotFoundException.php
@@ -4,7 +4,4 @@
namespace App\Application\Exception;
-class EntityNotFoundException extends \DomainException
-{
-}
-
+class EntityNotFoundException extends \DomainException {}
diff --git a/app/src/Application/HTTP/GzippedStream.php b/app/src/Application/HTTP/GzippedStream.php
index fd3788c5..b84b58f9 100644
--- a/app/src/Application/HTTP/GzippedStream.php
+++ b/app/src/Application/HTTP/GzippedStream.php
@@ -10,8 +10,7 @@ final class GzippedStream
{
public function __construct(
private readonly StreamInterface $stream,
- ) {
- }
+ ) {}
public function getPayload(): \Traversable
{
diff --git a/app/src/Application/HTTP/Interceptor/JsonResourceInterceptor.php b/app/src/Application/HTTP/Interceptor/JsonResourceInterceptor.php
index 6162ad18..bf326636 100644
--- a/app/src/Application/HTTP/Interceptor/JsonResourceInterceptor.php
+++ b/app/src/Application/HTTP/Interceptor/JsonResourceInterceptor.php
@@ -4,28 +4,36 @@
namespace App\Application\HTTP\Interceptor;
+use App\Application\HTTP\Response\ErrorResource;
use App\Application\HTTP\Response\ResourceInterface;
+use App\Application\HTTP\Response\ValidationResource;
use Psr\Http\Message\ResponseFactoryInterface;
use Spiral\Core\CoreInterceptorInterface;
use Spiral\Core\CoreInterface;
-use Spiral\Http\Request\InputManager;
+use Spiral\Exceptions\ExceptionHandlerInterface;
+use Spiral\Filters\Exception\ValidationException;
final class JsonResourceInterceptor implements CoreInterceptorInterface
{
public function __construct(
- private readonly InputManager $manager,
- private readonly ResponseFactoryInterface $responseFactory
- ) {
- }
+ private readonly ResponseFactoryInterface $responseFactory,
+ private readonly ExceptionHandlerInterface $exceptionHandler,
+ ) {}
public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed
{
- $response = $core->callAction($controller, $action, $parameters);
+ try {
+ $response = $core->callAction($controller, $action, $parameters);
+ } catch (ValidationException $e) {
+ $response = new ValidationResource($e);
+ } catch (\Throwable $e) {
+ $this->exceptionHandler->report($e);
+ $response = new ErrorResource($e);
+ }
if ($response instanceof ResourceInterface) {
$response = $response->toResponse(
- $this->manager->request(),
- $this->responseFactory->createResponse()
+ $this->responseFactory->createResponse(),
);
}
diff --git a/app/src/Application/HTTP/Response/ErrorResource.php b/app/src/Application/HTTP/Response/ErrorResource.php
new file mode 100644
index 00000000..07fa0d3c
--- /dev/null
+++ b/app/src/Application/HTTP/Response/ErrorResource.php
@@ -0,0 +1,36 @@
+ $this->data->getMessage(),
+ 'code' => $this->getCode(),
+ ];
+ }
+
+ protected function getCode(): int
+ {
+ return match (true) {
+ $this->data instanceof EntityNotFoundException => 404,
+ $this->data instanceof ClientException => $this->data->getCode(),
+ default => 500,
+ };
+ }
+}
diff --git a/app/src/Application/HTTP/Response/JsonResource.php b/app/src/Application/HTTP/Response/JsonResource.php
index ae86506e..847c207d 100644
--- a/app/src/Application/HTTP/Response/JsonResource.php
+++ b/app/src/Application/HTTP/Response/JsonResource.php
@@ -6,39 +6,32 @@
use JsonSerializable;
use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
use Spiral\Http\Traits\JsonTrait;
-class JsonResource implements ResourceInterface, \ArrayAccess
+class JsonResource implements ResourceInterface
{
use JsonTrait;
protected readonly mixed $data;
- public function __construct(mixed $data)
+ public function __construct(mixed $data = [])
{
$this->data = $data;
}
- protected function mapData(ServerRequestInterface $request): array|JsonSerializable
+ protected function mapData(): array|JsonSerializable
{
return $this->data;
}
- public function resolve(ServerRequestInterface $request): array
+ protected function getCode(): int
{
- $data = $this->mapData($request);
-
- if ($data instanceof JsonSerializable) {
- $data = $data->jsonSerialize();
- }
-
- return $this->wrapData($data);
+ return 200;
}
- public function toResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+ public function toResponse(ResponseInterface $response): ResponseInterface
{
- return $this->writeJson($response, $this->resolve($request));
+ return $this->writeJson($response, $this, $this->getCode());
}
protected function wrapData(array $data): array
@@ -46,33 +39,20 @@ protected function wrapData(array $data): array
return $data;
}
- public function offsetExists(mixed $offset): bool
+ public function jsonSerialize(): array
{
- return isset($this->data[$offset]);
- }
-
- public function offsetGet(mixed $offset): mixed
- {
- return $this->data[$offset];
- }
+ $data = $this->mapData();
- public function offsetSet(mixed $offset, mixed $value): void
- {
- throw new \RuntimeException('Resource is read-only');
- }
-
- public function offsetUnset(mixed $offset): void
- {
- throw new \RuntimeException('Resource is read-only');
- }
+ if ($data instanceof JsonSerializable) {
+ $data = $data->jsonSerialize();
+ }
- public function __isset($key)
- {
- return $this->offsetExists($key);
- }
+ foreach ($data as $key => $value) {
+ if ($value instanceof ResourceInterface) {
+ $data[$key] = $value->jsonSerialize();
+ }
+ }
- public function __get($key)
- {
- return $this->offsetGet($key);
+ return $this->wrapData($data);
}
}
diff --git a/app/src/Application/HTTP/Response/ResourceCollection.php b/app/src/Application/HTTP/Response/ResourceCollection.php
index c99d5317..787c866f 100644
--- a/app/src/Application/HTTP/Response/ResourceCollection.php
+++ b/app/src/Application/HTTP/Response/ResourceCollection.php
@@ -5,7 +5,6 @@
namespace App\Application\HTTP\Response;
use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
use Spiral\DataGrid\GridInterface;
use Spiral\Http\Traits\JsonTrait;
@@ -13,13 +12,17 @@ class ResourceCollection implements ResourceInterface
{
use JsonTrait;
+ private readonly array $args;
+
/**
* @param class-string $resourceClass
*/
public function __construct(
protected readonly iterable $data,
- protected string $resourceClass = JsonResource::class
+ protected string $resourceClass = JsonResource::class,
+ mixed ...$args
) {
+ $this->args = $args;
}
/**
@@ -35,21 +38,30 @@ protected function getData(): iterable
return $this->data;
}
- public function resolve(ServerRequestInterface $request): array
+ public function jsonSerialize(): array
{
$data = [];
- $resourceClass = $this->getResourceClass();
+ $resource = $this->getResourceClass();
foreach ($this->getData() as $key => $row) {
- $data[$key] = (new $resourceClass($row))->resolve($request);
+ if ($row instanceof \JsonSerializable) {
+ $data[$key] = $row;
+ continue;
+ }
+
+ if (\is_string($resource)) {
+ $resource = static fn(mixed $row, mixed ...$args): ResourceInterface => new $resource($row, ...$args);
+ }
+
+ $data[$key] = $resource($row, ...$this->args);
}
return $this->wrapData($data);
}
- public function toResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+ public function toResponse(ResponseInterface $response): ResponseInterface
{
- return $this->writeJson($response, $this->resolve($request));
+ return $this->writeJson($response, $this);
}
protected function wrapData(array $data): array
diff --git a/app/src/Application/HTTP/Response/ResourceInterface.php b/app/src/Application/HTTP/Response/ResourceInterface.php
index a4dfa83f..d8741918 100644
--- a/app/src/Application/HTTP/Response/ResourceInterface.php
+++ b/app/src/Application/HTTP/Response/ResourceInterface.php
@@ -7,9 +7,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-interface ResourceInterface
+interface ResourceInterface extends \JsonSerializable
{
- public function resolve(ServerRequestInterface $request): array;
-
- public function toResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface;
+ public function toResponse(ResponseInterface $response): ResponseInterface;
}
diff --git a/app/src/Application/HTTP/Response/SuccessResource.php b/app/src/Application/HTTP/Response/SuccessResource.php
new file mode 100644
index 00000000..c7675b8b
--- /dev/null
+++ b/app/src/Application/HTTP/Response/SuccessResource.php
@@ -0,0 +1,23 @@
+ $this->data,
+ ];
+ }
+}
diff --git a/app/src/Application/HTTP/Response/ValidationResource.php b/app/src/Application/HTTP/Response/ValidationResource.php
new file mode 100644
index 00000000..bf4a1d97
--- /dev/null
+++ b/app/src/Application/HTTP/Response/ValidationResource.php
@@ -0,0 +1,33 @@
+ $this->data->getMessage(),
+ 'code' => $this->getCode(),
+ 'errors' => $this->data->errors,
+ 'context' => $this->data->context,
+ ];
+ }
+
+ protected function getCode(): int
+ {
+ return $this->data->getCode();
+ }
+}
diff --git a/app/src/Application/Kernel.php b/app/src/Application/Kernel.php
index 14d28aaf..bf5f2a7e 100644
--- a/app/src/Application/Kernel.php
+++ b/app/src/Application/Kernel.php
@@ -90,12 +90,7 @@ protected function defineBootloaders(): array
StorageBootloader::class,
DistributionBootloader::class,
- ];
- }
- protected function defineAppBootloaders(): array
- {
- return [
HttpHandlerBootloader::class,
AppBootloader::class,
InspectorBootloader::class,
diff --git a/app/src/Application/Persistence/CacheEventRepository.php b/app/src/Application/Persistence/CacheEventRepository.php
index 134260e9..0013e216 100644
--- a/app/src/Application/Persistence/CacheEventRepository.php
+++ b/app/src/Application/Persistence/CacheEventRepository.php
@@ -30,7 +30,7 @@ final class CacheEventRepository implements EventRepositoryInterface
public function __construct(
CacheStorageProviderInterface $provider,
- private readonly int $ttl = 60 * 60 * 2
+ private readonly int $ttl = 60 * 60 * 2,
) {
$this->cache = $provider->storage('events');
}
@@ -66,7 +66,7 @@ public function store(Event $event): bool
'id' => $id,
'type' => $event->getType(),
'project_id' => $event->getProjectId(),
- 'date' => $event->getDate()->getTimestamp(),
+ 'date' => $event->getTimestamp(),
'payload' => $event->getPayload()->jsonSerialize(),
], Carbon::now()->addSeconds($this->ttl)->diffAsCarbonInterval());
}
@@ -159,7 +159,7 @@ private function mapDocumentInfoEvent(array $document): Event
uuid: Uuid::fromString($document['id']),
type: $document['type'],
payload: new Json((array)$document['payload']),
- date: Carbon::createFromTimestamp($document['date'])->toDateTimeImmutable(),
+ timestamp: $document['date'],
projectId: $document['project_id'],
);
}
diff --git a/app/src/Application/Persistence/MongoDBEventRepository.php b/app/src/Application/Persistence/MongoDBEventRepository.php
index c699996d..4dfcac89 100644
--- a/app/src/Application/Persistence/MongoDBEventRepository.php
+++ b/app/src/Application/Persistence/MongoDBEventRepository.php
@@ -15,8 +15,7 @@ final class MongoDBEventRepository implements EventRepositoryInterface
{
public function __construct(
private readonly Collection $collection,
- ) {
- }
+ ) {}
public function store(Event $event): bool
{
@@ -24,7 +23,7 @@ public function store(Event $event): bool
'_id' => (string)$event->getUuid(),
'type' => $event->getType(),
'project_id' => $event->getProjectId(),
- 'date' => $event->getDate()->getTimestamp(),
+ 'date' => $event->getTimestamp(),
'payload' => $event->getPayload()->jsonSerialize(),
]);
@@ -82,8 +81,8 @@ public function mapDocumentInfoEvent(\MongoDB\Model\BSONDocument $document): Eve
return new Event(
uuid: Uuid::fromString($document['_id']),
type: $document['type'],
- payload: new Json((array) $document['payload']),
- date: Carbon::createFromTimestamp($document['date'])->toDateTimeImmutable(),
+ payload: new Json((array)$document['payload']),
+ timestamp: $document['date'],
projectId: $document['project_id'],
);
}
diff --git a/app/src/Application/Service/HttpHandler/HandlerPipeline.php b/app/src/Application/Service/HttpHandler/HandlerPipeline.php
index 1542d873..8877d5bd 100644
--- a/app/src/Application/Service/HttpHandler/HandlerPipeline.php
+++ b/app/src/Application/Service/HttpHandler/HandlerPipeline.php
@@ -7,14 +7,22 @@
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Spiral\Core\FactoryInterface;
+use Spiral\Tokenizer\Attribute\TargetClass;
+use Spiral\Tokenizer\TokenizationListenerInterface;
-final class HandlerPipeline implements HandlerRegistryInterface, CoreHandlerInterface
+#[TargetClass(class: HandlerInterface::class)]
+final class HandlerPipeline implements HandlerRegistryInterface, CoreHandlerInterface, TokenizationListenerInterface
{
/** @var HandlerInterface[] */
private array $handlers = [];
private int $position = 0;
private bool $isHandled = false;
+ public function __construct(
+ private readonly FactoryInterface $factory,
+ ) {}
+
public function register(HandlerInterface $handler): void
{
if ($this->isHandled) {
@@ -55,7 +63,17 @@ private function handlePipeline(ServerRequestInterface $request): ResponseInterf
return $handler->handle(
$request,
- fn(ServerRequestInterface $request) => $this->handlePipeline($request)
+ fn(ServerRequestInterface $request) => $this->handlePipeline($request),
);
}
+
+ public function listen(\ReflectionClass $class): void
+ {
+ $this->register($this->factory->make($class->getName()));
+ }
+
+ public function finalize(): void
+ {
+ // TODO: Implement finalize() method.
+ }
}
diff --git a/app/src/Application/TCP/ExceptionHandlerInterceptor.php b/app/src/Application/TCP/ExceptionHandlerInterceptor.php
index 15e3bc0d..7f935670 100644
--- a/app/src/Application/TCP/ExceptionHandlerInterceptor.php
+++ b/app/src/Application/TCP/ExceptionHandlerInterceptor.php
@@ -14,8 +14,7 @@ final class ExceptionHandlerInterceptor implements CoreInterceptorInterface
{
public function __construct(
private readonly ExceptionReporterInterface $reporter
- ) {
- }
+ ) {}
public function process(string $controller, string $action, array $parameters, CoreInterface $core): ResponseInterface
{
diff --git a/app/src/Interfaces/Centrifugo/RPCService.php b/app/src/Interfaces/Centrifugo/RPCService.php
index bb04b573..a4b58383 100644
--- a/app/src/Interfaces/Centrifugo/RPCService.php
+++ b/app/src/Interfaces/Centrifugo/RPCService.php
@@ -17,8 +17,7 @@ final class RPCService implements ServiceInterface
public function __construct(
private readonly Http $http,
private readonly ServerRequestFactoryInterface $requestFactory,
- ) {
- }
+ ) {}
/**
* @param Request\RPC $request
@@ -60,10 +59,6 @@ public function createHttpRequest(Request\RPC $request): ServerRequestInterface
$httpRequest = $this->requestFactory->createServerRequest(\strtoupper($method), \ltrim($uri, '/'))
->withHeader('Content-Type', 'application/json');
-// foreach ($request->headers as $key => $headers) {
-// $httpRequest = $httpRequest->withHeader($key, $headers);
-// }
-
return match ($method) {
'GET', 'HEAD' => $httpRequest->withQueryParams($request->getData()),
'POST', 'PUT', 'DELETE' => $httpRequest->withParsedBody($request->getData()),
diff --git a/app/src/Interfaces/Http/GetVersionAction.php b/app/src/Interfaces/Http/Controller/GetVersionAction.php
similarity index 92%
rename from app/src/Interfaces/Http/GetVersionAction.php
rename to app/src/Interfaces/Http/Controller/GetVersionAction.php
index e5d20f31..7d1642c3 100644
--- a/app/src/Interfaces/Http/GetVersionAction.php
+++ b/app/src/Interfaces/Http/Controller/GetVersionAction.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace App\Interfaces\Http;
+namespace App\Interfaces\Http\Controller;
use App\Application\HTTP\Response\JsonResource;
use App\Application\HTTP\Response\ResourceInterface;
diff --git a/app/src/Interfaces/Http/FrontendRequest.php b/app/src/Interfaces/Http/Handler/FrontendRequest.php
similarity index 97%
rename from app/src/Interfaces/Http/FrontendRequest.php
rename to app/src/Interfaces/Http/Handler/FrontendRequest.php
index 3ff8db71..607e902b 100644
--- a/app/src/Interfaces/Http/FrontendRequest.php
+++ b/app/src/Interfaces/Http/Handler/FrontendRequest.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace App\Interfaces\Http;
+namespace App\Interfaces\Http\Handler;
use App\Application\Service\HttpHandler\HandlerInterface;
use GuzzleHttp\Psr7\MimeType;
@@ -24,8 +24,7 @@ final class FrontendRequest implements HandlerInterface
public function __construct(
private readonly string $publicPath,
- ) {
- }
+ ) {}
public function priority(): int
{
diff --git a/composer.json b/composer.json
index 1a810e51..6d98bb4b 100644
--- a/composer.json
+++ b/composer.json
@@ -38,7 +38,7 @@
"spiral-packages/league-event": "^1.0",
"spiral/cycle-bridge": "^2.5",
"spiral/data-grid": "^3.0",
- "spiral/framework": "^3.8",
+ "spiral/framework": "^3.10",
"spiral/nyholm-bridge": "^1.3",
"spiral/roadrunner-bridge": "^3.0",
"spiral/validator": "^1.1",
@@ -49,8 +49,9 @@
"require-dev": {
"phpunit/phpunit": "^9.5",
"qossmic/deptrac-shim": "^1.0",
- "spiral/testing": "^2.2",
- "vimeo/psalm": "^4.1"
+ "spiral/testing": "^2.6",
+ "friendsofphp/php-cs-fixer": "^3.40",
+ "vimeo/psalm": "^5.16"
},
"autoload": {
"psr-4": {
@@ -66,7 +67,8 @@
"config": {
"sort-packages": true,
"allow-plugins": {
- "spiral/composer-publish-plugin": true
+ "spiral/composer-publish-plugin": true,
+ "php-http/discovery": false
}
},
"scripts": {
@@ -76,9 +78,9 @@
"php app.php configure -vv",
"rr get-binary"
],
- "rr:download": "rr get-binary",
- "rr:download-protoc": "rr download-protoc-binary",
- "psalm:config": "psalm",
+ "psalm": "vendor/bin/psalm --config=psalm.xml ./app",
+ "cs-check": "vendor/bin/php-cs-fixer fix ./app/src --rules=@PER-CS2.0 --dry-run",
+ "cs-fix": "vendor/bin/php-cs-fixer fix ./app/src --rules=@PER-CS2.0",
"deptrack": [
"deptrac analyze --report-uncovered"
]
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 3017df66..414d6efb 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,4 +1,4 @@
-ARG ROAD_RUNNER_IMAGE=2.12.3
+ARG ROAD_RUNNER_IMAGE=2023.3.7
ARG CENTRIFUGO_IMAGE=v4
ARG FRONTEND_IMAGE_TAG=latest
diff --git a/phpunit.xml b/phpunit.xml
index a875d9e7..42bb75ba 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -14,20 +14,19 @@
stopOnFailure="false"
stopOnError="false"
stderr="true">
-
-
- app/src
-
-
-
-
- tests/Unit
-
-
- tests/Feature
-
-
- app/src/Module/*/Test
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/Feature
+
+
diff --git a/tests/App/Broadcasting/BroadcastFaker.php b/tests/App/Broadcasting/BroadcastFaker.php
new file mode 100644
index 00000000..64b1099a
--- /dev/null
+++ b/tests/App/Broadcasting/BroadcastFaker.php
@@ -0,0 +1,107 @@
+getMessages());
+
+ return $this;
+ }
+
+ public function reset(): self
+ {
+ $this->container->get(InMemoryDriver::class)->reset();
+
+ return $this;
+ }
+
+ public function assertPushedTimes(string|\Stringable $topic, int $times = 1): array
+ {
+ $messages = $this->filterMessages((string)$topic);
+
+ TestCase::assertCount(
+ $times,
+ $messages,
+ \sprintf(
+ 'The expected message in topic [%s] was sent {%d} times instead of {%d} times.',
+ $topic,
+ \count($messages),
+ $times,
+ ),
+ );
+
+ return $messages;
+ }
+
+
+ public function assertPushed(string|\Stringable $topic, \Closure $callback = null): self
+ {
+ $messages = $this->filterMessages((string)$topic, $callback);
+
+ TestCase::assertTrue(
+ \count($messages) > 0,
+ \sprintf('The expected message [%s] was not pushed.', $topic),
+ );
+
+ return $this;
+ }
+
+ public function assertNotPushed(string|\Stringable $topic, \Closure $callback = null): self
+ {
+ $messages = $this->filterMessages((string)$topic, $callback);
+
+ TestCase::assertCount(
+ 0,
+ $messages,
+ \sprintf('The unexpected message [%s] was pushed.', $topic),
+ );
+
+ return $this;
+ }
+
+ public function assertNothingPushed(): self
+ {
+ $pushedMessages = $this->getMessages();
+ $messages = \implode(', ', \array_keys($this->getMessages()));
+
+ TestCase::assertCount(
+ 0,
+ $pushedMessages,
+ \sprintf('The following messages were pushed unexpectedly in the following topics: %s', $messages),
+ );
+
+ return $this;
+ }
+
+ private function getMessages(): array
+ {
+ return $this->container->get(InMemoryDriver::class)->published();
+ }
+
+ private function filterMessages(string $topic, \Closure $callback = null): array
+ {
+ $messages = $this->getMessages()[$topic] ?? [];
+
+ $callback = $callback ?: static function (array $data): bool {
+ return true;
+ };
+
+ return \array_filter($messages, static function (array $data) use ($callback) {
+ return $callback($data);
+ });
+ }
+}
diff --git a/tests/App/Events/EventExpectation.php b/tests/App/Events/EventExpectation.php
new file mode 100644
index 00000000..2b73b04d
--- /dev/null
+++ b/tests/App/Events/EventExpectation.php
@@ -0,0 +1,27 @@
+expectation->andReturn($event);
+ }
+
+ public function andThrowNotFound(): void
+ {
+ $this->expectation->andThrow(new EntityNotFoundException('Event not found'));
+ }
+}
diff --git a/tests/App/Events/EventsMocker.php b/tests/App/Events/EventsMocker.php
new file mode 100644
index 00000000..db1e2e3b
--- /dev/null
+++ b/tests/App/Events/EventsMocker.php
@@ -0,0 +1,44 @@
+events
+ ->shouldReceive('findByPK')
+ ->with((string)$uuid)
+ ->once(),
+ );
+ }
+
+ public function eventShouldBeDeleted(Uuid $uuid, bool $status = true): void
+ {
+ $this->events
+ ->shouldReceive('deleteByPK')
+ ->with((string)$uuid)
+ ->once()
+ ->andReturn($status);
+ }
+
+ public function eventShouldBeClear(?string $type = null): void
+ {
+ $this->events
+ ->shouldReceive('deleteAll')
+ ->with($type ? ['type' => $type] : [])
+ ->once();
+ }
+}
diff --git a/tests/App/Http/HttpFaker.php b/tests/App/Http/HttpFaker.php
new file mode 100644
index 00000000..8512ad99
--- /dev/null
+++ b/tests/App/Http/HttpFaker.php
@@ -0,0 +1,77 @@
+date = Carbon::create(2021, 1, 1, 0, 0, 0);
+ }
+
+ public function showEvent(Uuid $uuid): ResponseAssertions
+ {
+ return $this->makeResponse(
+ $this->http->getJson(uri: '/api/event/' . $uuid),
+ );
+ }
+
+ public function deleteEvent(Uuid $uuid): ResponseAssertions
+ {
+ return $this->makeResponse(
+ $this->http->deleteJson(uri: '/api/event/' . $uuid),
+ );
+ }
+
+ public function clearEvents(?string $type = null): ResponseAssertions
+ {
+ return $this->makeResponse(
+ $this->http->deleteJson(
+ uri: '/api/events/',
+ data: $type ? ['type' => $type] : [],
+ ),
+ );
+ }
+
+ public function __call(string $name, array $arguments): ResponseAssertions
+ {
+ if (!method_exists($this->http, $name)) {
+ throw new \Exception("Method $name does not exist");
+ }
+
+ return $this->makeResponse(
+ $this->http->$name(...$arguments),
+ );
+ }
+
+ private function makeResponse(TestResponse $response): ResponseAssertions
+ {
+ if ($this->dumpResponse) {
+ $body = (string)$response;
+
+ try {
+ $body = \json_decode($body, true, 512, JSON_THROW_ON_ERROR);
+ } catch (\Throwable) {
+ }
+ }
+
+ return new ResponseAssertions($response);
+ }
+}
diff --git a/tests/App/Http/ResponseAssertions.php b/tests/App/Http/ResponseAssertions.php
new file mode 100644
index 00000000..c7afdac0
--- /dev/null
+++ b/tests/App/Http/ResponseAssertions.php
@@ -0,0 +1,207 @@
+response);
+
+ return $this;
+ }
+
+ public function assertNotFoundResource(string $message = 'Not found'): self
+ {
+ return $this
+ ->assertNotFound()
+ ->assertBodySame(
+ \json_encode([
+ 'message' => $message,
+ 'code' => 404,
+ ]),
+ );
+ }
+
+ public function assertResource(ResourceInterface $resource): self
+ {
+ $needle = \json_encode($resource);
+ TestCase::assertSame(
+ $needle,
+ (string)$this->response,
+ \sprintf('Response is not same with [%s]', $needle),
+ );
+
+ return $this;
+ }
+
+ /**
+ * @param ResourceInterface[] $resources
+ */
+ public function assertCollectionContainResources(array $resources): self
+ {
+ foreach ($resources as $resource) {
+ $this->assertCollectionHasResource($resource);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param ResourceInterface[] $resources
+ */
+ public function assertCollectionMissingResources(array $resources): self
+ {
+ foreach ($resources as $resource) {
+ $this->assertCollectionMissingResource($resource);
+ }
+
+ return $this;
+ }
+
+ public function assertCollectionHasResource(ResourceInterface $resource): self
+ {
+ $needle = \json_encode($resource);
+ $responseData = \json_decode((string)$this->response, true);
+
+ foreach ($responseData['data'] as $item) {
+ if ($needle === \json_encode($item)) {
+ return $this;
+ }
+ }
+
+ TestCase::fail(
+ \sprintf('Response does not contain resource [%s]', $needle),
+ );
+ }
+
+ public function assertCollectionMissingResource(ResourceInterface $resource): self
+ {
+ $needle = \json_encode($resource);
+ $responseData = \json_decode((string)$this->response, true);
+
+ foreach ($responseData['data'] as $item) {
+ if ($needle === \json_encode($item)) {
+ TestCase::fail(
+ \sprintf('Response contains resource [%s]', $needle),
+ );
+ }
+ }
+
+ return $this;
+ }
+
+ public function assertJsonResponseSame(array $data): self
+ {
+ $needle = \json_encode($data);
+ TestCase::assertSame(
+ $needle,
+ (string)$this->response,
+ \sprintf('Response is not same with [%s]', $needle),
+ );
+
+ return $this;
+ }
+
+ public function assertJsonResponseContains(array $data): self
+ {
+ $needle = \json_encode($data);
+ $responseData = \json_decode((string)$this->response, true);
+
+ $intersection = \array_intersect_key($responseData, $data);
+
+ $diff = [];
+
+ foreach ($data as $key => $value) {
+ if ($intersection[$key] !== $value) {
+ $diff[] = $key;
+ }
+ }
+
+ TestCase::assertSame(
+ $needle,
+ \json_encode($intersection),
+ \sprintf('The following keys are not same: [%s]', \implode(', ', $diff)),
+ );
+
+ return $this;
+ }
+
+ public function assertValidationErrors(array $errors = []): self
+ {
+ $responseData = \json_decode((string)$this->response, true);
+
+ if (!\array_is_list($errors)) {
+ foreach ($errors as $key => $value) {
+ TestCase::assertArrayHasKey(
+ $key,
+ $responseData['errors'] ?? [],
+ \sprintf('Validation error for key [%s] not found', $key),
+ );
+
+ TestCase::assertSame(
+ $value,
+ $responseData['errors'][$key],
+ \sprintf('Validation error for key [%s] is not same', $key),
+ );
+ }
+ } else {
+ $diff = \array_diff($errors, \array_keys($responseData['errors']));
+
+ TestCase::assertEmpty(
+ $diff,
+ \sprintf('Validation errors for keys [%s] not found', \implode(', ', $diff)),
+ );
+ }
+
+ return $this
+ ->assertJsonResponseContains([
+ 'message' => 'The given data was invalid.',
+ 'code' => 422,
+ 'context' => null,
+ ])
+ ->assertUnprocessable();
+ }
+
+ public function assertSuccessResource(bool $status = true): self
+ {
+ return $this->assertOk()->assertResource(new SuccessResource($status));
+ }
+
+ public function assertRedirect(int $status = 302, ?string $uri = null): self
+ {
+ $this->assertStatus($status);
+
+ if ($uri !== null) {
+ $this->assertHasHeader('Location', $uri);
+ }
+
+ return $this;
+ }
+
+ public function __call(string $name, array $arguments): self
+ {
+ if (!method_exists($this->response, $name)) {
+ throw new \Exception("Method $name does not exist");
+ }
+
+ $this->response->$name(...$arguments);
+
+ return $this;
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/ControllerTestCase.php b/tests/Feature/Interfaces/Http/ControllerTestCase.php
new file mode 100644
index 00000000..ac687563
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/ControllerTestCase.php
@@ -0,0 +1,26 @@
+http = new HttpFaker($this->fakeHttp(), $this);
+ }
+
+ protected function randomUuid(): Uuid
+ {
+ return Uuid::generate();
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Events/ClearActionTest.php b/tests/Feature/Interfaces/Http/Events/ClearActionTest.php
new file mode 100644
index 00000000..3547c5ea
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Events/ClearActionTest.php
@@ -0,0 +1,28 @@
+fakeEvents()->eventShouldBeClear();
+
+ $this->http
+ ->clearEvents()
+ ->assertSuccessResource();
+ }
+
+ public function testClearEventsByType(): void
+ {
+ $this->fakeEvents()->eventShouldBeClear(type: 'test');
+
+ $this->http
+ ->clearEvents(type: 'test')
+ ->assertSuccessResource();
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Events/DeleteActionTest.php b/tests/Feature/Interfaces/Http/Events/DeleteActionTest.php
new file mode 100644
index 00000000..aa135d39
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Events/DeleteActionTest.php
@@ -0,0 +1,21 @@
+randomUuid();
+
+ $this->fakeEvents()->eventShouldBeDeleted($uuid);
+
+ $this->http
+ ->deleteEvent($uuid)
+ ->assertSuccessResource();
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Events/ShowActionTest.php b/tests/Feature/Interfaces/Http/Events/ShowActionTest.php
new file mode 100644
index 00000000..2db27e0f
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Events/ShowActionTest.php
@@ -0,0 +1,49 @@
+randomUuid(),
+ type: 'test',
+ payload: new Json(['foo' => 'bar']),
+ timestamp: 123.456,
+ projectId: null,
+ );
+
+ $this->fakeEvents()
+ ->shouldRequestEventByUuid($event->getUuid())
+ ->andReturnEvent($event);
+
+ $this->http
+ ->showEvent($event->getUuid())
+ ->assertOk()
+ ->assertResource(new EventResource($event));
+ }
+
+ public function testNotFoundShowEvent(): void
+ {
+ $uuid = $this->randomUuid();
+ $this->fakeEvents()
+ ->shouldRequestEventByUuid($uuid)
+ ->andThrowNotFound();
+
+ $this->http
+ ->showEvent($uuid)
+ ->assertNotFound()
+ ->assertJsonResponseSame([
+ 'message' => 'Event not found',
+ 'code' => 404,
+ ]);
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/HttpDumps/HttpDumpsActionTest.php b/tests/Feature/Interfaces/Http/HttpDumps/HttpDumpsActionTest.php
new file mode 100644
index 00000000..c3342c38
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/HttpDumps/HttpDumpsActionTest.php
@@ -0,0 +1,103 @@
+http
+ ->postJson(
+ uri: '/',
+ data: ['foo' => 'bar'],
+ headers: ['X-Buggregator-Event' => 'http-dump'],
+ cookies: ['foo' => 'bar'],
+ )
+ ->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('http-dump', $data['data']['type']);
+ $this->assertSame('POST', $data['data']['payload']['request']['method']);
+ $this->assertSame('', $data['data']['payload']['request']['uri']);
+ $this->assertSame(['http-dump'], $data['data']['payload']['request']['headers']['X-Buggregator-Event']);
+ $this->assertSame('{"foo":"bar"}', $data['data']['payload']['request']['body']);
+ $this->assertSame(['foo' => 'bar'], $data['data']['payload']['request']['cookies']);
+ $this->assertSame([], $data['data']['payload']['request']['files']);
+ $this->assertSame(['foo' => 'bar'], $data['data']['payload']['request']['post']);
+ $this->assertSame([], $data['data']['payload']['request']['query']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+ $this->assertNotEmpty($data['data']['payload']['received_at']);
+
+ return true;
+ });
+ }
+
+ public function testHttpDumpsGet(): void
+ {
+ $this->http
+ ->getJson(
+ uri: '/?bar=foo',
+ query: ['foo' => 'bar'],
+ headers: ['X-Buggregator-Event' => 'http-dump'],
+ cookies: ['foo' => 'bar'],
+ )
+ ->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('http-dump', $data['data']['type']);
+ $this->assertSame('GET', $data['data']['payload']['request']['method']);
+ $this->assertSame('', $data['data']['payload']['request']['uri']);
+ $this->assertSame(['http-dump'], $data['data']['payload']['request']['headers']['X-Buggregator-Event']);
+ $this->assertSame('{"foo":"bar"}', $data['data']['payload']['request']['body']);
+ $this->assertSame(['foo' => 'bar'], $data['data']['payload']['request']['cookies']);
+ $this->assertSame([], $data['data']['payload']['request']['files']);
+ $this->assertSame(['foo' => 'bar'], $data['data']['payload']['request']['post']);
+ $this->assertSame(['bar' => 'foo'], $data['data']['payload']['request']['query']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+ $this->assertNotEmpty($data['data']['payload']['received_at']);
+
+ return true;
+ });
+ }
+
+ public function testHttpDumpsDelete(): void
+ {
+ $this->http
+ ->deleteJson(
+ uri: '/',
+ data: ['foo' => 'bar'],
+ headers: ['X-Buggregator-Event' => 'http-dump'],
+ cookies: ['foo' => 'bar'],
+ )
+ ->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('http-dump', $data['data']['type']);
+ $this->assertSame('DELETE', $data['data']['payload']['request']['method']);
+ $this->assertSame('', $data['data']['payload']['request']['uri']);
+ $this->assertSame(['http-dump'], $data['data']['payload']['request']['headers']['X-Buggregator-Event']);
+ $this->assertSame('{"foo":"bar"}', $data['data']['payload']['request']['body']);
+ $this->assertSame(['foo' => 'bar'], $data['data']['payload']['request']['cookies']);
+ $this->assertSame([], $data['data']['payload']['request']['files']);
+ $this->assertSame(['foo' => 'bar'], $data['data']['payload']['request']['post']);
+ $this->assertSame([], $data['data']['payload']['request']['query']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+ $this->assertNotEmpty($data['data']['payload']['received_at']);
+
+ return true;
+ });
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Inspector/InspectorActionTest.php b/tests/Feature/Interfaces/Http/Inspector/InspectorActionTest.php
new file mode 100644
index 00000000..377c1096
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Inspector/InspectorActionTest.php
@@ -0,0 +1,50 @@
+http
+ ->post(
+ uri: '/',
+ data: Stream::create(
+ <<<'BODY'
+W3sibW9kZWwiOiJ0cmFuc2FjdGlvbiIsIm5hbWUiOiJcL2ZvbyIsInR5cGUiOiJwcm9jZXNzIiwiaGFzaCI6Ijk3OWZmYmNlY2ZjZDNhNzJjMWM0ZDUzNmFhMWZlODViM2U5OTZkZjFkNzA5Mzc1NWI5YjRhMWRlZDhlMzNiNWMiLCJob3N0Ijp7Imhvc3RuYW1lIjoiQnV0c2Noc3RlckxwcCIsImlwIjoiMTI3LjAuMS4xIiwib3MiOiJMaW51eCJ9LCJ0aW1lc3RhbXAiOjE3MDE0NjQwMzkuNjUwNjIyLCJtZW1vcnlfcGVhayI6MTUuNTMsImR1cmF0aW9uIjowLjIyfSx7Im1vZGVsIjoic2VnbWVudCIsInR5cGUiOiJteS1wcm9jZXNzIiwiaG9zdCI6eyJob3N0bmFtZSI6IkJ1dHNjaHN0ZXJMcHAiLCJpcCI6IjEyNy4wLjEuMSIsIm9zIjoiTGludXgifSwidHJhbnNhY3Rpb24iOnsibmFtZSI6IlwvZm9vIiwiaGFzaCI6Ijk3OWZmYmNlY2ZjZDNhNzJjMWM0ZDUzNmFhMWZlODViM2U5OTZkZjFkNzA5Mzc1NWI5YjRhMWRlZDhlMzNiNWMiLCJ0aW1lc3RhbXAiOjE3MDE0NjQwMzkuNjUwNjIyfSwic3RhcnQiOjAuMiwidGltZXN0YW1wIjoxNzAxNDY0MDM5LjY1MDgyNiwiZHVyYXRpb24iOjAuMDF9XQ==
+BODY,
+ ),
+ headers: [
+ 'X-Buggregator-Event' => 'inspector',
+ 'X-Inspector-Key' => 'test',
+ 'X-Inspector-Version' => '1.0.0',
+ ],
+ )->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('inspector', $data['data']['type']);
+
+ $this->assertSame('transaction', $data['data']['payload'][0]['model']);
+ $this->assertSame('/foo', $data['data']['payload'][0]['name']);
+ $this->assertSame('process', $data['data']['payload'][0]['type']);
+ $this->assertSame(
+ '979ffbcecfcd3a72c1c4d536aa1fe85b3e996df1d7093755b9b4a1ded8e33b5c',
+ $data['data']['payload'][0]['hash'],
+ );
+
+ $this->assertSame('segment', $data['data']['payload'][1]['model']);
+ $this->assertSame('my-process', $data['data']['payload'][1]['type']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+
+ return true;
+ });
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Ray/AvailabilityCheckActionTest.php b/tests/Feature/Interfaces/Http/Ray/AvailabilityCheckActionTest.php
new file mode 100644
index 00000000..0c63be3e
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Ray/AvailabilityCheckActionTest.php
@@ -0,0 +1,18 @@
+http->getJson('/_availability_check', headers: [
+ 'X-Buggregator-Event' => 'ray',
+ ])
+ ->assertStatus(400);
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Ray/LocksActionTest.php b/tests/Feature/Interfaces/Http/Ray/LocksActionTest.php
new file mode 100644
index 00000000..30d2b4d7
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Ray/LocksActionTest.php
@@ -0,0 +1,35 @@
+http->getJson('/locks/123', headers: [
+ 'X-Buggregator-Event' => 'ray',
+ ])
+ ->assertOk()
+ ->assertJsonResponseSame([
+ 'active' => true,
+ 'stop_execution' => false,
+ ]);
+ }
+
+ public function testCheckWithLock(): void
+ {
+ $cache = $this->get(CacheInterface::class);
+ $cache->set('123', $response = ['active' => false, 'stop_execution' => true]);
+
+ $this->http->getJson('/locks/123', headers: [
+ 'X-Buggregator-Event' => 'ray',
+ ])
+ ->assertOk()
+ ->assertJsonResponseSame($response);
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Ray/RayActionTest.php b/tests/Feature/Interfaces/Http/Ray/RayActionTest.php
new file mode 100644
index 00000000..4630d1b9
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Ray/RayActionTest.php
@@ -0,0 +1,82 @@
+http->postJson(
+ uri: '/',
+ data: Stream::create(
+ <<<'JSON'
+{"uuid":"11325003-b9cf-4c06-83d0-8a18fe368ac4","payloads":[{"type":"log","content":{"values":["foo"],"meta":[{"clipboard_data":"foo"}]},"origin":{"file":"\/root\/repos\/buggreagtor\/spiral-app\/tests\/Feature\/Interfaces\/Http\/Ray\/RayActionTest.php","line_number":13,"hostname":"ButschsterLpp"}}],"meta":{"php_version":"8.2.5","php_version_id":80205,"project_name":"","ray_package_version":"1.40.1.0"}}
+JSON,
+ ),
+ headers: [
+ 'X-Buggregator-Event' => 'ray',
+ ],
+ )->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('ray', $data['data']['type']);
+
+ $this->assertSame('11325003-b9cf-4c06-83d0-8a18fe368ac4', $data['data']['payload']['uuid']);
+ $this->assertSame('8.2.5', $data['data']['payload']['meta']['php_version']);
+ $this->assertSame('1.40.1.0', $data['data']['payload']['meta']['ray_package_version']);
+
+
+ $this->assertSame('log', $data['data']['payload']['payloads'][0]['type']);
+ $this->assertSame(['foo'], $data['data']['payload']['payloads'][0]['content']['values']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+
+ return true;
+ });
+ }
+
+ public function testSendDumpWithMerge(): void
+ {
+ $payload = <<<'JSON'
+{"uuid":"11325003-b9cf-4c06-83d0-8a18fe368ac4","payloads":[{"type":"log","content":{"values":["foo"],"meta":[{"clipboard_data":"foo"}]},"origin":{"file":"\/root\/repos\/buggreagtor\/spiral-app\/tests\/Feature\/Interfaces\/Http\/Ray\/RayActionTest.php","line_number":13,"hostname":"ButschsterLpp"}}],"meta":{"php_version":"8.2.5","php_version_id":80205,"project_name":"","ray_package_version":"1.40.1.0"}}
+JSON;
+ $color = <<<'JSON'
+{"uuid":"11325003-b9cf-4c06-83d0-8a18fe368ac4","payloads":[{"type":"color","content":{"color":"red"},"origin":{"file":"\/root\/repos\/buggreagtor\/spiral-app\/tests\/Feature\/Interfaces\/Http\/Ray\/RayActionTest.php","line_number":47,"hostname":"ButschsterLpp"}}],"meta":{"php_version":"8.2.5","php_version_id":80205,"project_name":"","ray_package_version":"1.40.1.0"}}
+JSON;
+
+ $this->http->postJson(uri: '/', data: Stream::create($payload), headers: ['X-Buggregator-Event' => 'ray',],
+ )->assertOk();
+ $this->broadcastig->reset();
+ $this->http->postJson(uri: '/', data: Stream::create($color), headers: ['X-Buggregator-Event' => 'ray',],
+ )->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('ray', $data['data']['type']);
+
+ $this->assertSame('11325003-b9cf-4c06-83d0-8a18fe368ac4', $data['data']['payload']['uuid']);
+ $this->assertSame('8.2.5', $data['data']['payload']['meta']['php_version']);
+ $this->assertSame('1.40.1.0', $data['data']['payload']['meta']['ray_package_version']);
+
+
+ $this->assertSame('log', $data['data']['payload']['payloads'][0]['type']);
+ $this->assertSame(['foo'], $data['data']['payload']['payloads'][0]['content']['values']);
+
+
+ $this->assertSame('color', $data['data']['payload']['payloads'][1]['type']);
+ $this->assertSame(['color' => 'red'], $data['data']['payload']['payloads'][1]['content']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+
+ return true;
+ });
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php b/tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php
new file mode 100644
index 00000000..1952f05a
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php
@@ -0,0 +1,45 @@
+http
+ ->postJson(
+ uri: '/api/1/store/',
+ data: Stream::create(
+ <<<'BODY'
+{"event_id":"f7b7f09d40e645c79a8a2846e2111c81","timestamp":1701453725.632805,"platform":"php","sdk":{"name":"sentry.php","version":"3.22.1"},"logger":"php","server_name":"Test","environment":"production","modules":{"amphp\/amp":"v2.6.2","amphp\/byte-stream":"v1.8.1","brick\/math":"0.11.0","buggregator\/app":"dev-master@818ea82","clue\/stream-filter":"v1.6.0","cocur\/slugify":"v3.2","codedungeon\/php-cli-colors":"1.12.2","composer\/pcre":"3.1.1","composer\/semver":"3.4.0","composer\/xdebug-handler":"3.0.3","cycle\/annotated":"v3.4.0","cycle\/database":"2.6.1","cycle\/migrations":"v4.2.1","cycle\/orm":"v2.4.0","cycle\/schema-builder":"v2.6.1","cycle\/schema-migrations-generator":"2.2.0","cycle\/schema-renderer":"1.2.0","defuse\/php-encryption":"v2.4.0","dnoegel\/php-xdg-base-dir":"v0.1.1","doctrine\/annotations":"2.0.1","doctrine\/collections":"1.8.0","doctrine\/deprecations":"1.1.2","doctrine\/inflector":"2.0.8","doctrine\/instantiator":"2.0.0","doctrine\/lexer":"3.0.0","egulias\/email-validator":"4.0.2","felixfbecker\/advanced-json-rpc":"v3.2.1","felixfbecker\/language-server-protocol":"v1.5.2","fidry\/cpu-core-counter":"0.5.1","google\/common-protos":"v4.4.0","google\/protobuf":"v3.25.1","graham-campbell\/result-type":"v1.1.2","grpc\/grpc":"1.57.0","guzzlehttp\/promises":"2.0.1","guzzlehttp\/psr7":"2.6.1","hamcrest\/hamcrest-php":"v2.0.1","http-interop\/http-factory-guzzle":"1.2.0","jean85\/pretty-package-versions":"2.0.5","league\/event":"3.0.2","league\/flysystem":"2.5.0","league\/mime-type-detection":"1.14.0","mockery\/mockery":"1.6.6","monolog\/monolog":"2.9.2","myclabs\/deep-copy":"1.11.1","nesbot\/carbon":"2.71.0","netresearch\/jsonmapper":"v4.2.0","nette\/php-generator":"v4.1.2","nette\/utils":"v4.0.3","nikic\/php-parser":"v4.17.1","nyholm\/psr7":"1.8.1","paragonie\/random_compat":"v9.99.100","phar-io\/manifest":"2.0.3","phar-io\/version":"3.2.1","php-http\/client-common":"2.7.1","php-http\/discovery":"1.19.2","php-http\/httplug":"2.4.0","php-http\/message":"1.16.0","php-http\/message-factory":"1.1.0","php-http\/promise":"1.2.1","phpdocumentor\/reflection-common":"2.2.0","phpdocumentor\/reflection-docblock":"5.3.0","phpdocumentor\/type-resolver":"1.7.3","phpoption\/phpoption":"1.9.2","phpstan\/phpdoc-parser":"1.24.3","phpunit\/php-code-coverage":"9.2.29","phpunit\/php-file-iterator":"3.0.6","phpunit\/php-invoker":"3.1.1","phpunit\/php-text-template":"2.0.4","phpunit\/php-timer":"5.0.3","phpunit\/phpunit":"9.6.13","pimple\/pimple":"v3.5.0","psr\/cache":"3.0.0","psr\/clock":"1.0.0","psr\/container":"2.0.2","psr\/event-dispatcher":"1.0.0","psr\/http-client":"1.0.3","psr\/http-factory":"1.0.2","psr\/http-message":"2.0","psr\/http-server-handler":"1.0.2","psr\/http-server-middleware":"1.0.2","psr\/log":"3.0.0","psr\/simple-cache":"3.0.0","qossmic\/deptrac-shim":"1.0.2","ralouphie\/getallheaders":"3.0.3","ramsey\/collection":"2.0.0","ramsey\/uuid":"4.7.5","roadrunner-php\/app-logger":"1.1.0","roadrunner-php\/centrifugo":"2.0.0","roadrunner-php\/roadrunner-api-dto":"1.4.0","sebastian\/cli-parser":"1.0.1","sebastian\/code-unit":"1.0.8","sebastian\/code-unit-reverse-lookup":"2.0.3","sebastian\/comparator":"4.0.8","sebastian\/complexity":"2.0.2","sebastian\/diff":"4.0.5","sebastian\/environment":"5.1.5","sebastian\/exporter":"4.0.5","sebastian\/global-state":"5.0.6","sebastian\/lines-of-code":"1.0.3","sebastian\/object-enumerator":"4.0.4","sebastian\/object-reflector":"2.0.4","sebastian\/recursion-context":"4.0.5","sebastian\/resource-operations":"3.0.3","sebastian\/type":"3.2.1","sebastian\/version":"3.0.2","sentry\/sdk":"3.5.0","sentry\/sentry":"3.22.1","spatie\/array-to-xml":"3.2.2","spiral-packages\/cqrs":"v2.2.0","spiral-packages\/league-event":"1.0.1","spiral\/attributes":"v3.1.2","spiral\/composer-publish-plugin":"v1.1.2","spiral\/cycle-bridge":"v2.8.0","spiral\/data-grid":"v3.0.0","spiral\/data-grid-bridge":"v3.0.1","spiral\/framework":"3.10.0","spiral\/goridge":"4.1.0","spiral\/nyholm-bridge":"v1.3.0","spiral\/roadrunner":"v2023.3.6","spiral\/roadrunner-bridge":"3.0.2","spiral\/roadrunner-grpc":"3.1.0","spiral\/roadrunner-http":"3.2.0","spiral\/roadrunner-jobs":"4.2.0","spiral\/roadrunner-kv":"4.0.0","spiral\/roadrunner-metrics":"3.1.0","spiral\/roadrunner-services":"2.1.0","spiral\/roadrunner-tcp":"3.0.0","spiral\/roadrunner-worker":"3.2.0","spiral\/testing":"2.6.2","spiral\/validator":"1.5.0","symfony\/clock":"v6.3.4","symfony\/console":"v6.3.8","symfony\/deprecation-contracts":"v3.4.0","symfony\/event-dispatcher":"v6.3.2","symfony\/event-dispatcher-contracts":"v3.4.0","symfony\/filesystem":"v6.3.1","symfony\/finder":"v6.3.5","symfony\/http-client":"v6.4.0","symfony\/http-client-contracts":"v3.4.0","symfony\/mailer":"v6.3.5","symfony\/messenger":"v6.3.7","symfony\/mime":"v6.3.5","symfony\/options-resolver":"v7.0.0","symfony\/polyfill-ctype":"v1.28.0","symfony\/polyfill-iconv":"v1.28.0","symfony\/polyfill-intl-grapheme":"v1.28.0","symfony\/polyfill-intl-idn":"v1.28.0","symfony\/polyfill-intl-normalizer":"v1.28.0","symfony\/polyfill-mbstring":"v1.28.0","symfony\/polyfill-php72":"v1.28.0","symfony\/polyfill-php80":"v1.28.0","symfony\/polyfill-php83":"v1.28.0","symfony\/process":"v6.3.4","symfony\/service-contracts":"v3.4.0","symfony\/string":"v6.3.8","symfony\/translation":"v6.3.7","symfony\/translation-contracts":"v3.4.0","symfony\/var-dumper":"v6.3.8","symfony\/yaml":"v6.3.8","theseer\/tokenizer":"1.2.2","vimeo\/psalm":"5.16.0","vlucas\/phpdotenv":"v5.6.0","webmozart\/assert":"1.11.0","yiisoft\/friendly-exception":"1.1.0","zbateson\/mail-mime-parser":"2.4.0","zbateson\/mb-wrapper":"1.2.0","zbateson\/stream-decorators":"1.2.1","zentlix\/swagger-php":"1.x-dev@1f4927a","zircote\/swagger-php":"4.7.15"},"contexts":{"os":{"name":"Linux","version":"5.15.133.1-microsoft-standard-WSL2","build":"#1 SMP Thu Oct 5 21:02:42 UTC 2023","kernel_version":"Linux Test 5.15.133.1-microsoft-standard-WSL2 #1 SMP Thu Oct 5 21:02:42 UTC 2023 x86_64"},"runtime":{"name":"php","version":"8.2.5"},"trace":{"trace_id":"9fc5094fa3a048209c9cc5f86fab33c8","span_id":"ce2a557634354a20"}},"exception":{"values":[{"type":"Exception","value":"test","stacktrace":{"frames":[{"filename":"\/vendor\/phpunit\/phpunit\/phpunit","lineno":107,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/phpunit","pre_context":["","unset($options);","","require PHPUNIT_COMPOSER_INSTALL;",""],"context_line":"PHPUnit\\TextUI\\Command::main();","post_context":[""]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":97,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::main","raw_function":"PHPUnit\\TextUI\\Command::main","pre_context":[" * @throws Exception"," *\/"," public static function main(bool $exit = true): int"," {"," try {"],"context_line":" return (new static)->run($_SERVER['argv'], $exit);","post_context":[" } catch (Throwable $t) {"," throw new RuntimeException("," $t->getMessage(),"," (int) $t->getCode(),"," $t,"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":144,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::run","raw_function":"PHPUnit\\TextUI\\Command::run","pre_context":[" }",""," unset($this->arguments['test'], $this->arguments['testFile']);",""," try {"],"context_line":" $result = $runner->run($suite, $this->arguments, $this->warnings, $exit);","post_context":[" } catch (Throwable $t) {"," print $t->getMessage() . PHP_EOL;"," }",""," $return = TestRunner::FAILURE_EXIT;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","lineno":651,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","function":"PHPUnit\\TextUI\\TestRunner::run","raw_function":"PHPUnit\\TextUI\\TestRunner::run","pre_context":[" if ($extension instanceof BeforeFirstTestHook) {"," $extension->executeBeforeFirstTest();"," }"," }",""],"context_line":" $suite->run($result);","post_context":[""," foreach ($this->extensions as $extension) {"," if ($extension instanceof AfterLastTestHook) {"," $extension->executeAfterLastTest();"," }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":[" $test->setBackupGlobals($this->backupGlobals);"," $test->setBackupStaticAttributes($this->backupStaticAttributes);"," $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);"," }",""],"context_line":" $test->run($result);","post_context":[" }",""," if ($this->testCase && class_exists($this->name, false)) {"," foreach ($hookMethods['afterClass'] as $afterClassMethod) {"," if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":[" $test->setBackupGlobals($this->backupGlobals);"," $test->setBackupStaticAttributes($this->backupStaticAttributes);"," $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);"," }",""],"context_line":" $test->run($result);","post_context":[" }",""," if ($this->testCase && class_exists($this->name, false)) {"," foreach ($hookMethods['afterClass'] as $afterClassMethod) {"," if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":968,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::run","raw_function":"PHPUnit\\Framework\\TestCase::run","pre_context":[" $template->setVar($var);",""," $php = AbstractPhpProcess::factory();"," $php->runTestJob($template->render(), $this, $result, $processResultFile);"," } else {"],"context_line":" $result->run($this);","post_context":[" }",""," $this->result = null;",""," return $result;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","lineno":728,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","function":"PHPUnit\\Framework\\TestResult::run","raw_function":"PHPUnit\\Framework\\TestResult::run","pre_context":[" $_timeout = $this->defaultTimeLimit;"," }",""," $invoker->invoke([$test, 'runBare'], [], $_timeout);"," } else {"],"context_line":" $test->runBare();","post_context":[" }"," } catch (TimeoutException $e) {"," $this->addFailure("," $test,"," new RiskyTestError("]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1218,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runBare","raw_function":"PHPUnit\\Framework\\TestCase::runBare","pre_context":[""," foreach ($hookMethods['preCondition'] as $method) {"," $this->{$method}();"," }",""],"context_line":" $this->testResult = $this->runTest();","post_context":[" $this->verifyMockObjects();",""," foreach ($hookMethods['postCondition'] as $method) {"," $this->{$method}();"," }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1612,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runTest","raw_function":"PHPUnit\\Framework\\TestCase::runTest","pre_context":[" $testArguments = array_merge($this->data, $this->dependencyInput);",""," $this->registerMockObjectsFromTestArguments($testArguments);",""," try {"],"context_line":" $testResult = $this->{$this->name}(...array_values($testArguments));","post_context":[" } catch (Throwable $exception) {"," if (!$this->checkExceptionExpectations($exception)) {"," throw $exception;"," }",""]},{"filename":"\/tests\/Feature\/Interfaces\/Http\/HttpDumps\/HttpDumpsActionTest.php","lineno":14,"in_app":true,"abs_path":"\\/tests\/Feature\/Interfaces\/Http\/HttpDumps\/HttpDumpsActionTest.php","function":"Tests\\Feature\\Interfaces\\Http\\HttpDumps\\HttpDumpsActionTest::testHttpDumpsPost","raw_function":"Tests\\Feature\\Interfaces\\Http\\HttpDumps\\HttpDumpsActionTest::testHttpDumpsPost","pre_context":["final class HttpDumpsActionTest extends ControllerTestCase","{"," public function testHttpDumpsPost(): void"," {"," \\Sentry\\init(['dsn' => 'http:\/\/user@127.0.0.1:8082\/1']);"],"context_line":" \\Sentry\\captureException(new \\Exception('test'));","post_context":["",""," $this->http"," ->postJson("," uri: '\/',"]}]},"mechanism":{"type":"generic","handled":true,"data":{"code":0}}}]}}
+BODY,
+ ),
+ headers: ['X-Buggregator-Event' => 'sentry'],
+ )->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('sentry', $data['data']['type']);
+
+ $this->assertSame('f7b7f09d40e645c79a8a2846e2111c81', $data['data']['payload']['event_id']);
+ $this->assertSame(1701453725.632805, $data['data']['payload']['timestamp']);
+ $this->assertSame('php', $data['data']['payload']['platform']);
+ $this->assertSame('php', $data['data']['payload']['logger']);
+ $this->assertSame('sentry.php', $data['data']['payload']['sdk']['name']);
+ $this->assertSame('3.22.1', $data['data']['payload']['sdk']['version']);
+ $this->assertSame('Test', $data['data']['payload']['server_name']);
+ $this->assertSame('production', $data['data']['payload']['environment']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+
+
+ return true;
+ });
+ }
+}
diff --git a/tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php b/tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php
new file mode 100644
index 00000000..336b5271
--- /dev/null
+++ b/tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php
@@ -0,0 +1,82 @@
+run($_SERVER['argv'], $exit);","post_context":[" } catch (Throwable $t) {"," throw new RuntimeException("," $t->getMessage(),"," (int) $t->getCode(),"," $t,"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":144,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::run","raw_function":"PHPUnit\\TextUI\\Command::run","pre_context":[" }",""," unset($this->arguments['test'], $this->arguments['testFile']);",""," try {"],"context_line":" $result = $runner->run($suite, $this->arguments, $this->warnings, $exit);","post_context":[" } catch (Throwable $t) {"," print $t->getMessage() . PHP_EOL;"," }",""," $return = TestRunner::FAILURE_EXIT;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","lineno":651,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","function":"PHPUnit\\TextUI\\TestRunner::run","raw_function":"PHPUnit\\TextUI\\TestRunner::run","pre_context":[" if ($extension instanceof BeforeFirstTestHook) {"," $extension->executeBeforeFirstTest();"," }"," }",""],"context_line":" $suite->run($result);","post_context":[""," foreach ($this->extensions as $extension) {"," if ($extension instanceof AfterLastTestHook) {"," $extension->executeAfterLastTest();"," }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":[" $test->setBackupGlobals($this->backupGlobals);"," $test->setBackupStaticAttributes($this->backupStaticAttributes);"," $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);"," }",""],"context_line":" $test->run($result);","post_context":[" }",""," if ($this->testCase && class_exists($this->name, false)) {"," foreach ($hookMethods['afterClass'] as $afterClassMethod) {"," if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":[" $test->setBackupGlobals($this->backupGlobals);"," $test->setBackupStaticAttributes($this->backupStaticAttributes);"," $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);"," }",""],"context_line":" $test->run($result);","post_context":[" }",""," if ($this->testCase && class_exists($this->name, false)) {"," foreach ($hookMethods['afterClass'] as $afterClassMethod) {"," if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":968,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::run","raw_function":"PHPUnit\\Framework\\TestCase::run","pre_context":[" $template->setVar($var);",""," $php = AbstractPhpProcess::factory();"," $php->runTestJob($template->render(), $this, $result, $processResultFile);"," } else {"],"context_line":" $result->run($this);","post_context":[" }",""," $this->result = null;",""," return $result;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","lineno":728,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","function":"PHPUnit\\Framework\\TestResult::run","raw_function":"PHPUnit\\Framework\\TestResult::run","pre_context":[" $_timeout = $this->defaultTimeLimit;"," }",""," $invoker->invoke([$test, 'runBare'], [], $_timeout);"," } else {"],"context_line":" $test->runBare();","post_context":[" }"," } catch (TimeoutException $e) {"," $this->addFailure("," $test,"," new RiskyTestError("]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1218,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runBare","raw_function":"PHPUnit\\Framework\\TestCase::runBare","pre_context":[""," foreach ($hookMethods['preCondition'] as $method) {"," $this->{$method}();"," }",""],"context_line":" $this->testResult = $this->runTest();","post_context":[" $this->verifyMockObjects();",""," foreach ($hookMethods['postCondition'] as $method) {"," $this->{$method}();"," }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1612,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runTest","raw_function":"PHPUnit\\Framework\\TestCase::runTest","pre_context":[" $testArguments = array_merge($this->data, $this->dependencyInput);",""," $this->registerMockObjectsFromTestArguments($testArguments);",""," try {"],"context_line":" $testResult = $this->{$this->name}(...array_values($testArguments));","post_context":[" } catch (Throwable $exception) {"," if (!$this->checkExceptionExpectations($exception)) {"," throw $exception;"," }",""]},{"filename":"\/tests\/Feature\/Interfaces\/Http\/Sentry\/SentryV4ActionTest.php","lineno":14,"in_app":true,"abs_path":"\\/tests\/Feature\/Interfaces\/Http\/Sentry\/SentryV4ActionTest.php","function":"Interfaces\\Http\\Sentry\\SentryV4ActionTest::testSend","raw_function":"Interfaces\\Http\\Sentry\\SentryV4ActionTest::testSend","pre_context":["final class SentryV4ActionTest extends ControllerTestCase","{"," public function testSend(): void"," {"," \\Sentry\\init(['dsn' => 'http:\/\/user@127.0.0.1:8082\/1']);"],"context_line":" \\Sentry\\captureException(new \\Exception('test'));","post_context":["","\/\/ $this->http","\/\/ ->postJson(","\/\/ uri: '\/api\/1\/store\/',","\/\/ data: Stream::create("]}]},"mechanism":{"type":"generic","handled":true,"data":{"code":0}}}]}}
+BODY;
+
+ public function testSendWithoutGzip(): void
+ {
+ $this->http
+ ->postJson(
+ uri: '/api/1/envelope/',
+ data: Stream::create(self::JSON),
+ headers: [
+ 'X-Buggregator-Event' => 'sentry',
+ 'Content-Type' => 'application/x-sentry-envelope',
+ 'X-Sentry-Auth' => 'Sentry sentry_version=7, sentry_client=sentry.php/4.0.1, sentry_key=user',
+ ],
+ )->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('sentry', $data['data']['type']);
+
+ $this->assertSame(1701455435.634665, $data['data']['payload']['timestamp']);
+ $this->assertSame('php', $data['data']['payload']['platform']);
+ $this->assertSame('sentry.php', $data['data']['payload']['sdk']['name']);
+ $this->assertSame('4.0.1', $data['data']['payload']['sdk']['version']);
+ $this->assertSame('Test', $data['data']['payload']['server_name']);
+ $this->assertSame('production', $data['data']['payload']['environment']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+
+
+ return true;
+ });
+ }
+
+ public function testSendGzipped(): void
+ {
+ $this->http
+ ->postJson(
+ uri: '/api/1/envelope/',
+ data: Stream::create(\gzcompress(self::JSON, -1, \ZLIB_ENCODING_GZIP)),
+ headers: [
+ 'Content-Encoding' => 'gzip',
+ 'X-Buggregator-Event' => 'sentry',
+ 'Content-Type' => 'application/x-sentry-envelope',
+ 'X-Sentry-Auth' => 'Sentry sentry_version=7, sentry_client=sentry.php/4.0.1, sentry_key=user',
+ ],
+ )->assertOk();
+
+ $this->broadcastig->assertPushed('events', function (array $data) {
+ $this->assertSame('event.received', $data['event']);
+ $this->assertSame('sentry', $data['data']['type']);
+
+ $this->assertSame(1701455435.634665, $data['data']['payload']['timestamp']);
+ $this->assertSame('php', $data['data']['payload']['platform']);
+ $this->assertSame('sentry.php', $data['data']['payload']['sdk']['name']);
+ $this->assertSame('4.0.1', $data['data']['payload']['sdk']['version']);
+ $this->assertSame('Test', $data['data']['payload']['server_name']);
+ $this->assertSame('production', $data['data']['payload']['environment']);
+
+ $this->assertNotEmpty($data['data']['uuid']);
+ $this->assertNotEmpty($data['data']['timestamp']);
+
+
+ return true;
+ });
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 282bd126..d4391119 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -5,57 +5,90 @@
namespace Tests;
use App\Application\Service\ErrorHandler\Handler;
-use Spiral\Config\ConfiguratorInterface;
-use Spiral\Config\Patch\Set;
+use Modules\Events\Domain\EventRepositoryInterface;
+use Psr\SimpleCache\CacheInterface;
+use Spiral\Cache\Storage\ArrayStorage;
use Spiral\Core\Container;
+use Spiral\Core\ContainerScope;
use Spiral\Testing\TestableKernelInterface;
use Spiral\Testing\TestCase as BaseTestCase;
-use Spiral\Translator\TranslatorInterface;
+use Tests\App\Broadcasting\BroadcastFaker;
+use Tests\App\Events\EventsMocker;
use Tests\App\TestKernel;
class TestCase extends BaseTestCase
{
+ protected BroadcastFaker $broadcastig;
+ private ?EventsMocker $events = null;
+
protected function setUp(): void
{
- $this->beforeBooting(static function (ConfiguratorInterface $config): void {
- if (! $config->exists('session')) {
- return;
- }
-
- $config->modify('session', new Set('handler', null));
- });
-
parent::setUp();
- $this->getContainer()->get(TranslatorInterface::class)->setLocale('en');
+ // Bind container to ContainerScope
+ (new \ReflectionClass(ContainerScope::class))->setStaticPropertyValue('container', $this->getContainer());
+ $this->broadcastig = new BroadcastFaker($this->getContainer());
}
public function createAppInstance(Container $container = new Container()): TestableKernelInterface
{
return TestKernel::create(
directories: $this->defineDirectories(
- $this->rootDirectory()
+ $this->rootDirectory(),
),
exceptionHandler: Handler::class,
- container: $container
+ container: $container,
);
}
protected function tearDown(): void
{
+ parent::tearDown();
+
// Uncomment this line if you want to clean up runtime directory.
- $this->cleanUpRuntimeDirectory();
+ // $this->cleanUpRuntimeDirectory();
+
+ (new \ReflectionClass(ContainerScope::class))->setStaticPropertyValue('container', null);
+ $this->broadcastig->reset();
}
public function rootDirectory(): string
{
- return __DIR__.'/..';
+ return __DIR__ . '/..';
}
public function defineDirectories(string $root): array
{
return [
'root' => $root,
+ 'modules' => $root . '/app/modules',
+ 'public' => $root . '/frontend/.output/public',
];
}
+
+ /**
+ * @template T
+ *
+ * @param class-string|string $id
+ *
+ * @return T|mixed
+ * @psalm-return ($id is class-string ? T : mixed)
+ *
+ * @throws \Throwable
+ */
+ public function get(string $id): mixed
+ {
+ return $this->getApp()->getContainer()->get($id);
+ }
+
+ public function fakeEvents(): EventsMocker
+ {
+ if ($this->events === null) {
+ $this->events = new EventsMocker(
+ $this->mockContainer(EventRepositoryInterface::class),
+ );
+ }
+
+ return $this->events;
+ }
}