Files
crewli/api/tests/Feature/Observability/BindSentryRouteContextTest.php
bert.hausmans 9414d09472 refactor: BindSentryContext to AuthScopeContextListener for auth-scope tags
Sentry-context binding split into two responsibilities:

- Route-scope (app, http.method, route_name) stays in middleware on
  the api group as BindSentryRouteContext — works on every request,
  no auth required.
- Auth-scope (user_id, actor_type) moves to AuthScopeContextListener
  on Illuminate\Auth\Events\Authenticated — works on every
  authentication mechanism (Sanctum, portal-tokens, future
  authenticators) without per-route middleware-attachment. Listener
  also augments Log::withContext with user_id (closes OBS-2).

Architecturally fault-preventing rather than fault-detecting: new
authenticated route groups need no separate sentry.context aliasing,
so silent observability gaps are no longer possible (closes OBS-3).

Impersonation tagging is co-located with HandleImpersonation: after
the user-swap, the middleware re-tags Sentry scope with the target
user_id/actor_type and adds impersonation.active /
impersonation.impersonator_user_id / impersonation.session_id. The
Authenticated event fires for the admin (Sanctum's natural flow),
the listener tags the admin, then HandleImpersonation overwrites
post-swap.

Files renamed:
- BindSentryContext -> BindSentryRouteContext (route-scope only)
- BindSentryContextTest -> BindSentryRouteContextTest (4 cases)

Files added:
- AuthScopeContextListener
- AuthScopeContextListenerTest (6 cases)

bootstrap/app.php drops the sentry.context alias and prepends
BindSentryRouteContext to the api group. routes/api.php drops every
sentry.context middleware string from auth:sanctum groups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:53:14 +02:00

90 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Observability;
use App\Http\Middleware\BindSentryRouteContext;
use Illuminate\Http\Request;
use Sentry\Event as SentryEvent;
use Sentry\SentrySdk;
use Sentry\State\Scope;
use Tests\TestCase;
use function Sentry\configureScope;
/**
* Route-scope tags (app, http.method, route_name) on every API request.
*
* Auth-scope assertions (user_id, actor_type, organisation_id, etc.) live
* in {@see AuthScopeContextListenerTest} — that's the file to look at if
* you're changing what gets tagged on authenticated events.
*/
final class BindSentryRouteContextTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
SentrySdk::getCurrentHub()->pushScope();
}
/**
* @return array<string, string>
*/
private function captureScopeTags(): array
{
$event = SentryEvent::createEvent();
configureScope(static function (Scope $scope) use ($event): void {
$scope->applyToEvent($event);
});
return $event->getTags();
}
private function runMiddleware(Request $request): void
{
(new BindSentryRouteContext())->handle($request, static fn (Request $req) => response('ok'));
}
public function test_app_tag_is_api(): void
{
$request = Request::create('http://localhost/api/v1/_anything', 'GET');
$this->runMiddleware($request);
$this->assertSame('api', $this->captureScopeTags()['app'] ?? null);
}
public function test_http_method_tag_present(): void
{
$request = Request::create('http://localhost/api/v1/me/profile', 'PATCH');
$this->runMiddleware($request);
$this->assertSame('PATCH', $this->captureScopeTags()['http.method'] ?? null);
}
public function test_route_name_tag_present(): void
{
$request = Request::create('http://localhost/api/v1/me/profile', 'GET');
$route = new \Illuminate\Routing\Route(['GET'], 'me/profile', static fn () => null);
$route->name('me.profile');
$route->bind($request);
$request->setRouteResolver(static fn () => $route);
$this->runMiddleware($request);
$this->assertSame('me.profile', $this->captureScopeTags()['route_name'] ?? null);
}
public function test_route_name_tag_omitted_when_route_has_no_name(): void
{
$request = Request::create('http://localhost/api/v1/anonymous', 'GET');
$this->runMiddleware($request);
$this->assertArrayNotHasKey('route_name', $this->captureScopeTags());
}
}