diff --git a/bundle/Controller/Callback/Cloudinary/Notify.php b/bundle/Controller/Callback/Cloudinary/Notify.php index ec75b015..691a9053 100644 --- a/bundle/Controller/Callback/Cloudinary/Notify.php +++ b/bundle/Controller/Callback/Cloudinary/Notify.php @@ -8,8 +8,10 @@ use Cloudinary\Api\Upload\UploadApi; use Doctrine\ORM\EntityManagerInterface; use Netgen\RemoteMedia\API\ProviderInterface; +use Netgen\RemoteMedia\API\Values\Folder; use Netgen\RemoteMedia\API\Values\RemoteResource; use Netgen\RemoteMedia\Core\Provider\Cloudinary\CacheableGatewayInterface; +use Netgen\RemoteMedia\Core\Provider\Cloudinary\CloudinaryProvider; use Netgen\RemoteMedia\Core\Provider\Cloudinary\CloudinaryRemoteId; use Netgen\RemoteMedia\Core\Provider\Cloudinary\GatewayInterface; use Netgen\RemoteMedia\Core\RequestVerifierInterface; @@ -31,35 +33,23 @@ final class Notify extends AbstractController { private const RESOURCE_UPLOAD = 'upload'; private const RESOURCE_DELETE = 'delete'; + private const RESOURCE_MOVE = 'move'; private const RESOURCE_TAGS_CHANGED = 'resource_tags_changed'; private const RESOURCE_CONTEXT_CHANGED = 'resource_context_changed'; private const RESOURCE_RENAME = 'rename'; + private const RESOURCE_DISPLAY_NAME_CHANGED = 'resource_display_name_changed'; private const FOLDER_CREATE = 'create_folder'; private const FOLDER_DELETE = 'delete_folder'; - - private GatewayInterface $gateway; - - private ProviderInterface $provider; - - private RequestVerifierInterface $signatureVerifier; - - private EntityManagerInterface $entityManager; - - private EventDispatcherInterface $eventDispatcher; + private const FOLDER_MOVE_RENAME = 'move_or_rename_asset_folder'; public function __construct( - GatewayInterface $gateway, - ProviderInterface $provider, - RequestVerifierInterface $signatureVerifier, - EntityManagerInterface $entityManager, - EventDispatcherInterface $eventDispatcher, - ) { - $this->gateway = $gateway; - $this->provider = $provider; - $this->signatureVerifier = $signatureVerifier; - $this->entityManager = $entityManager; - $this->eventDispatcher = $eventDispatcher; - } + private GatewayInterface $gateway, + private ProviderInterface $provider, + private RequestVerifierInterface $signatureVerifier, + private EntityManagerInterface $entityManager, + private EventDispatcherInterface $eventDispatcher, + private string $folderMode, + ) {} public function __invoke(Request $request): Response { @@ -84,6 +74,11 @@ public function __invoke(Request $request): Response break; + case self::RESOURCE_MOVE: + $this->handleResourceMoved($requestContent); + + break; + case self::RESOURCE_TAGS_CHANGED: $this->handleTagsChanged($requestContent); @@ -99,8 +94,14 @@ public function __invoke(Request $request): Response break; + case self::RESOURCE_DISPLAY_NAME_CHANGED: + $this->handleDisplayNameChanged($requestContent); + + break; + case self::FOLDER_CREATE: case self::FOLDER_DELETE: + case self::FOLDER_MOVE_RENAME: $this->handleFoldersChanged(); break; @@ -140,7 +141,7 @@ private function handleResourceUploaded(array $requestContent): void $resource ->setUrl($this->gateway->getDownloadLink($cloudinaryRemoteId)) - ->setName(pathinfo($cloudinaryRemoteId->getResourceId(), PATHINFO_FILENAME)) + ->setName($this->resolveName($requestContent)) ->setVersion((string) $requestContent['version']) ->setSize($requestContent['bytes']) ->setTags($requestContent['tags']); @@ -176,6 +177,42 @@ private function handleResourceDeleted(array $requestContent): void } } + private function handleResourceMoved(array $requestContent): void + { + if ($this->folderMode !== CloudinaryProvider::FOLDER_MODE_DYNAMIC) { + return; + } + + if ($this->gateway instanceof CacheableGatewayInterface) { + $this->gateway->invalidateResourceListCache(); + $this->gateway->invalidateFoldersCache(); + } + + foreach ($requestContent['resources'] ?? [] as $publicId => $resourceData) { + $cloudinaryRemoteId = new CloudinaryRemoteId( + $resourceData['type'], + $resourceData['resource_type'], + (string) $publicId, + ); + + $this->gateway->invalidateResourceCache($cloudinaryRemoteId); + + try { + $resource = $this->provider->loadByRemoteId($cloudinaryRemoteId->getRemoteId()); + } catch (RemoteResourceNotFoundException $e) { + continue; + } + + $resource->setFolder(Folder::fromPath($resourceData['to_asset_folder'])); + + if (($resourceData['display_name'] ?? null) !== null) { + $resource->setName($resourceData['display_name']); + } + + $this->provider->store($resource); + } + } + /** * This method is a bit hacky due to inconsistent Cloudinary API response. */ @@ -216,7 +253,7 @@ private function handleResourceRenamed(array $requestContent): void $resource ->setRemoteId($cloudinaryRemoteId->getRemoteId()) - ->setName(pathinfo($cloudinaryRemoteId->getResourceId(), PATHINFO_FILENAME)) + ->setName($this->resolveName($requestContent)) ->setUrl($this->gateway->getDownloadLink($cloudinaryRemoteId)) ->setFolder($cloudinaryRemoteId->getFolder()); @@ -234,6 +271,41 @@ private function handleResourceRenamed(array $requestContent): void } } + private function handleDisplayNameChanged(array $requestContent): void + { + if ($this->folderMode !== CloudinaryProvider::FOLDER_MODE_DYNAMIC) { + return; + } + + if ($this->gateway instanceof CacheableGatewayInterface) { + $this->gateway->invalidateResourceListCache(); + } + + foreach ($requestContent['resources'] ?? [] as $resourceData) { + $cloudinaryRemoteId = new CloudinaryRemoteId( + $resourceData['type'], + $resourceData['resource_type'], + (string) $resourceData['public_id'], + ); + + if ($this->gateway instanceof CacheableGatewayInterface) { + $this->gateway->invalidateResourceCache($cloudinaryRemoteId); + } + + try { + $resource = $this->provider->loadByRemoteId( + $cloudinaryRemoteId->getRemoteId(), + ); + } catch (RemoteResourceNotFoundException $e) { + continue; + } + + $resource->setName($resourceData['new_display_name']); + + $this->provider->store($resource); + } + } + private function handleTagsChanged(array $requestContent): void { if ($this->gateway instanceof CacheableGatewayInterface) { @@ -382,4 +454,13 @@ private function handleFoldersChanged(): void $this->gateway->invalidateFoldersCache(); } } + + private function resolveName(array $data): string + { + $cloudinaryRemoteId = CloudinaryRemoteId::fromCloudinaryData($data); + + return $this->folderMode === CloudinaryProvider::FOLDER_MODE_FIXED + ? pathinfo($cloudinaryRemoteId->getResourceId(), PATHINFO_FILENAME) + : $data['display_name'] ?? pathinfo($cloudinaryRemoteId->getResourceId(), PATHINFO_FILENAME); + } } diff --git a/bundle/Resources/config/services/controllers.yaml b/bundle/Resources/config/services/controllers.yaml index 50d94b92..30539d60 100644 --- a/bundle/Resources/config/services/controllers.yaml +++ b/bundle/Resources/config/services/controllers.yaml @@ -41,5 +41,6 @@ services: - '@netgen_remote_media.provider.cloudinary.verifier.controller.signature' - '@doctrine.orm.entity_manager' - '@event_dispatcher' + - '%netgen_remote_media.cloudinary.folder_mode%' calls: - [setContainer, ['@service_container']] diff --git a/tests/bundle/Controller/Callback/Cloudinary/NotifyTest.php b/tests/bundle/Controller/Callback/Cloudinary/NotifyTest.php index 11c4183e..78b994b3 100644 --- a/tests/bundle/Controller/Callback/Cloudinary/NotifyTest.php +++ b/tests/bundle/Controller/Callback/Cloudinary/NotifyTest.php @@ -7,8 +7,10 @@ use Doctrine\ORM\EntityManagerInterface; use Netgen\Bundle\RemoteMediaBundle\Controller\Callback\Cloudinary\Notify as NotifyController; use Netgen\RemoteMedia\API\ProviderInterface; +use Netgen\RemoteMedia\API\Values\Folder; use Netgen\RemoteMedia\API\Values\RemoteResource; use Netgen\RemoteMedia\Core\Provider\Cloudinary\CacheableGatewayInterface; +use Netgen\RemoteMedia\Core\Provider\Cloudinary\CloudinaryProvider; use Netgen\RemoteMedia\Core\Provider\Cloudinary\CloudinaryRemoteId; use Netgen\RemoteMedia\Core\RequestVerifierInterface; use Netgen\RemoteMedia\Event\Cloudinary\NotificationReceivedEvent; @@ -29,7 +31,9 @@ #[CoversClass(NotifyController::class)] final class NotifyTest extends TestCase { - private NotifyController $controller; + private NotifyController $fixedFolderModeController; + + private NotifyController $dynamicFolderModeController; private CacheableGatewayInterface|MockObject $gatewayMock; @@ -49,12 +53,22 @@ protected function setUp(): void $this->entityManagerMock = $this->createMock(EntityManagerInterface::class); $this->eventDispatcherMock = $this->createMock(EventDispatcherInterface::class); - $this->controller = new NotifyController( + $this->fixedFolderModeController = new NotifyController( + $this->gatewayMock, + $this->providerMock, + $this->signatureVerifierMock, + $this->entityManagerMock, + $this->eventDispatcherMock, + CloudinaryProvider::FOLDER_MODE_FIXED, + ); + + $this->dynamicFolderModeController = new NotifyController( $this->gatewayMock, $this->providerMock, $this->signatureVerifierMock, $this->entityManagerMock, $this->eventDispatcherMock, + CloudinaryProvider::FOLDER_MODE_DYNAMIC, ); } @@ -68,7 +82,7 @@ public function testUnverified(): void ->with($request) ->willReturn(false); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class, @@ -166,7 +180,7 @@ public function testResourceUploaded(): void ->with($cloudinaryRemoteId->getRemoteId()) ->willThrowException(new RemoteResourceNotFoundException($cloudinaryRemoteId->getRemoteId())); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class, @@ -281,7 +295,7 @@ public function testResourceRewritten(): void ->with($resource) ->willReturn($resource); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class, @@ -404,7 +418,12 @@ public function testResourcesDeleted(): void static fn (string $remoteId): ?RemoteResource => match ($remoteId) { $cloudinaryRemoteId1->getRemoteId() => $resource, $cloudinaryRemoteId2->getRemoteId() => $resource2, - default => null, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "loadByRemoteId" with value "%s" matches one of the expecting values.', + $resource->getRemoteId(), + ), + ), }, ); @@ -423,7 +442,7 @@ public function testResourcesDeleted(): void }, ); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class, @@ -509,7 +528,508 @@ public function testResourceDeletedNotFound(): void ->with($cloudinaryRemoteId->getRemoteId()) ->willThrowException(new RemoteResourceNotFoundException($cloudinaryRemoteId->getRemoteId())); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); + + self::assertInstanceOf( + JsonResponse::class, + $response, + ); + + self::assertSame( + '"Notification handled."', + $response->getContent(), + ); + + self::assertSame( + Response::HTTP_OK, + $response->getStatusCode(), + ); + } + + public function testResourceMovedFixed(): void + { + $body = json_encode([ + 'notification_type' => 'move', + 'resources' => [ + 'sample' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'from_asset_folder' => 'clothing', + 'to_asset_folder' => 'clothing_sale', + 'display_name' => 'blue_sweater', + ], + ], + ]); + + $request = new Request( + [], + [], + [], + [], + [], + [], + $body, + ); + + $request->headers->add( + [ + 'x-cld-timestamp' => time(), + 'x-cld-signature' => 'test', + ], + ); + + $this->signatureVerifierMock + ->expects(self::once()) + ->method('verify') + ->with($request) + ->willReturn(true); + + $event = new NotificationReceivedEvent($request); + + $this->eventDispatcherMock + ->expects(self::once()) + ->method('dispatch') + ->with($event, $event::NAME); + + $this->gatewayMock + ->expects(self::never()) + ->method('invalidateResourceListCache'); + + $this->gatewayMock + ->expects(self::never()) + ->method('invalidateFoldersCache'); + + $this->gatewayMock + ->expects(self::never()) + ->method('invalidateResourceCache'); + + $this->providerMock + ->expects(self::never()) + ->method('loadByRemoteId'); + + $this->providerMock + ->expects(self::never()) + ->method('store'); + + $response = $this->fixedFolderModeController->__invoke($request); + + self::assertInstanceOf( + JsonResponse::class, + $response, + ); + + self::assertSame( + '"Notification handled."', + $response->getContent(), + ); + + self::assertSame( + Response::HTTP_OK, + $response->getStatusCode(), + ); + } + + public function testResourceMovedDynamic(): void + { + $body = json_encode([ + 'notification_type' => 'move', + 'resources' => [ + 'blue_sweater' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'from_asset_folder' => 'clothing', + 'to_asset_folder' => 'clothing_sale', + 'display_name' => 'blue_sweater', + ], + 'red_shirt' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'from_asset_folder' => 'shirts', + 'to_asset_folder' => 'old_shirts', + 'display_name' => 'red shirt', + ], + 'non_existing_shirt' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'from_asset_folder' => 'shirts', + 'to_asset_folder' => 'old_shirts', + 'display_name' => 'some shirt', + ], + 'black_pants' => [ + 'resource_type' => 'video', + 'type' => 'upload', + 'from_asset_folder' => 'clothing/pants', + 'to_asset_folder' => 'clothing_sale/pants', + 'display_name' => 'Black pants', + ], + ], + ]); + + $request = new Request( + [], + [], + [], + [], + [], + [], + $body, + ); + + $request->headers->add( + [ + 'x-cld-timestamp' => time(), + 'x-cld-signature' => 'test', + ], + ); + + $this->signatureVerifierMock + ->expects(self::once()) + ->method('verify') + ->with($request) + ->willReturn(true); + + $event = new NotificationReceivedEvent($request); + + $this->eventDispatcherMock + ->expects(self::once()) + ->method('dispatch') + ->with($event, $event::NAME); + + $this->gatewayMock + ->expects(self::once()) + ->method('invalidateResourceListCache'); + + $this->gatewayMock + ->expects(self::once()) + ->method('invalidateFoldersCache'); + + $this->gatewayMock + ->expects(self::exactly(4)) + ->method('invalidateResourceCache') + ->willReturnCallback( + static fn (CloudinaryRemoteId $cloudinaryRemoteId) => match ($cloudinaryRemoteId->getRemoteId()) { + 'upload|image|blue_sweater', 'upload|image|red_shirt', 'upload|image|non_existing_shirt', 'upload|video|black_pants' => null, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "invalidateResourceCache" with value "%s" matches one of the expecting values.', + $cloudinaryRemoteId->getRemoteId(), + ), + ), + }, + ); + + $blueSweater = new RemoteResource( + remoteId: 'upload|image|blue_sweater', + type: 'image', + url: 'https://res.cloudinary.com/demo/image/upload/blue_sweater', + md5: 'r43tr4t45454324342', + id: 5, + name: 'Sweater (blue)', + folder: Folder::fromPath('clothing'), + size: 380250, + ); + + $redShirt = new RemoteResource( + remoteId: 'upload|image|red_shirt', + type: 'image', + url: 'https://res.cloudinary.com/demo/video/image/red_shirt', + md5: '3r43456fdgregregre', + id: 6, + name: 'red shirt', + folder: Folder::fromPath('shirts'), + size: 3802350, + ); + + $blackPants = new RemoteResource( + remoteId: 'upload|video|black_pants', + type: 'video', + url: 'https://res.cloudinary.com/demo/video/upload/black_pants', + md5: '3r43456fdgregregre', + id: 6, + name: 'black_pants', + folder: Folder::fromPath('clothing/pants'), + size: 329987438, + ); + + $this->providerMock + ->expects(self::exactly(4)) + ->method('loadByRemoteId') + ->willReturnCallback( + static fn (string $remoteId): ?RemoteResource => match ($remoteId) { + 'upload|image|blue_sweater' => $blueSweater, + 'upload|image|red_shirt' => $redShirt, + 'upload|image|non_existing_shirt' => throw new RemoteResourceNotFoundException('upload|image|non_existing_shirt'), + 'upload|video|black_pants' => $blackPants, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "loadByRemoteId" with value "%s" matches one of the expecting values.', + $remoteId, + ), + ), + }, + ); + + $this->providerMock + ->expects(self::exactly(3)) + ->method('store') + ->willReturnCallback( + static fn (RemoteResource $resource): ?RemoteResource => match ($resource->getRemoteId() . $resource->getFolder()->getPath() . $resource->getName()) { + 'upload|image|blue_sweaterclothing_saleblue_sweater', 'upload|image|red_shirtold_shirtsred shirt', 'upload|video|black_pantsclothing_sale/pantsBlack pants' => $resource, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "store" with value "%s" matches one of the expecting values.', + $resource->getRemoteId() . $resource->getFolder()->getPath() . $resource->getName(), + ), + ), + }, + ); + + $response = $this->dynamicFolderModeController->__invoke($request); + + self::assertInstanceOf( + JsonResponse::class, + $response, + ); + + self::assertSame( + '"Notification handled."', + $response->getContent(), + ); + + self::assertSame( + Response::HTTP_OK, + $response->getStatusCode(), + ); + } + + public function testDisplayNameChangedFixed(): void + { + $body = json_encode([ + 'notification_type' => 'resource_display_name_changed', + 'resources' => [ + 'sample' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'public_id' => 'upload', + 'from_asset_folder' => 'clothing', + 'to_asset_folder' => 'clothing_sale', + 'display_name' => 'blue_sweater', + ], + ], + ]); + + $request = new Request( + [], + [], + [], + [], + [], + [], + $body, + ); + + $request->headers->add( + [ + 'x-cld-timestamp' => time(), + 'x-cld-signature' => 'test', + ], + ); + + $this->signatureVerifierMock + ->expects(self::once()) + ->method('verify') + ->with($request) + ->willReturn(true); + + $event = new NotificationReceivedEvent($request); + + $this->eventDispatcherMock + ->expects(self::once()) + ->method('dispatch') + ->with($event, $event::NAME); + + $this->gatewayMock + ->expects(self::never()) + ->method('invalidateResourceListCache'); + + $this->gatewayMock + ->expects(self::never()) + ->method('invalidateResourceCache'); + + $this->providerMock + ->expects(self::never()) + ->method('loadByRemoteId'); + + $this->providerMock + ->expects(self::never()) + ->method('store'); + + $response = $this->fixedFolderModeController->__invoke($request); + + self::assertInstanceOf( + JsonResponse::class, + $response, + ); + + self::assertSame( + '"Notification handled."', + $response->getContent(), + ); + + self::assertSame( + Response::HTTP_OK, + $response->getStatusCode(), + ); + } + + public function testDisplayNameChangedDynamic(): void + { + $body = json_encode([ + 'notification_type' => 'resource_display_name_changed', + 'resources' => [ + 'blue_sweater' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'public_id' => 'blue_sweater', + 'new_display_name' => 'blue_sweater', + ], + 'red_shirt' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'public_id' => 'red_shirt', + 'new_display_name' => 'red shirt', + ], + 'non_existing_shirt' => [ + 'resource_type' => 'image', + 'type' => 'upload', + 'public_id' => 'non_existing_shirt', + 'new_display_name' => 'some shirt', + ], + 'black_pants' => [ + 'resource_type' => 'video', + 'type' => 'upload', + 'public_id' => 'black_pants', + 'new_display_name' => 'Black pants', + ], + ], + ]); + + $request = new Request( + [], + [], + [], + [], + [], + [], + $body, + ); + + $request->headers->add( + [ + 'x-cld-timestamp' => time(), + 'x-cld-signature' => 'test', + ], + ); + + $this->signatureVerifierMock + ->expects(self::once()) + ->method('verify') + ->with($request) + ->willReturn(true); + + $event = new NotificationReceivedEvent($request); + + $this->eventDispatcherMock + ->expects(self::once()) + ->method('dispatch') + ->with($event, $event::NAME); + + $this->gatewayMock + ->expects(self::once()) + ->method('invalidateResourceListCache'); + + $this->gatewayMock + ->expects(self::exactly(4)) + ->method('invalidateResourceCache') + ->willReturnCallback( + static fn (CloudinaryRemoteId $cloudinaryRemoteId) => match ($cloudinaryRemoteId->getRemoteId()) { + 'upload|image|blue_sweater', 'upload|image|red_shirt', 'upload|image|non_existing_shirt', 'upload|video|black_pants' => null, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "invalidateResourceCache" with value "%s" matches one of the expecting values.', + $cloudinaryRemoteId->getRemoteId(), + ), + ), + }, + ); + + $blueSweater = new RemoteResource( + remoteId: 'upload|image|blue_sweater', + type: 'image', + url: 'https://res.cloudinary.com/demo/image/upload/blue_sweater', + md5: 'r43tr4t45454324342', + id: 5, + name: 'Sweater (blue)', + folder: Folder::fromPath('clothing'), + size: 380250, + ); + + $redShirt = new RemoteResource( + remoteId: 'upload|image|red_shirt', + type: 'image', + url: 'https://res.cloudinary.com/demo/video/image/red_shirt', + md5: '3r43456fdgregregre', + id: 6, + name: 'red shirt', + folder: Folder::fromPath('shirts'), + size: 3802350, + ); + + $blackPants = new RemoteResource( + remoteId: 'upload|video|black_pants', + type: 'video', + url: 'https://res.cloudinary.com/demo/video/upload/black_pants', + md5: '3r43456fdgregregre', + id: 6, + name: 'black_pants', + folder: Folder::fromPath('clothing/pants'), + size: 329987438, + ); + + $this->providerMock + ->expects(self::exactly(4)) + ->method('loadByRemoteId') + ->willReturnCallback( + static fn (string $remoteId): ?RemoteResource => match ($remoteId) { + 'upload|image|blue_sweater' => $blueSweater, + 'upload|image|red_shirt' => $redShirt, + 'upload|image|non_existing_shirt' => throw new RemoteResourceNotFoundException('upload|image|non_existing_shirt'), + 'upload|video|black_pants' => $blackPants, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "loadByRemoteId" with value "%s" matches one of the expecting values.', + $remoteId, + ), + ), + }, + ); + + $this->providerMock + ->expects(self::exactly(3)) + ->method('store') + ->willReturnCallback( + static fn (RemoteResource $resource): ?RemoteResource => match ($resource->getRemoteId() . $resource->getName()) { + 'upload|image|blue_sweaterblue_sweater', 'upload|image|red_shirtred shirt', 'upload|video|black_pantsBlack pants' => $resource, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "store" with value "%s" matches one of the expecting values.', + $resource->getRemoteId() . $resource->getName(), + ), + ), + }, + ); + + $response = $this->dynamicFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class, @@ -558,6 +1078,7 @@ public function testTagsChanged(): void ], ], ]); + $request = new Request( [], [], @@ -637,7 +1158,12 @@ public function testTagsChanged(): void 'upload|image|sample' => $image, 'upload|video|video_sample' => $video, 'upload|raw|non_existing_sample' => throw new RemoteResourceNotFoundException('upload|raw|non_existing_sample'), - default => null, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "loadByRemoteId" with value "%s" matches one of the expecting values.', + $remoteId, + ), + ), }, ); @@ -655,11 +1181,16 @@ public function testTagsChanged(): void ->willReturnCallback( static fn (RemoteResource $resource): ?RemoteResource => match ($resource->getRemoteId()) { 'upload|image|sample', 'upload|video|video_sample' => $resource, - default => null, + default => throw new RuntimeException( + sprintf( + 'Failed asserting that argument #1 for method "store" with value "%s" matches one of the expecting values.', + $resource->getRemoteId(), + ), + ), }, ); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class, @@ -718,7 +1249,7 @@ public function testFolderCreated(): void ->expects(self::once()) ->method('invalidateFoldersCache'); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class, @@ -777,7 +1308,66 @@ public function testFolderDeleted(): void ->expects(self::once()) ->method('invalidateFoldersCache'); - $response = $this->controller->__invoke($request); + $response = $this->fixedFolderModeController->__invoke($request); + + self::assertInstanceOf( + JsonResponse::class, + $response, + ); + + self::assertSame( + '"Notification handled."', + $response->getContent(), + ); + + self::assertSame( + Response::HTTP_OK, + $response->getStatusCode(), + ); + } + + public function testFolderRenamed(): void + { + $body = json_encode([ + 'notification_type' => 'move_or_rename_asset_folder', + 'resources' => [], + ]); + + $request = new Request( + [], + [], + [], + [], + [], + [], + $body, + ); + + $request->headers->add( + [ + 'x-cld-timestamp' => time(), + 'x-cld-signature' => 'test', + ], + ); + + $this->signatureVerifierMock + ->expects(self::once()) + ->method('verify') + ->with($request) + ->willReturn(true); + + $event = new NotificationReceivedEvent($request); + + $this->eventDispatcherMock + ->expects(self::once()) + ->method('dispatch') + ->with($event, $event::NAME); + + $this->gatewayMock + ->expects(self::once()) + ->method('invalidateFoldersCache'); + + $response = $this->fixedFolderModeController->__invoke($request); self::assertInstanceOf( JsonResponse::class,