get(EntityManagerInterface::class); return $em->createQueryBuilder() ->select('s') ->from(Scenario::class, 's'); } private function extensionWithUser(?object $user): ScenarioOwnerExtension { /** @var Security&Stub $security */ $security = $this->createStub(Security::class); $security->method('getUser')->willReturn($user); return new ScenarioOwnerExtension($security); } public function testAddsOwnerFilterForAnAuthenticatedUserOnItsOwnResource(): void { $qb = $this->queryBuilder(); $extension = $this->extensionWithUser(new User()); $extension->applyToCollection($qb, new QueryNameGenerator(), Scenario::class); self::assertStringContainsStringIgnoringCase( '.owner = :', (string) $qb->getDQL(), 'An authenticated read of its own resource must be scoped to the owner.', ); } public function testDoesNothingForAResourceItDoesNotGuard(): void { $qb = $this->queryBuilder(); $extension = $this->extensionWithUser(new User()); // Bucket is guarded by a different extension — this one must no-op. $extension->applyToCollection($qb, new QueryNameGenerator(), \App\Entity\Bucket::class); self::assertStringNotContainsString( 'owner', (string) $qb->getDQL(), 'The extension must not touch queries for a resource it does not guard.', ); } public function testDoesNothingForAnAnonymousRequest(): void { $qb = $this->queryBuilder(); $extension = $this->extensionWithUser(null); $extension->applyToItem($qb, new QueryNameGenerator(), Scenario::class, ['id' => 'x']); self::assertStringNotContainsString( 'owner', (string) $qb->getDQL(), 'With no authenticated user the extension must not add an owner filter ' .'(it is a fail-safe behind the resource-level ROLE_USER wall).', ); } }