diff --git a/CHANGELOG.md b/CHANGELOG.md index 784e7a696..6a25a4add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- [#260](https://github.com/os2display/display-api-service/pull/260) + - Changed how exceptions are handled in InstantBook. + ## [2.5.1] - 2025-06-23 - [#245](https://github.com/os2display/display-api-service/pull/245) diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 3455a12d1..89ad82cb3 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -50,3 +50,22 @@ api_platform: email: itkdev@mkb.aarhus.dk license: name: MIT + + # @see https://api-platform.com/docs/core/errors/#exception-to-status-configuration-using-symfony + exception_to_status: + # The 4 following handlers are registered by default, keep those lines to prevent unexpected side effects + Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended) + ApiPlatform\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST + ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface: 400 + Doctrine\ORM\OptimisticLockException: 409 + + # Validation exception + ApiPlatform\Validator\Exception\ValidationException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_UNPROCESSABLE_ENTITY + + # App exception mappings + App\Exceptions\BadRequestException: 400 + App\Exceptions\ForbiddenException: 403 + App\Exceptions\NotFoundException: 404 + App\Exceptions\NotAcceptableException: 406 + App\Exceptions\ConflictException: 409 + App\Exceptions\TooManyRequestsException: 429 diff --git a/src/Controller/InteractiveController.php b/src/Controller/InteractiveController.php index 16ebab476..d5e5b82f3 100644 --- a/src/Controller/InteractiveController.php +++ b/src/Controller/InteractiveController.php @@ -6,13 +6,16 @@ use App\Entity\ScreenUser; use App\Entity\Tenant\Slide; -use App\Entity\User; -use App\Exceptions\NotFoundException; +use App\Exceptions\BadRequestException; +use App\Exceptions\ConflictException; +use App\Exceptions\NotAcceptableException; +use App\Exceptions\TooManyRequestsException; use App\Service\InteractiveSlideService; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\AsController; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; #[AsController] final readonly class InteractiveController @@ -22,18 +25,28 @@ public function __construct( private Security $security, ) {} + /** + * @throws ConflictException + * @throws BadRequestException + * @throws NotAcceptableException + * @throws TooManyRequestsException + */ public function __invoke(Request $request, Slide $slide): JsonResponse { + $user = $this->security->getUser(); + + if (!($user instanceof ScreenUser)) { + throw new AccessDeniedHttpException('Only screen user can perform action.'); + } + + $tenant = $user->getActiveTenant(); + $requestBody = $request->toArray(); $interactionRequest = $this->interactiveSlideService->parseRequestBody($requestBody); - $user = $this->security->getUser(); - - if (!($user instanceof User || $user instanceof ScreenUser)) { - throw new NotFoundException('User not found'); - } + $actionResult = $this->interactiveSlideService->performAction($tenant, $slide, $interactionRequest); - return new JsonResponse($this->interactiveSlideService->performAction($user, $slide, $interactionRequest)); + return new JsonResponse($actionResult); } } diff --git a/src/Entity/Tenant/InteractiveSlide.php b/src/Entity/Tenant/InteractiveSlideConfig.php similarity index 90% rename from src/Entity/Tenant/InteractiveSlide.php rename to src/Entity/Tenant/InteractiveSlideConfig.php index faf36d679..ea7fa46ab 100644 --- a/src/Entity/Tenant/InteractiveSlide.php +++ b/src/Entity/Tenant/InteractiveSlideConfig.php @@ -9,7 +9,8 @@ use Symfony\Component\Serializer\Annotation\Ignore; #[ORM\Entity(repositoryClass: InteractiveSlideRepository::class)] -class InteractiveSlide extends AbstractTenantScopedEntity +#[ORM\Table(name: 'interactive_slide')] +class InteractiveSlideConfig extends AbstractTenantScopedEntity { #[Ignore] #[ORM\Column(nullable: true)] diff --git a/src/Exceptions/InteractiveSlideException.php b/src/Exceptions/ForbiddenException.php similarity index 55% rename from src/Exceptions/InteractiveSlideException.php rename to src/Exceptions/ForbiddenException.php index cd9a844cf..74f7e0caa 100644 --- a/src/Exceptions/InteractiveSlideException.php +++ b/src/Exceptions/ForbiddenException.php @@ -4,6 +4,6 @@ namespace App\Exceptions; -class InteractiveSlideException extends \Exception +class ForbiddenException extends \Exception { } diff --git a/src/Exceptions/NotAcceptableException.php b/src/Exceptions/NotAcceptableException.php new file mode 100644 index 000000000..64f6090e0 --- /dev/null +++ b/src/Exceptions/NotAcceptableException.php @@ -0,0 +1,9 @@ +action) { - self::ACTION_GET_QUICK_BOOK_OPTIONS => $this->getQuickBookOptions($slide, $interactionRequest), - self::ACTION_QUICK_BOOK => $this->quickBook($slide, $interactionRequest), - default => throw new BadRequestHttpException('Action not allowed'), + self::ACTION_GET_QUICK_BOOK_OPTIONS => $this->getQuickBookOptions($tenant, $slide, $interactionRequest), + self::ACTION_QUICK_BOOK => $this->quickBook($tenant, $slide, $interactionRequest), + default => throw new NotAcceptableException('Action not supported'), }; } /** * @throws \Throwable + * @throws NotAcceptableException */ private function authenticate(array $configuration): array { @@ -105,7 +111,7 @@ private function authenticate(array $configuration): array $password = $this->keyValueService->getValue($configuration['password']); if (4 !== count(array_filter([$tenantId, $clientId, $username, $password]))) { - throw new BadRequestHttpException('tenantId, clientId, username, password must all be set.'); + throw new NotAcceptableException('tenantId, clientId, username, password must all be set.', 400); } $url = self::LOGIN_ENDPOINT.$tenantId.self::OAUTH_PATH; @@ -124,14 +130,15 @@ private function authenticate(array $configuration): array } /** + * @throws NotAcceptableException * @throws InvalidArgumentException */ - private function getToken(Tenant $tenant, InteractiveSlide $interactive): string + private function getToken(Tenant $tenant, InteractiveSlideConfig $interactive): string { $configuration = $interactive->getConfiguration(); if (null === $configuration) { - throw new BadRequestHttpException('InteractiveSlide has no configuration'); + throw new NotAcceptableException('InteractiveSlide has no configuration'); } return $this->interactiveSlideCache->get( @@ -147,99 +154,116 @@ function (CacheItemInterface $item) use ($configuration): mixed { } /** - * @throws \Throwable + * @throws BadRequestException + * @throws InvalidArgumentException */ - private function getQuickBookOptions(Slide $slide, InteractionSlideRequest $interactionRequest): array + private function getQuickBookOptions(Tenant $tenant, Slide $slide, InteractionSlideRequest $interactionRequest): array { $resource = $interactionRequest->data['resource'] ?? null; if (null === $resource) { - throw new \Exception('Resource not set.'); + throw new BadRequestException('Resource not set.'); } + $start = (new \DateTime())->setTimezone(new \DateTimeZone('UTC')); + return $this->interactiveSlideCache->get(self::CACHE_KEY_OPTIONS_PREFIX.$resource, - function (CacheItemInterface $item) use ($slide, $resource, $interactionRequest) { + function (CacheItemInterface $item) use ($slide, $resource, $interactionRequest, $start, $tenant) { $item->expiresAfter(new \DateInterval(self::CACHE_LIFETIME_QUICK_BOOK_OPTIONS)); - /** @var User|ScreenUser $activeUser */ - $activeUser = $this->security->getUser(); - $tenant = $activeUser->getActiveTenant(); - - $interactive = $this->interactiveService->getInteractiveSlide($tenant, $interactionRequest->implementationClass); + // If any exceptions are thrown we return an empty options entry. + try { + $interactiveSlideConfig = $this->interactiveService->getInteractiveSlideConfig($tenant, $interactionRequest->implementationClass); - if (null === $interactive) { - throw new \Exception('InteractiveSlide not found'); - } + if (null === $interactiveSlideConfig) { + throw new NotAcceptableException('InteractiveSlideConfig not found'); + } - // Optional limiting of available resources. - $this->checkPermission($interactive, $resource); + $this->checkPermission($interactiveSlideConfig, $resource); - $feed = $slide->getFeed(); + $feed = $slide->getFeed(); - if (null === $feed) { - throw new \Exception('Slide feed not set.'); - } + if (null === $feed) { + throw new NotAcceptableException('Slide feed not set.'); + } - if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) { - throw new \Exception('Resource not in feed resources'); - } + if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) { + throw new NotAcceptableException('Resource not in feed resources'); + } - $token = $this->getToken($tenant, $interactive); + $token = $this->getToken($tenant, $interactiveSlideConfig); - $start = (new \DateTime())->setTimezone(new \DateTimeZone('UTC')); - $startFormatted = $start->format('c'); + $startPlus1Hour = (clone $start)->add(new \DateInterval('PT1H'))->setTimezone(new \DateTimeZone('UTC')); - $startPlus1Hour = (clone $start)->add(new \DateInterval('PT1H'))->setTimezone(new \DateTimeZone('UTC')); + // Get resources that are watched for availability. + $watchedResources = $this->interactiveSlideCache->get(self::CACHE_KEY_RESOURCES, fn () => []); - // Get resources that are watched for availability. - $watchedResources = $this->interactiveSlideCache->get(self::CACHE_KEY_RESOURCES, fn () => []); + // Add resource to watchedResources, if not in list. + if (!in_array($resource, $watchedResources)) { + $watchedResources[] = $resource; + } - // Add resource to watchedResources, if not in list. - if (!in_array($resource, $watchedResources)) { - $this->interactiveSlideCache->delete(self::CACHE_KEY_RESOURCES); + $schedules = $this->getBusyIntervals($token, $watchedResources, $start, $startPlus1Hour); - $watchedResources[] = $resource; - $this->interactiveSlideCache->get(self::CACHE_KEY_RESOURCES, fn () => $watchedResources); - } + $result = []; - $schedules = $this->getBusyIntervals($token, $watchedResources, $start, $startPlus1Hour); + // Refresh entries for all watched resources. + foreach ($watchedResources as $key => $watchResource) { + $schedule = $schedules[$watchResource] ?? null; - $result = []; + if (!isset($schedules[$watchResource])) { + unset($watchedResources[$key]); + } - // Refresh entries for all watched resources. - foreach ($watchedResources as $watchResource) { - $entry = $this->createEntry($watchResource, $schedules[$watchResource], $startFormatted, $start); + $entry = $this->createEntry($watchResource, $start, $schedule); - if ($watchResource == $resource) { - $result = $entry; - } else { - // Refresh cache entry for resources in watch list that are not handled in current request. - $this->interactiveSlideCache->delete(self::CACHE_KEY_OPTIONS_PREFIX.$watchResource); - $this->interactiveSlideCache->get(self::CACHE_KEY_OPTIONS_PREFIX.$watchResource, - function (CacheItemInterface $item) use ($entry) { - $item->expiresAfter(new \DateInterval(self::CACHE_LIFETIME_QUICK_BOOK_OPTIONS)); + if ($watchResource == $resource) { + $result = $entry; + } else { + // Refresh cache entry for resources in watch list that are not handled in current request. + $this->interactiveSlideCache->delete(self::CACHE_KEY_OPTIONS_PREFIX.$watchResource); + $this->interactiveSlideCache->get(self::CACHE_KEY_OPTIONS_PREFIX.$watchResource, + function (CacheItemInterface $item) use ($entry) { + $item->expiresAfter(new \DateInterval(self::CACHE_LIFETIME_QUICK_BOOK_OPTIONS)); - return $entry; - } - ); + return $entry; + } + ); + } } - } - return $result; + $this->interactiveSlideCache->delete(self::CACHE_KEY_RESOURCES); + $this->interactiveSlideCache->get(self::CACHE_KEY_RESOURCES, fn () => $watchedResources); + + return $result; + } catch (\Throwable) { + // All errors should result in empty options. + return $this->createEntry($resource, $start); + } } ); } - private function createEntry(string $resource, array $schedules, string $startFormatted, \DateTime $start): array + private function createEntry(string $resource, \DateTime $start, ?array $schedules = null): array { + $startFormatted = $start->format('c'); + $entry = [ 'resource' => $resource, 'from' => $startFormatted, 'options' => [], ]; + if (null === $schedules) { + return $entry; + } + foreach (self::DURATIONS as $durationMinutes) { - $startPlus = (clone $start)->add(new \DateInterval('PT'.$durationMinutes.'M'))->setTimezone(new \DateTimeZone('UTC')); + try { + $startPlus = (clone $start)->add(new \DateInterval('PT'.$durationMinutes.'M'))->setTimezone(new \DateTimeZone('UTC')); + } catch (\Exception) { + continue; + } if ($this->intervalFree($schedules, $start, $startPlus)) { $entry['options'][] = [ @@ -253,9 +277,15 @@ private function createEntry(string $resource, array $schedules, string $startFo } /** + * @throws TooManyRequestsException + * @throws ConflictException + * @throws BadRequestException + * @throws InvalidArgumentException + * @throws NotAcceptableException + * @throws ForbiddenException * @throws \Throwable */ - private function quickBook(Slide $slide, InteractionSlideRequest $interactionRequest): array + private function quickBook(Tenant $tenant, Slide $slide, InteractionSlideRequest $interactionRequest): array { $resource = (string) $this->getValueFromInterval('resource', $interactionRequest); $durationMinutes = $this->getValueFromInterval('durationMinutes', $interactionRequest); @@ -273,38 +303,34 @@ function (CacheItemInterface $item) use ($now): \DateTime { ); if ($lastRequestDateTime->add(new \DateInterval(self::CACHE_LIFETIME_QUICK_BOOK_SPAM_PROTECT)) > $now) { - throw new ServiceUnavailableHttpException(60); + throw new TooManyRequestsException('Service unavailable', 503); } - /** @var User|ScreenUser $activeUser */ - $activeUser = $this->security->getUser(); - $tenant = $activeUser->getActiveTenant(); - - $interactive = $this->interactiveService->getInteractiveSlide($tenant, $interactionRequest->implementationClass); + $interactiveSlideConfig = $this->interactiveService->getInteractiveSlideConfig($tenant, $interactionRequest->implementationClass); - if (null === $interactive) { - throw new BadRequestHttpException('Interactive not found'); + if (null === $interactiveSlideConfig) { + throw new NotAcceptableException('InteractiveSlideConfig not found', 400); } // Optional limiting of available resources. - $this->checkPermission($interactive, $resource); + $this->checkPermission($interactiveSlideConfig, $resource); $feed = $slide->getFeed(); if (null === $feed) { - throw new BadRequestHttpException('Slide feed not set.'); + throw new NotAcceptableException('Slide feed not set.'); } if (!in_array($resource, $feed->getConfiguration()['resources'] ?? [])) { - throw new BadRequestHttpException('Resource not in feed resources'); + throw new NotAcceptableException('Resource not in feed resources'); } - $token = $this->getToken($tenant, $interactive); + $token = $this->getToken($tenant, $interactiveSlideConfig); - $configuration = $interactive->getConfiguration(); + $configuration = $interactiveSlideConfig->getConfiguration(); if (null === $configuration) { - throw new BadRequestHttpException('Interactive no configuration'); + throw new NotAcceptableException('InteractiveSlideConfig has no configuration'); } $username = $this->keyValueService->getValue($configuration['username']); @@ -315,7 +341,7 @@ function (CacheItemInterface $item) use ($now): \DateTime { // Make sure interval is free. $busyIntervals = $this->getBusyIntervals($token, [$resource], $start, $startPlusDuration); if (count($busyIntervals[$resource]) > 0) { - throw new ConflictHttpException('Interval booked already'); + throw new ConflictException('Interval booked already'); } $requestBody = [ @@ -351,10 +377,13 @@ function (CacheItemInterface $item) use ($now): \DateTime { $status = $response->getStatusCode(); - return ['status' => $status, 'interval' => [ - 'from' => $start->format('c'), - 'to' => $startPlusDuration->format('c'), - ]]; + return [ + 'status' => $status, + 'interval' => [ + 'from' => $start->format('c'), + 'to' => $startPlusDuration->format('c'), + ], + ]; } /** @@ -362,7 +391,7 @@ function (CacheItemInterface $item) use ($now): \DateTime { * * @throws \Throwable */ - public function getBusyIntervals(string $token, array $resources, \DateTime $startTime, \DateTime $endTime): array + private function getBusyIntervals(string $token, array $resources, \DateTime $startTime, \DateTime $endTime): array { $body = [ 'schedules' => $resources, @@ -389,9 +418,16 @@ public function getBusyIntervals(string $token, array $resources, \DateTime $sta $result = []; foreach ($scheduleData as $schedule) { - $scheduleId = $schedule['scheduleId']; + $scheduleId = $schedule['scheduleId'] ?? null; + $scheduleItems = $schedule['scheduleItems'] ?? null; + + if (null === $scheduleId || null === $scheduleItems) { + continue; + } + $result[$scheduleId] = []; - foreach ($schedule['scheduleItems'] as $scheduleItem) { + + foreach ($scheduleItems as $scheduleItem) { $eventStartArray = $scheduleItem['start']; $eventEndArray = $scheduleItem['end']; @@ -419,18 +455,21 @@ public function intervalFree(array $schedule, \DateTime $from, \DateTime $to): b return true; } + /** + * @throws BadRequestException + */ private function getValueFromInterval(string $key, InteractionSlideRequest $interactionRequest): string|int { $interval = $interactionRequest->data['interval'] ?? null; if (null === $interval) { - throw new BadRequestHttpException('interval not set.'); + throw new BadRequestException('interval not set.'); } $value = $interval[$key] ?? null; if (null === $value) { - throw new BadRequestHttpException("interval.'.$key.' not set."); + throw new BadRequestException("interval.'.$key.' not set.", 400); } return $value; @@ -445,37 +484,47 @@ private function getHeaders(string $token): array ]; } - private function checkPermission(InteractiveSlide $interactive, string $resource): void + /** + * @throws NotAcceptableException + * @throws ForbiddenException + * @throws InvalidArgumentException + */ + private function checkPermission(InteractiveSlideConfig $interactive, string $resource): void { $configuration = $interactive->getConfiguration(); - // Optional limiting of available resources. + + // Will only limit access to resources if list is set up. if (null !== $configuration && !empty($configuration['resourceEndpoint'])) { $allowedResources = $this->getAllowedResources($interactive); if (!in_array($resource, $allowedResources)) { - throw new \Exception('Not allowed'); + throw new ForbiddenException('Not allowed'); } } } - private function getAllowedResources(InteractiveSlide $interactive): array + /** + * @throws NotAcceptableException + * @throws InvalidArgumentException + */ + private function getAllowedResources(InteractiveSlideConfig $interactive): array { - return $this->interactiveSlideCache->get(self::CACHE_ALLOWED_RESOURCES_PREFIX.$interactive->getId(), function (CacheItemInterface $item) use ($interactive) { - $item->expiresAfter(60 * 60); + $configuration = $interactive->getConfiguration(); - $configuration = $interactive->getConfiguration(); + $key = $configuration['resourceEndpoint'] ?? null; - $key = $configuration['resourceEndpoint'] ?? null; + if (null === $key) { + throw new NotAcceptableException('resourceEndpoint not set', 400); + } - if (null === $key) { - throw new \Exception('resourceEndpoint not set'); - } + $resourceEndpoint = $this->keyValueService->getValue($key); - $resourceEndpoint = $this->keyValueService->getValue($key); + if (null === $resourceEndpoint) { + throw new NotAcceptableException('resourceEndpoint value not set', 400); + } - if (null === $resourceEndpoint) { - throw new \Exception('resourceEndpoint value not set'); - } + return $this->interactiveSlideCache->get(self::CACHE_ALLOWED_RESOURCES_PREFIX.$interactive->getId(), function (CacheItemInterface $item) use ($resourceEndpoint) { + $item->expiresAfter(60 * 60); $response = $this->client->request('GET', $resourceEndpoint); $content = $response->toArray(); diff --git a/src/InteractiveSlide/InteractiveSlideInterface.php b/src/InteractiveSlide/InteractiveSlideInterface.php index 5ba3343d7..df957842e 100644 --- a/src/InteractiveSlide/InteractiveSlideInterface.php +++ b/src/InteractiveSlide/InteractiveSlideInterface.php @@ -4,9 +4,12 @@ namespace App\InteractiveSlide; +use App\Entity\Tenant; use App\Entity\Tenant\Slide; -use App\Exceptions\InteractiveSlideException; -use Symfony\Component\Security\Core\User\UserInterface; +use App\Exceptions\BadRequestException; +use App\Exceptions\ConflictException; +use App\Exceptions\NotAcceptableException; +use App\Exceptions\TooManyRequestsException; interface InteractiveSlideInterface { @@ -15,7 +18,10 @@ public function getConfigOptions(): array; /** * Perform the given InteractionRequest with the given Slide. * - * @throws InteractiveSlideException + * @throws ConflictException + * @throws BadRequestException + * @throws NotAcceptableException + * @throws TooManyRequestsException */ - public function performAction(UserInterface $user, Slide $slide, InteractionSlideRequest $interactionRequest): array; + public function performAction(Tenant $tenant, Slide $slide, InteractionSlideRequest $interactionRequest): array; } diff --git a/src/Repository/InteractiveSlideRepository.php b/src/Repository/InteractiveSlideRepository.php index a9d52ab90..5e263f4bf 100644 --- a/src/Repository/InteractiveSlideRepository.php +++ b/src/Repository/InteractiveSlideRepository.php @@ -4,22 +4,22 @@ namespace App\Repository; -use App\Entity\Tenant\InteractiveSlide; +use App\Entity\Tenant\InteractiveSlideConfig; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @extends ServiceEntityRepository + * @extends ServiceEntityRepository * - * @method InteractiveSlide|null find($id, $lockMode = null, $lockVersion = null) - * @method InteractiveSlide|null findOneBy(array $criteria, array $orderBy = null) - * @method InteractiveSlide[] findAll() - * @method InteractiveSlide[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @method InteractiveSlideConfig|null find($id, $lockMode = null, $lockVersion = null) + * @method InteractiveSlideConfig|null findOneBy(array $criteria, array $orderBy = null) + * @method InteractiveSlideConfig[] findAll() + * @method InteractiveSlideConfig[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class InteractiveSlideRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { - parent::__construct($registry, InteractiveSlide::class); + parent::__construct($registry, InteractiveSlideConfig::class); } } diff --git a/src/Service/InteractiveSlideService.php b/src/Service/InteractiveSlideService.php index e4c8a6804..21460e002 100644 --- a/src/Service/InteractiveSlideService.php +++ b/src/Service/InteractiveSlideService.php @@ -4,17 +4,17 @@ namespace App\Service; -use App\Entity\ScreenUser; use App\Entity\Tenant; -use App\Entity\Tenant\InteractiveSlide; +use App\Entity\Tenant\InteractiveSlideConfig; use App\Entity\Tenant\Slide; -use App\Entity\User; -use App\Exceptions\InteractiveSlideException; +use App\Exceptions\BadRequestException; +use App\Exceptions\ConflictException; +use App\Exceptions\NotAcceptableException; +use App\Exceptions\TooManyRequestsException; use App\InteractiveSlide\InteractionSlideRequest; use App\InteractiveSlide\InteractiveSlideInterface; use App\Repository\InteractiveSlideRepository; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Security\Core\User\UserInterface; /** * Service for handling Slide interactions. @@ -33,7 +33,7 @@ public function __construct( * * @param array $requestBody the request body from the http request * - * @throws InteractiveSlideException + * @throws BadRequestException */ public function parseRequestBody(array $requestBody): InteractionSlideRequest { @@ -42,7 +42,7 @@ public function parseRequestBody(array $requestBody): InteractionSlideRequest $data = $requestBody['data'] ?? null; if (null === $implementationClass || null === $action || null === $data) { - throw new InteractiveSlideException('implementationClass, action and/or data not set.'); + throw new BadRequestException('implementationClass, action and/or data not set.'); } return new InteractionSlideRequest($implementationClass, $action, $data); @@ -51,27 +51,24 @@ public function parseRequestBody(array $requestBody): InteractionSlideRequest /** * Perform an action for an interactive slide. * - * @throws InteractiveSlideException + * @throws ConflictException + * @throws BadRequestException + * @throws NotAcceptableException + * @throws TooManyRequestsException */ - public function performAction(UserInterface $user, Slide $slide, InteractionSlideRequest $interactionRequest): array + public function performAction(Tenant $tenant, Slide $slide, InteractionSlideRequest $interactionRequest): array { - if (!$user instanceof ScreenUser && !$user instanceof User) { - throw new InteractiveSlideException('User is not supported'); - } - - $tenant = $user->getActiveTenant(); - $implementationClass = $interactionRequest->implementationClass; - $interactive = $this->getInteractiveSlide($tenant, $implementationClass); + $interactive = $this->getInteractiveSlideConfig($tenant, $implementationClass); if (null === $interactive) { - throw new InteractiveSlideException('Interactive slide not found'); + throw new NotAcceptableException('Interactive slide config not found'); } $interactiveImplementation = $this->getImplementation($interactive->getImplementationClass()); - return $interactiveImplementation->performAction($user, $slide, $interactionRequest); + return $interactiveImplementation->performAction($tenant, $slide, $interactionRequest); } /** @@ -91,7 +88,7 @@ public function getConfigurables(): array /** * Find the implementation class. * - * @throws InteractiveSlideException + * @throws BadRequestException */ public function getImplementation(?string $implementationClass): InteractiveSlideInterface { @@ -99,7 +96,7 @@ public function getImplementation(?string $implementationClass): InteractiveSlid $interactiveImplementations = array_filter($asArray, fn ($implementation) => $implementation::class === $implementationClass); if (0 === count($interactiveImplementations)) { - throw new InteractiveSlideException('Interactive implementation class not found'); + throw new BadRequestException('Interactive implementation class not found'); } return $interactiveImplementations[0]; @@ -108,7 +105,7 @@ public function getImplementation(?string $implementationClass): InteractiveSlid /** * Get the interactive slide. */ - public function getInteractiveSlide(Tenant $tenant, string $implementationClass): ?InteractiveSlide + public function getInteractiveSlideConfig(Tenant $tenant, string $implementationClass): ?InteractiveSlideConfig { return $this->interactiveSlideRepository->findOneBy([ 'implementationClass' => $implementationClass, @@ -127,7 +124,7 @@ public function saveConfiguration(Tenant $tenant, string $implementationClass, a ]); if (null === $entry) { - $entry = new InteractiveSlide(); + $entry = new InteractiveSlideConfig(); $entry->setTenant($tenant); $entry->setImplementationClass($implementationClass); diff --git a/tests/Service/InteractiveServiceTest.php b/tests/Service/InteractiveServiceTest.php index 5f5fee8b4..9ce070292 100644 --- a/tests/Service/InteractiveServiceTest.php +++ b/tests/Service/InteractiveServiceTest.php @@ -5,7 +5,8 @@ namespace App\Tests\Service; use App\Entity\Tenant\Slide; -use App\Exceptions\InteractiveSlideException; +use App\Exceptions\BadRequestException; +use App\Exceptions\NotAcceptableException; use App\InteractiveSlide\InstantBook; use App\InteractiveSlide\InteractionSlideRequest; use App\Repository\UserRepository; @@ -41,7 +42,7 @@ public function testParseRequestBody(): void { $interactiveService = $this->container->get(InteractiveSlideService::class); - $this->expectException(InteractiveSlideException::class); + $this->expectException(BadRequestException::class); $interactiveService->parseRequestBody([ 'test' => 'test', @@ -73,19 +74,19 @@ public function testPerformAction(): void 'data' => [], ]); - $this->expectException(InteractiveSlideException::class); - $this->expectExceptionMessage('Interactive slide not found'); + $this->expectException(NotAcceptableException::class); + $this->expectExceptionMessage('Interactive slide config not found'); $tenant = $user->getActiveTenant(); - $interactiveService->performAction($user, $slide, $interactionRequest); + $interactiveService->performAction($tenant, $slide, $interactionRequest); $interactiveService->saveConfiguration($tenant, InstantBook::class, []); - $this->expectException(InteractiveSlideException::class); + $this->expectException(NotAcceptableException::class); $this->expectExceptionMessage('Action not allowed'); - $interactiveService->performAction($user, $slide, $interactionRequest); + $interactiveService->performAction($tenant, $slide, $interactionRequest); } public function testGetConfigurables(): void