diff --git a/api/composer.json b/api/composer.json index 3db3a76af6..5da3db9c97 100644 --- a/api/composer.json +++ b/api/composer.json @@ -70,7 +70,6 @@ "hautelook/alice-bundle": "2.16.0", "justinrainbow/json-schema": "6.6.2", "php-coveralls/php-coveralls": "2.9.0", - "phpspec/prophecy-phpunit": "2.4.0", "phpstan/phpstan": "2.1.32", "phpunit/phpunit": "12.4.4", "rector/rector": "2.2.9", diff --git a/api/composer.lock b/api/composer.lock index 1ee65dcef9..9bc941022d 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "26fb723f4b47d0b5dfc995fc8db95c52", + "content-hash": "84f0b1aaac34762e91b4fc6bddf68e4e", "packages": [ { "name": "api-platform/doctrine-common", @@ -13192,132 +13192,6 @@ }, "time": "2025-11-06T10:39:48+00:00" }, - { - "name": "phpspec/prophecy", - "version": "v1.23.1", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "9500f939e4b22c40c3d5cca5f10837f2a9c87cb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/9500f939e4b22c40c3d5cca5f10837f2a9c87cb0", - "reference": "9500f939e4b22c40c3d5cca5f10837f2a9c87cb0", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2 || ^2.0", - "php": "8.2.* || 8.3.* || 8.4.*", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/deprecation-contracts": "^2.5 || ^3.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.88", - "phpspec/phpspec": "^6.0 || ^7.0 || ^8.0", - "phpstan/phpstan": "^2.1.13", - "phpunit/phpunit": "^11.0 || ^12.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "dev", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.23.1" - }, - "time": "2025-10-27T22:44:31+00:00" - }, - { - "name": "phpspec/prophecy-phpunit", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy-phpunit.git", - "reference": "d3c28041d9390c9bca325a08c5b2993ac855bded" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/d3c28041d9390c9bca325a08c5b2993ac855bded", - "reference": "d3c28041d9390c9bca325a08c5b2993ac855bded", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8", - "phpspec/prophecy": "^1.18", - "phpunit/phpunit": "^9.1 || ^10.1 || ^11.0 || ^12.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.10" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\PhpUnit\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christophe Coevoet", - "email": "stof@notk.org" - } - ], - "description": "Integrating the Prophecy mocking library in PHPUnit test cases", - "homepage": "http://phpspec.net", - "keywords": [ - "phpunit", - "prophecy" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy-phpunit/issues", - "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.4.0" - }, - "time": "2025-05-13T13:52:32+00:00" - }, { "name": "phpstan/phpstan", "version": "2.1.32", @@ -16092,5 +15966,5 @@ "ext-iconv": "*" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php index 225f6ba8ef..0e321b08b2 100644 --- a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -37,74 +37,60 @@ use Doctrine\ORM\UnitOfWork; use FOS\HttpCacheBundle\CacheManager; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; +use Symfony\Bundle\MakerBundle\Doctrine\StaticReflectionService; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use function PHPUnit\Framework\assertThat; +use function PHPUnit\Framework\containsEqual; +use function PHPUnit\Framework\logicalAnd; + /** * @author Kévin Dunglas * * @internal */ class PurgeHttpCacheListenerTest extends TestCase { - use ProphecyTrait; - - private ObjectProphecy $cacheManagerProphecy; - private ObjectProphecy $resourceClassResolverProphecy; - private ObjectProphecy $uowProphecy; - private ObjectProphecy $emProphecy; - private ObjectProphecy $propertyAccessorProphecy; - private ObjectProphecy $iriConverterProphecy; - private ObjectProphecy $metadataFactoryProphecy; + private CacheManager $cacheManagerProphecy; + private ResourceClassResolverInterface $resourceClassResolverProphecy; + private UnitOfWork $uowProphecy; + private EntityManagerInterface $emProphecy; + private PropertyAccessorInterface $propertyAccessorProphecy; + private IriConverterInterface $iriConverterProphecy; + private ResourceMetadataCollectionFactoryInterface $metadataFactoryProphecy; protected function setUp(): void { - $this->cacheManagerProphecy = $this->prophesize(CacheManager::class); - $this->cacheManagerProphecy->flush()->willReturn(0); - - $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true); - $this->resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class); - - $this->uowProphecy = $this->prophesize(UnitOfWork::class); - - $this->emProphecy = $this->prophesize(EntityManagerInterface::class); - $this->emProphecy->detach(Argument::any()); - $this->emProphecy->getUnitOfWork()->willReturn($this->uowProphecy->reveal()); - - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getAssociationMappings()->willReturn([ - 'relatedDummy' => [ - 'targetEntity' => RelatedDummy::class, - 'isOwningSide' => true, - 'inversedBy' => 'dummies', - 'mappedBy' => null, - ], - 'relatedOwningDummy' => [ - 'targetEntity' => RelatedOwningDummy::class, - 'isOwningSide' => true, - 'inversedBy' => 'ownedDummy', - 'mappedBy' => null, - ], - ]); - $classMetadataProphecy->setFieldValue(Argument::any(), Argument::any(), Argument::any())->will(function ($args) { - $entity = $args[0]; - $field = $args[1]; - $value = $args[2]; - $entity->{$field} = $value; - }); - $this->emProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal()); + $this->cacheManagerProphecy = $this->createMock(CacheManager::class); + $this->cacheManagerProphecy->method('flush')->willReturn(0); + + $this->resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class); + $this->resourceClassResolverProphecy->method('isResourceClass')->willReturn(true); + $this->resourceClassResolverProphecy->method('getResourceClass')->willReturn(Dummy::class); - $this->propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); - $this->propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true); - $this->propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(false); - $this->propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummy')->willReturn(null); - $this->propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(null); + $this->uowProphecy = $this->createMock(UnitOfWork::class); - $this->metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $this->emProphecy = $this->createMock(EntityManagerInterface::class); + $this->emProphecy->method('getUnitOfWork')->willReturnCallback(fn() => $this->uowProphecy); + + $dummyClassMetadata = new ClassMetadata(Dummy::class); + $dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); + $dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']); + $dummyClassMetadata->wakeupReflection(new StaticReflectionService()); + $this->emProphecy->method('getClassMetadata')->with(Dummy::class)->willReturn($dummyClassMetadata); + + $this->propertyAccessorProphecy = $this->createMock(PropertyAccessorInterface::class); + $this->propertyAccessorProphecy->method('isReadable')->willReturnCallback(function ($obj, $prop) { + if ($obj instanceof Dummy && 'relatedDummy' === $prop) { + return true; + } + + return false; + }); + $this->propertyAccessorProphecy->method('getValue')->willReturn(null); + + $this->metadataFactoryProphecy = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); $operation = new GetCollection()->withShortName('Dummy')->withClass(Dummy::class); $operation2 = new GetCollection()->withShortName('DummyAsSubresource')->withClass(Dummy::class); - $this->metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ + $this->metadataFactoryProphecy->method('create')->with(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ new ApiResource('Dummy') ->withShortName('Dummy') ->withOperations(new Operations([ @@ -113,10 +99,21 @@ protected function setUp(): void { ])), ])); - $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $this->iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, $operation)->willReturn('/dummies'); - $this->iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, $operation2)->will(function ($args) { return '/related_dummies/'.$args[0]->getRelatedDummy()->getId().'/dummies'; }); - $this->iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class))->will(function ($args) { return '/dummies/'.$args[0]->getId(); }); + $this->iriConverterProphecy = $this->createMock(IriConverterInterface::class); + $this->iriConverterProphecy->method('getIriFromResource')->willReturnCallback(function (object|string $resource, ...$args) use ($operation, $operation2): ?string { + if ($resource instanceof Dummy) { + if (isset($args[1]) && $args[1] === $operation) { + return '/dummies'; + } + if (isset($args[1]) && $args[1] === $operation2) { + return '/related_dummies/'.$resource->getRelatedDummy()->getId().'/dummies'; + } + + return '/dummies/'.$resource->getId(); + } + + return null; + }); } /** @@ -136,36 +133,73 @@ public function testOnFlush(): void { $toDeleteNoPurge = new DummyNoGetOperation(); $toDeleteNoPurge->setId('5'); - $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($cacheManagerProphecy)->shouldBeCalled(); - $cacheManagerProphecy->invalidateTags(['/dummies/3'])->willReturn($cacheManagerProphecy)->shouldBeCalled(); - $cacheManagerProphecy->invalidateTags(['/dummies/4'])->willReturn($cacheManagerProphecy)->shouldBeCalled(); - $cacheManagerProphecy->flush()->willReturn(0); + $cacheManagerProphecy = $this->createMock(CacheManager::class); + $cacheManagerInvalidateTagsCalls = []; + $cacheManagerProphecy + ->method('invalidateTags') + ->willReturnCallback(function (array $tags) use (&$cacheManagerInvalidateTagsCalls, $cacheManagerProphecy) { + $cacheManagerInvalidateTagsCalls[] = $tags; - $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + return $cacheManagerProphecy; + }) + ; + $cacheManagerProphecy->method('flush')->willReturn(0); + + $metadataFactoryProphecy = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); $operation = new GetCollection()->withShortName('Dummy')->withClass(Dummy::class); - $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ - new ApiResource('Dummy') - ->withShortName('Dummy') - ->withOperations(new Operations([ - 'get' => $operation, - ])), - ]))->shouldBeCalled(); - $metadataFactoryProphecy->create(DummyNoGetOperation::class)->willReturn(new ResourceMetadataCollection('DummyNoGetOperation', [ - new ApiResource('DummyNoGetOperation') - ->withShortName('DummyNoGetOperation'), - ]))->shouldBeCalled(); - - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, Argument::type(GetCollection::class))->willReturn('/dummies')->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($toDelete1)->willReturn('/dummies/3')->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($toDelete2)->willReturn('/dummies/4')->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($toDeleteNoPurge)->willReturn(null)->shouldBeCalled(); - - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); - $resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class)->shouldBeCalled(); - $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyNoGetOperation::class))->willReturn(DummyNoGetOperation::class)->shouldBeCalled(); + $metadataFactoryProphecy + ->method('create') + ->willReturnCallback(function (string $class) use ($operation) { + switch ($class) { + case Dummy::class: + return new ResourceMetadataCollection('Dummy', [ + new ApiResource('Dummy') + ->withShortName('Dummy') + ->withOperations(new Operations([ + 'get' => $operation, + ])), + ]); + + case DummyNoGetOperation::class: + return new ResourceMetadataCollection('DummyNoGetOperation', [ + new ApiResource('DummyNoGetOperation') + ->withShortName('DummyNoGetOperation'), + ]); + + default: + TestCase::fail('Unexpected class passed to metadata factory: '.$class); + } + }) + ; + + $iriConverterProphecy = $this->createMock(IriConverterInterface::class); + $iriConverterProphecy + ->method('getIriFromResource') + ->willReturnCallback(function (object|string $resource, ...$args) use (&$toDelete1, &$toDelete2, &$toDeleteNoPurge): ?string { + if ($resource == $toDelete1) { + return '/dummies/3'; + } + if ($resource == $toDelete2) { + return '/dummies/4'; + } + if ($resource == $toDeleteNoPurge) { + return null; + } + if ($resource instanceof Dummy && isset($args[0], $args[1]) && UrlGeneratorInterface::ABS_PATH === $args[0] && $args[1] instanceof GetCollection) { + return '/dummies'; + } + + return null; + }) + ; + + $resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->expects($this->atLeastOnce())->method('isResourceClass')->willReturn(true); + $resourceClassResolverProphecy->method('getResourceClass')->willReturnCallback( + function ($resource): string { + return $resource::class; + } + ); $uowMock = $this->createMock(UnitOfWork::class); $uowMock->method('getScheduledEntityInsertions')->willReturn([$toInsert1, $toInsert2]); @@ -175,33 +209,41 @@ public function testOnFlush(): void { $uowMock->method('getScheduledCollectionDeletions')->willReturn([]); $uowMock->method('getEntityChangeSet')->willReturn([]); - $emProphecy = $this->prophesize(EntityManagerInterface::class); - $emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled(); - $emProphecy->detach(Argument::any()); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->atLeastOnce())->method('getUnitOfWork')->willReturn($uowMock); $dummyClassMetadata = new ClassMetadata(Dummy::class); $dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); $dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']); - $emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata)->shouldBeCalled(); - $emProphecy->getClassMetadata(DummyNoGetOperation::class)->willReturn(new ClassMetadata(DummyNoGetOperation::class))->shouldBeCalled(); - $em = $emProphecy->reveal(); - - $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); - $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true); - $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(false); - $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummy')->willReturn(null); - $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(null); + $entityManager->expects($this->atLeast(2))->method('getClassMetadata') + ->willReturnCallback(function (string $class) use ($dummyClassMetadata) { + return match ($class) { + Dummy::class => $dummyClassMetadata, + RelatedDummy::class => new ClassMetadata(DummyNoGetOperation::class), + DummyNoGetOperation::class => new ClassMetadata(DummyNoGetOperation::class), + default => throw new \InvalidArgumentException('Unexpected class: '.$class), + }; + }) + ; + + $propertyAccessorProphecy = $this->createMock(PropertyAccessorInterface::class); + $propertyAccessorProphecy->method('isReadable')->willReturnCallback(function ($obj, $prop) { + return $obj instanceof Dummy && in_array($prop, ['relatedDummy', 'relatedOwningDummy'], true) && ('relatedDummy' === $prop || 'relatedOwningDummy' === $prop); + }); + $propertyAccessorProphecy->method('getValue')->willReturn(null); $listener = new PurgeHttpCacheListener( - iriConverter: $iriConverterProphecy->reveal(), - resourceClassResolver: $resourceClassResolverProphecy->reveal(), - propertyAccessor: $propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $metadataFactoryProphecy->reveal(), - cacheManager: $cacheManagerProphecy->reveal(), - em: $em, + iriConverter: $iriConverterProphecy, + resourceClassResolver: $resourceClassResolverProphecy, + propertyAccessor: $propertyAccessorProphecy, + resourceMetadataCollectionFactory: $metadataFactoryProphecy, + cacheManager: $cacheManagerProphecy, + em: $entityManager, ); $listener->onFlush(); $listener->postFlush(); + + assertThat($cacheManagerInvalidateTagsCalls, logicalAnd(containsEqual(['/dummies']), containsEqual(['/dummies/3']), containsEqual(['/dummies/4']))); } public function testPreUpdate(): void { @@ -214,39 +256,61 @@ public function testPreUpdate(): void { $dummy = new Dummy(); $dummy->setId('1'); - $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->invalidateTags(['/related_dummies/old#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy); - $cacheManagerProphecy->invalidateTags(['/related_dummies/new#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy); - $cacheManagerProphecy->flush()->willReturn(0); - - $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource($oldRelatedDummy)->willReturn('/related_dummies/old')->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($newRelatedDummy)->willReturn('/related_dummies/new')->shouldBeCalled(); - - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); - - $emProphecy = $this->prophesize(EntityManagerInterface::class); + $cacheManagerProphecy = $this->createMock(CacheManager::class); + $cacheManagerProphecy->expects($this->exactly(2)) + ->method('invalidateTags') + ->willReturnCallback(function (array $tags) use ($cacheManagerProphecy) { + static $i = 0; + $expected = [ + ['/related_dummies/old#dummies'], + ['/related_dummies/new#dummies'], + ]; + TestCase::assertEquals($expected[$i], $tags); + ++$i; + + return $cacheManagerProphecy; + }) + ; + $cacheManagerProphecy->method('flush')->willReturn(0); + + $metadataFactoryProphecy = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + + $iriConverterProphecy = $this->createMock(IriConverterInterface::class); + $iriConverterProphecy->expects($this->exactly(2)) + ->method('getIriFromResource') + ->willReturnCallback(function ($resource) use ($oldRelatedDummy, $newRelatedDummy) { + static $i = 0; + $expected = [$oldRelatedDummy, $newRelatedDummy]; + TestCase::assertSame($expected[$i], $resource); + $ret = ['/related_dummies/old', '/related_dummies/new'][$i]; + ++$i; + + return $ret; + }) + ; + + $resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->expects($this->atLeastOnce())->method('isResourceClass')->willReturn(true); + + $emProphecy = $this->createMock(EntityManagerInterface::class); $classMetadata = new ClassMetadata(Dummy::class); $classMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); - $emProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadata)->shouldBeCalled(); + $emProphecy->expects($this->once())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadata); $changeSet = ['relatedDummy' => [$oldRelatedDummy, $newRelatedDummy]]; - $em = $emProphecy->reveal(); + $em = $emProphecy; $eventArgs = new PreUpdateEventArgs($dummy, $em, $changeSet); - $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy = $this->createMock(PropertyAccessorInterface::class); $listener = new PurgeHttpCacheListener( - iriConverter: $iriConverterProphecy->reveal(), - resourceClassResolver: $resourceClassResolverProphecy->reveal(), - propertyAccessor: $propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $metadataFactoryProphecy->reveal(), - cacheManager: $cacheManagerProphecy->reveal(), - em: $em, + iriConverter: $iriConverterProphecy, + resourceClassResolver: $resourceClassResolverProphecy, + propertyAccessor: $propertyAccessorProphecy, + resourceMetadataCollectionFactory: $metadataFactoryProphecy, + cacheManager: $cacheManagerProphecy, + em: $emProphecy, ); $listener->preUpdate($eventArgs); $listener->postFlush(); @@ -256,37 +320,37 @@ public function testNothingToPurge(): void { $dummyNoGetOperation = new DummyNoGetOperation(); $dummyNoGetOperation->setId('1'); - $purgerProphecy = $this->prophesize(PurgerInterface::class); - $purgerProphecy->purge([])->shouldNotBeCalled(); + $purgerProphecy = $this->createMock(PurgerInterface::class); + $purgerProphecy->expects($this->never())->method('purge'); - $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->invalidateTags(Argument::any())->shouldNotBeCalled(); - $cacheManagerProphecy->flush()->willReturn(0); + $cacheManagerProphecy = $this->createMock(CacheManager::class); + $cacheManagerProphecy->expects($this->never())->method('invalidateTags'); + $cacheManagerProphecy->method('flush')->willReturn(0); - $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $metadataFactoryProphecy = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy = $this->createMock(IriConverterInterface::class); - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class); - $emProphecy = $this->prophesize(EntityManagerInterface::class); + $emProphecy = $this->createMock(EntityManagerInterface::class); $classMetadata = new ClassMetadata(DummyNoGetOperation::class); - $emProphecy->getClassMetadata(DummyNoGetOperation::class)->willReturn($classMetadata)->shouldBeCalled(); + $emProphecy->expects($this->once())->method('getClassMetadata')->with(DummyNoGetOperation::class)->willReturn($classMetadata); $changeSet = ['lorem' => 'ipsum']; - $em = $emProphecy->reveal(); + $em = $emProphecy; $eventArgs = new PreUpdateEventArgs($dummyNoGetOperation, $em, $changeSet); - $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy = $this->createMock(PropertyAccessorInterface::class); $listener = new PurgeHttpCacheListener( - iriConverter: $iriConverterProphecy->reveal(), - resourceClassResolver: $resourceClassResolverProphecy->reveal(), - propertyAccessor: $propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $metadataFactoryProphecy->reveal(), - cacheManager: $cacheManagerProphecy->reveal(), - em: $em, + iriConverter: $iriConverterProphecy, + resourceClassResolver: $resourceClassResolverProphecy, + propertyAccessor: $propertyAccessorProphecy, + resourceMetadataCollectionFactory: $metadataFactoryProphecy, + cacheManager: $cacheManagerProphecy, + em: $emProphecy, ); $listener->preUpdate($eventArgs); $listener->postFlush(); @@ -295,42 +359,42 @@ public function testNothingToPurge(): void { public function testNotAResourceClass(): void { $nonResource = new NotAResource('foo', 'bar'); - $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->invalidateTags(Argument::any())->shouldNotBeCalled(); - $cacheManagerProphecy->flush()->willReturn(0); + $cacheManagerProphecy = $this->createMock(CacheManager::class); + $cacheManagerProphecy->expects($this->never())->method('invalidateTags'); + $cacheManagerProphecy->method('flush')->willReturn(0); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource($nonResource)->shouldNotBeCalled(); + $iriConverterProphecy = $this->createMock(IriConverterInterface::class); + $iriConverterProphecy->expects($this->never())->method('getIriFromResource'); - $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $metadataFactoryProphecy = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false)->shouldBeCalled(); + $resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->expects($this->once())->method('isResourceClass')->with(NotAResource::class)->willReturn(false); - $uowProphecy = $this->prophesize(UnitOfWork::class); - $uowProphecy->getScheduledEntityInsertions()->willReturn([$nonResource])->shouldBeCalled(); - $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled(); - $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled(); - $uowProphecy->getScheduledCollectionUpdates()->willReturn([])->shouldBeCalled(); - $uowProphecy->getScheduledCollectionDeletions()->willReturn([])->shouldBeCalled(); + $uowProphecy = $this->createMock(UnitOfWork::class); + $uowProphecy->expects($this->once())->method('getScheduledEntityInsertions')->willReturn([$nonResource]); + $uowProphecy->expects($this->once())->method('getScheduledEntityDeletions')->willReturn([]); + $uowProphecy->expects($this->once())->method('getScheduledEntityUpdates')->willReturn([]); + $uowProphecy->expects($this->once())->method('getScheduledCollectionUpdates')->willReturn([]); + $uowProphecy->expects($this->once())->method('getScheduledCollectionDeletions')->willReturn([]); - $emProphecy = $this->prophesize(EntityManagerInterface::class); - $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); + $emProphecy = $this->createMock(EntityManagerInterface::class); + $emProphecy->method('getUnitOfWork')->willReturn($uowProphecy); $dummyClassMetadata = new ClassMetadata(ContainNonResource::class); - $emProphecy->getClassMetadata(NotAResource::class)->willReturn($dummyClassMetadata); - $em = $emProphecy->reveal(); + $emProphecy->expects($this->once())->method('getClassMetadata')->with(NotAResource::class)->willReturn($dummyClassMetadata); + $em = $emProphecy; new OnFlushEventArgs($em); - $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy = $this->createMock(PropertyAccessorInterface::class); $listener = new PurgeHttpCacheListener( - iriConverter: $iriConverterProphecy->reveal(), - resourceClassResolver: $resourceClassResolverProphecy->reveal(), - propertyAccessor: $propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $metadataFactoryProphecy->reveal(), - cacheManager: $cacheManagerProphecy->reveal(), - em: $em, + iriConverter: $iriConverterProphecy, + resourceClassResolver: $resourceClassResolverProphecy, + propertyAccessor: $propertyAccessorProphecy, + resourceMetadataCollectionFactory: $metadataFactoryProphecy, + cacheManager: $cacheManagerProphecy, + em: $emProphecy, ); $listener->onFlush(); } @@ -339,55 +403,74 @@ public function testPropertyIsNotAResourceClass(): void { $containNonResource = new ContainNonResource(); $nonResource = new NotAResource('foo', 'bar'); - $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->invalidateTags(Argument::any())->shouldNotBeCalled(); - $cacheManagerProphecy->flush()->willReturn(0); + $cacheManagerProphecy = $this->createMock(CacheManager::class); + $cacheManagerProphecy->expects($this->never())->method('invalidateTags'); + $cacheManagerProphecy->method('flush')->willReturn(0); - $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $metadataFactoryProphecy->create(ContainNonResource::class)->willReturn(new ResourceMetadataCollection('ContainNonResource', [ + $metadataFactoryProphecy = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $metadataFactoryProphecy->expects($this->once())->method('create')->with(ContainNonResource::class)->willReturn(new ResourceMetadataCollection('ContainNonResource', [ new ApiResource('ContainNonResource') ->withShortName('ContainNonResource'), - ]))->shouldBeCalled(); + ])); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(ContainNonResource::class, UrlGeneratorInterface::ABS_PATH, Argument::any())->willReturn('/dummies/1'); - $iriConverterProphecy->getIriFromResource($nonResource)->shouldNotBeCalled(); + $iriConverterProphecy = $this->createMock(IriConverterInterface::class); + $that = $this; + $iriConverterProphecy->method('getIriFromResource')->willReturnCallback(function (object|string $resource, ...$args) use ($nonResource, $that): ?string { + $that->assertNotSame($nonResource, $resource, 'getIriFromResource should not be called with non-resource'); + if (ContainNonResource::class === $resource) { + return '/dummies/1'; + } - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->getResourceClass(Argument::type(ContainNonResource::class))->willReturn(ContainNonResource::class)->shouldBeCalled(); - $resourceClassResolverProphecy->isResourceClass(ContainNonResource::class)->willReturn(true)->shouldBeCalled(); - $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false)->shouldBeCalled(); + return null; + }); + + $resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->method('getResourceClass')->willReturn(ContainNonResource::class); + $resourceClassResolverProphecy->expects($this->exactly(2))->method('isResourceClass')->willReturnCallback(function (string $class) { + if (ContainNonResource::class === $class) { + return true; + } + if (NotAResource::class === $class) { + return false; + } + TestCase::fail('Unexpected class passed to isResourceClass: '.$class); + }); - $uowProphecy = $this->prophesize(UnitOfWork::class); - $uowProphecy->getScheduledEntityInsertions()->willReturn([$containNonResource])->shouldBeCalled(); - $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled(); - $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled(); - $uowProphecy->getScheduledCollectionUpdates()->willReturn([])->shouldBeCalled(); - $uowProphecy->getScheduledCollectionDeletions()->willReturn([])->shouldBeCalled(); + $uowProphecy = $this->createMock(UnitOfWork::class); + $uowProphecy->expects($this->once())->method('getScheduledEntityInsertions')->willReturn([$containNonResource]); + $uowProphecy->expects($this->once())->method('getScheduledEntityDeletions')->willReturn([]); + $uowProphecy->expects($this->once())->method('getScheduledEntityUpdates')->willReturn([]); + $uowProphecy->expects($this->once())->method('getScheduledCollectionUpdates')->willReturn([]); + $uowProphecy->expects($this->once())->method('getScheduledCollectionDeletions')->willReturn([]); - $emProphecy = $this->prophesize(EntityManagerInterface::class); - $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); + $emProphecy = $this->createMock(EntityManagerInterface::class); + $emProphecy->expects($this->once())->method('getUnitOfWork')->willReturn($uowProphecy); $dummyClassMetadata = new ClassMetadata(ContainNonResource::class); $dummyClassMetadata->mapManyToOne(['fieldName' => 'notAResource', 'targetEntity' => NotAResource::class, 'inversedBy' => 'resources']); $dummyClassMetadata->mapOneToMany(['fieldName' => 'collectionOfNotAResource', 'targetEntity' => NotAResource::class, 'mappedBy' => 'resource']); - $emProphecy->getClassMetadata(ContainNonResource::class)->willReturn($dummyClassMetadata); - $em = $emProphecy->reveal(); + $emProphecy->method('getClassMetadata')->willReturnCallback(function (string $class) use ($dummyClassMetadata) { + return match ($class) { + ContainNonResource::class => $dummyClassMetadata, + default => new ClassMetadata($class), + }; + }); + $em = $emProphecy; new OnFlushEventArgs($em); - $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); - $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'notAResource')->willReturn(true); - $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->willReturn(true); - $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'notAResource')->shouldNotBeCalled(); - $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); + $propertyAccessorProphecy = $this->createMock(PropertyAccessorInterface::class); + $propertyAccessorProphecy->method('isReadable')->willReturnCallback(function ($obj, $prop) { + return $obj instanceof ContainNonResource && in_array($prop, ['notAResource', 'collectionOfNotAResource'], true); + }); + $propertyAccessorProphecy->expects($this->never())->method('getValue'); $listener = new PurgeHttpCacheListener( - iriConverter: $iriConverterProphecy->reveal(), - resourceClassResolver: $resourceClassResolverProphecy->reveal(), - propertyAccessor: $propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $metadataFactoryProphecy->reveal(), - cacheManager: $cacheManagerProphecy->reveal(), - em: $em, + iriConverter: $iriConverterProphecy, + resourceClassResolver: $resourceClassResolverProphecy, + propertyAccessor: $propertyAccessorProphecy, + resourceMetadataCollectionFactory: $metadataFactoryProphecy, + cacheManager: $cacheManagerProphecy, + em: $emProphecy, ); $listener->onFlush(); } @@ -403,25 +486,37 @@ public function testInsertingShouldPurgeSubresourceCollections(): void { $relatedDummy->setId('100'); $toInsert1->setRelatedDummy($relatedDummy); - $this->uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1]); - $this->uowProphecy->getScheduledEntityDeletions()->willReturn([]); - $this->uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled(); - $this->uowProphecy->getScheduledCollectionUpdates()->willReturn([]); - $this->uowProphecy->getScheduledCollectionDeletions()->willReturn([]); + $this->uowProphecy->method('getScheduledEntityInsertions')->willReturn([$toInsert1]); + $this->uowProphecy->method('getScheduledEntityDeletions')->willReturn([]); + $this->uowProphecy->expects($this->once())->method('getScheduledEntityUpdates')->willReturn([]); + $this->uowProphecy->method('getScheduledCollectionUpdates')->willReturn([]); + $this->uowProphecy->method('getScheduledCollectionDeletions')->willReturn([]); // then - $this->cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); - $this->cacheManagerProphecy->invalidateTags(['/related_dummies/100/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->expects($this->exactly(2)) + ->method('invalidateTags') + ->willReturnCallback(function (array $tags) { + static $i = 0; + $expected = [ + ['/dummies'], + ['/related_dummies/100/dummies'], + ]; + TestCase::assertEquals($expected[$i], $tags); + ++$i; + + return $this->cacheManagerProphecy; + }) + ; // when - $em = $this->emProphecy->reveal(); + $em = $this->emProphecy; $listener = new PurgeHttpCacheListener( - iriConverter: $this->iriConverterProphecy->reveal(), - resourceClassResolver: $this->resourceClassResolverProphecy->reveal(), - propertyAccessor: $this->propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $this->metadataFactoryProphecy->reveal(), - cacheManager: $this->cacheManagerProphecy->reveal(), + iriConverter: $this->iriConverterProphecy, + resourceClassResolver: $this->resourceClassResolverProphecy, + propertyAccessor: $this->propertyAccessorProphecy, + resourceMetadataCollectionFactory: $this->metadataFactoryProphecy, + cacheManager: $this->cacheManagerProphecy, em: $em, ); $listener->onFlush(); @@ -436,30 +531,41 @@ public function testDeleteShouldPurgeSubresourceCollections(): void { $relatedDummy->setId('100'); $toDelete1->setRelatedDummy($relatedDummy); - $uowMock = $this->createMock(UnitOfWork::class); - $uowMock->method('getScheduledEntityInsertions')->willReturn([]); - $uowMock->method('getScheduledEntityUpdates')->willReturn([]); - $uowMock->method('getScheduledEntityDeletions')->willReturn([$toDelete1]); - $uowMock->method('getScheduledCollectionUpdates')->willReturn([]); - $uowMock->method('getScheduledCollectionDeletions')->willReturn([]); - $uowMock->method('getEntityChangeSet')->willReturn([]); + $unitOfWork = $this->createMock(UnitOfWork::class); + $unitOfWork->method('getScheduledEntityInsertions')->willReturn([]); + $unitOfWork->method('getScheduledEntityUpdates')->willReturn([]); + $unitOfWork->method('getScheduledEntityDeletions')->willReturn([$toDelete1]); + $unitOfWork->method('getScheduledCollectionUpdates')->willReturn([]); + $unitOfWork->method('getScheduledCollectionDeletions')->willReturn([]); + $unitOfWork->method('getEntityChangeSet')->willReturn([]); - $this->emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled(); + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getUnitOfWork')->willReturn($unitOfWork); // then - $this->cacheManagerProphecy->invalidateTags(['/dummies/1'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); - $this->cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); - $this->cacheManagerProphecy->invalidateTags(['/related_dummies/100/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->expects($this->exactly(3)) + ->method('invalidateTags') + ->willReturnCallback(function (array $tags) { + static $i = 0; + $expected = [ + ['/dummies/1'], + ['/dummies'], + ['/related_dummies/100/dummies'], + ]; + TestCase::assertEquals($expected[$i], $tags); + ++$i; + + return $this->cacheManagerProphecy; + }) + ; // when - - $em = $this->emProphecy->reveal(); $listener = new PurgeHttpCacheListener( - iriConverter: $this->iriConverterProphecy->reveal(), - resourceClassResolver: $this->resourceClassResolverProphecy->reveal(), - propertyAccessor: $this->propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $this->metadataFactoryProphecy->reveal(), - cacheManager: $this->cacheManagerProphecy->reveal(), + iriConverter: $this->iriConverterProphecy, + resourceClassResolver: $this->resourceClassResolverProphecy, + propertyAccessor: $this->propertyAccessorProphecy, + resourceMetadataCollectionFactory: $this->metadataFactoryProphecy, + cacheManager: $this->cacheManagerProphecy, em: $em, ); $listener->onFlush(); @@ -477,31 +583,39 @@ public function testUpdateShouldPurgeSubresourceCollections(): void { $relatedDummyOld = new RelatedDummy(); $relatedDummyOld->setId('99'); - $uowMock = $this->createMock(UnitOfWork::class); - $uowMock->method('getScheduledEntityInsertions')->willReturn([]); - $uowMock->method('getScheduledEntityUpdates')->willReturn([$toUpdate1]); - $uowMock->method('getScheduledEntityDeletions')->willReturn([]); - $uowMock->method('getScheduledCollectionUpdates')->willReturn([]); - $uowMock->method('getScheduledCollectionDeletions')->willReturn([]); - $uowMock->method('getEntityChangeSet')->willReturn(['relatedDummy' => [$relatedDummyOld, $relatedDummy]]); - - $this->emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled(); + $this->uowProphecy = $this->createMock(UnitOfWork::class); + $this->uowProphecy->method('getScheduledEntityInsertions')->willReturn([]); + $this->uowProphecy->method('getScheduledEntityUpdates')->willReturn([$toUpdate1]); + $this->uowProphecy->method('getScheduledEntityDeletions')->willReturn([]); + $this->uowProphecy->method('getScheduledCollectionUpdates')->willReturn([]); + $this->uowProphecy->method('getScheduledCollectionDeletions')->willReturn([]); + $this->uowProphecy->method('getEntityChangeSet')->willReturn(['relatedDummy' => [$relatedDummyOld, $relatedDummy]]); // then - $this->cacheManagerProphecy->invalidateTags(['/dummies/1'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); - $this->cacheManagerProphecy->invalidateTags(['/related_dummies/100/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); - $this->cacheManagerProphecy->invalidateTags(['/related_dummies/99/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->expects($this->exactly(3)) + ->method('invalidateTags') + ->willReturnCallback(function (array $tags) { + static $i = 0; + $expected = [ + ['/dummies/1'], + ['/related_dummies/100/dummies'], + ['/related_dummies/99/dummies'], + ]; + TestCase::assertEquals($expected[$i], $tags); + ++$i; + + return $this->cacheManagerProphecy; + }) + ; // when - - $em = $this->emProphecy->reveal(); $listener = new PurgeHttpCacheListener( - iriConverter: $this->iriConverterProphecy->reveal(), - resourceClassResolver: $this->resourceClassResolverProphecy->reveal(), - propertyAccessor: $this->propertyAccessorProphecy->reveal(), - resourceMetadataCollectionFactory: $this->metadataFactoryProphecy->reveal(), - cacheManager: $this->cacheManagerProphecy->reveal(), - em: $em, + iriConverter: $this->iriConverterProphecy, + resourceClassResolver: $this->resourceClassResolverProphecy, + propertyAccessor: $this->propertyAccessorProphecy, + resourceMetadataCollectionFactory: $this->metadataFactoryProphecy, + cacheManager: $this->cacheManagerProphecy, + em: $this->emProphecy, ); $listener->onFlush(); $listener->postFlush();