seed(RoleSeeder::class); $this->resetSentryScope(); } private function resetSentryScope(): void { SentrySdk::getCurrentHub()->pushScope(); } /** * Read the current Sentry scope by applying it to a fresh Event and * harvesting the tags / user context. * * @return array{tags: array, user: ?array} */ private function captureScope(): array { $event = SentryEvent::createEvent(); configureScope(static function (Scope $scope) use ($event): void { $scope->applyToEvent($event); }); $userBag = $event->getUser(); return [ 'tags' => $event->getTags(), 'user' => $userBag === null ? null : array_filter([ 'id' => $userBag->getId(), 'username' => $userBag->getUsername(), ], static fn ($v) => $v !== null), ]; } private function runMiddleware(Request $request): void { (new BindSentryContext)->handle($request, static fn (Request $req) => response('ok')); } private function makeAuthenticatedRequest(User $user, string $path = 'api/v1/me/profile'): Request { $request = Request::create('http://localhost/'.$path, 'GET'); $request->setUserResolver(static fn () => $user); return $request; } public function test_authenticated_org_admin_request_tags_organisation_id(): void { $org = Organisation::factory()->create(); $user = User::factory()->create(); $org->users()->attach($user, ['role' => 'org_admin']); $user->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($user, 'api/v1/organisations/'.$org->id.'/some-path'); $request->setRouteResolver(function () use ($org, $request) { $route = new \Illuminate\Routing\Route(['GET'], 'organisations/{organisation}/some-path', static fn () => null); $route->bind($request); $route->setParameter('organisation', $org); $route->name('organisations.test'); return $route; }); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame($org->id, $scope['tags']['organisation_id'] ?? null); } public function test_authenticated_org_admin_request_tags_user_id(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile'); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame($user->id, $scope['tags']['user_id'] ?? null); $this->assertSame($user->id, $scope['user']['id'] ?? null); $this->assertSame($user->id, $scope['user']['username'] ?? null); } public function test_authenticated_org_admin_request_tags_actor_type_organizer_admin(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($user); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('organizer_admin', $scope['tags']['actor_type'] ?? null); } public function test_super_admin_request_tags_actor_type_super_admin(): void { $user = User::factory()->create(); $user->assignRole('super_admin'); $request = $this->makeAuthenticatedRequest($user, 'api/v1/admin/organisations'); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('super_admin', $scope['tags']['actor_type'] ?? null); } public function test_org_member_authenticated_user_tags_actor_type_org_member(): void { // Crewli has no `volunteer` Spatie role today; volunteers fall into // org_member. The VOLUNTEER ActorType case is reserved for a future // split — see ActorType::resolve() docblock. $user = User::factory()->create(); $user->assignRole('org_member'); $request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile'); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('org_member', $scope['tags']['actor_type'] ?? null); } public function test_portal_token_request_tags_actor_type_portal_token(): void { $org = Organisation::factory()->create(); $event = Event::factory()->create(['organisation_id' => $org->id]); $request = Request::create('http://localhost/api/v1/portal/me', 'GET'); $request->attributes->set('portal_context', 'artist'); $request->attributes->set('portal_event', $event); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('portal_token', $scope['tags']['actor_type'] ?? null); } public function test_portal_token_request_tags_organisation_id_from_token(): void { $org = Organisation::factory()->create(); $event = Event::factory()->create(['organisation_id' => $org->id]); $request = Request::create('http://localhost/api/v1/portal/me', 'GET'); $request->attributes->set('portal_context', 'artist'); $request->attributes->set('portal_event', $event); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame($org->id, $scope['tags']['organisation_id'] ?? null); $this->assertSame($event->id, $scope['tags']['event_id'] ?? null); } public function test_unauthenticated_request_tags_actor_type_unauthenticated(): void { $request = Request::create('http://localhost/api/v1/auth/login', 'POST'); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('unauthenticated', $scope['tags']['actor_type'] ?? null); $this->assertArrayNotHasKey('user_id', $scope['tags']); } public function test_authenticated_request_to_tenant_scoped_route_without_org_throws_in_test_environment(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($user, 'api/v1/organisations/missing/events'); // Synthesise a route that DECLARES {organisation} but has no // bound parameter — the invariant must fire. $route = new \Illuminate\Routing\Route(['GET'], 'organisations/{organisation}/events', static fn () => null); $route->bind($request); $request->setRouteResolver(static fn () => $route); $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches('/lacks resolvable organisation_id/'); $this->runMiddleware($request); } public function test_authenticated_request_to_user_scoped_route_skips_invariant(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); // /me/profile route declares no {organisation} or {event} param — // user-scoped, invariant skipped. $request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile'); $route = new \Illuminate\Routing\Route(['GET'], 'me/profile', static fn () => null); $route->bind($request); $request->setRouteResolver(static fn () => $route); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('organizer_admin', $scope['tags']['actor_type'] ?? null); } public function test_impersonation_active_tag_when_session_active(): void { $admin = User::factory()->create(); $admin->assignRole('super_admin'); $target = User::factory()->create(); $target->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($target, 'api/v1/me/profile'); $request->attributes->set('impersonator', $admin); $session = new ImpersonationSession; $session->id = '01J0000000000000000000IMPS'; $session->admin_id = $admin->id; $session->target_user_id = $target->id; $request->attributes->set('impersonation_session', $session); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('true', $scope['tags']['impersonation.active'] ?? null); } public function test_impersonation_impersonator_user_id_tag_when_session_active(): void { $admin = User::factory()->create(); $admin->assignRole('super_admin'); $target = User::factory()->create(); $target->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($target, 'api/v1/me/profile'); $request->attributes->set('impersonator', $admin); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame($admin->id, $scope['tags']['impersonation.impersonator_user_id'] ?? null); } public function test_impersonation_active_false_when_no_session(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile'); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('false', $scope['tags']['impersonation.active'] ?? null); $this->assertArrayNotHasKey('impersonation.impersonator_user_id', $scope['tags']); } public function test_route_name_tag_present(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile'); $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); $scope = $this->captureScope(); $this->assertSame('me.profile', $scope['tags']['route_name'] ?? null); } public function test_http_method_tag_present(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); $request = Request::create('http://localhost/api/v1/me/profile', 'PATCH'); $request->setUserResolver(static fn () => $user); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('PATCH', $scope['tags']['http.method'] ?? null); } public function test_app_tag_is_api(): void { $user = User::factory()->create(); $user->assignRole('org_admin'); $request = $this->makeAuthenticatedRequest($user); $this->runMiddleware($request); $scope = $this->captureScope(); $this->assertSame('api', $scope['tags']['app'] ?? null); } }