diff --git a/config/functions.php b/config/functions.php index 113afa9d..e7065c80 100644 --- a/config/functions.php +++ b/config/functions.php @@ -1,7 +1,9 @@ setName('user-list'); $group // User create form is rendered by the client and loads the available dropdown options via Ajax - ->get('/dropdown-options', \App\Application\Action\User\Ajax\FetchDropdownOptionsForUserCreateAction::class) - ->setName('user-dropdown-options'); + ->get('/dropdown-options', \App\Application\Action\User\Ajax\UserCreateDropdownOptionsFetchAction::class) + ->setName('user-create-dropdown'); $group->get('/activity', \App\Application\Action\User\Ajax\UserActivityFetchListAction::class) ->setName('user-get-activity'); @@ -100,7 +100,7 @@ // Client create form is rendered by the client and loads the available dropdown options via Ajax $group->get( '/dropdown-options', - \App\Application\Action\Client\Ajax\FetchDropdownOptionsForClientCreateAction::class + \App\Application\Action\Client\Ajax\ClientCreateDropdownOptionsFetchAction::class )->setName('client-create-dropdown'); $group->post('', \App\Application\Action\Client\Ajax\ClientCreateAction::class) @@ -123,18 +123,18 @@ 'note-read-page' ); $group->post('', \App\Application\Action\Note\Ajax\NoteCreateAction::class)->setName( - 'note-submit-creation' + 'note-create-submit' ); $group->put('/{note_id:[0-9]+}', \App\Application\Action\Note\Ajax\NoteUpdateAction::class) - ->setName('note-submit-modification'); + ->setName('note-update-submit'); $group->delete('/{note_id:[0-9]+}', \App\Application\Action\Note\Ajax\NoteDeleteAction::class) - ->setName('note-submit-delete'); + ->setName('note-delete-submit'); })->add(UserAuthenticationMiddleware::class); // API routes $app->group('/api', function (RouteCollectorProxy $group) { // Client creation API call - $group->post('/clients', \App\Application\Action\Client\Ajax\ApiClientCreateAction::class) + $group->post('/clients', \App\Application\Action\Client\Api\ApiClientCreateAction::class) ->setName('api-client-create-submit'); })// Cross-Origin Resource Sharing (CORS) middleware. Allow another domain to access '/api' routes. // If an error occurs, the CORS middleware will not be executed and the exception caught and a response diff --git a/phpstan.neon b/phpstan.neon index c4f4273d..ab0775ab 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,5 +5,4 @@ parameters: - src - tests ignoreErrors: - - '#Parameter \#2 \$locale of function setlocale expects array\|string\|null, int given.#' - - '#^Method .*::getTranslatedValues\(\) is unused\.$#' \ No newline at end of file + - '#Parameter \#2 \$locale of function setlocale expects array\|string\|null, int given.#' \ No newline at end of file diff --git a/public/assets/general/general-js/initialization.js b/public/assets/general/general-js/initialization.js index aeec2192..efffa8f9 100644 --- a/public/assets/general/general-js/initialization.js +++ b/public/assets/general/general-js/initialization.js @@ -25,9 +25,6 @@ window.addEventListener("load", function (event) { /** Throttle time countdown */ countDownThrottleTimer(); - - initCollapsible(); - /** Scroll to anchor if there is any in the url */ scrollToAnchor(); }); diff --git a/public/assets/general/page-component/flash-message/flash-message.css b/public/assets/general/page-component/flash-message/flash-message.css index 3e7f0ce2..cd163456 100644 --- a/public/assets/general/page-component/flash-message/flash-message.css +++ b/public/assets/general/page-component/flash-message/flash-message.css @@ -80,6 +80,7 @@ /*Start to wrap right before ending*/ 100% { white-space: break-spaces; + overflow-wrap: anywhere; /*Takes initial max-width value which is 90%*/ } } @@ -297,6 +298,7 @@ margin: 15px 0 10px 0px; transform: translateX(130%); white-space: break-spaces; + overflow-wrap: anywhere; /*Allow pointer events on the flash message after removing it on the container*/ pointer-events: auto; } diff --git a/public/index.php b/public/index.php index 3cf0f10c..d785cd0f 100644 --- a/public/index.php +++ b/public/index.php @@ -1,10 +1,9 @@ run(); diff --git a/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php b/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php index 4b43528c..77324dff 100644 --- a/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php +++ b/src/Application/Action/Authentication/Ajax/NewPasswordResetSubmitAction.php @@ -57,7 +57,10 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res ); // Pre-fill email input field for more user comfort. if ($invalidTokenException->userData->email !== null) { - $this->templateRenderer->addPhpViewAttribute('preloadValues', ['email' => $invalidTokenException->userData->email]); + $this->templateRenderer->addPhpViewAttribute( + 'preloadValues', + ['email' => $invalidTokenException->userData->email] + ); } $this->logger->error( @@ -67,8 +70,7 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res // The login page is rendered, but the url is reset-password. In login-main.js the url is replaced, and // the password-forgotten form is shown instead of the login form. return $this->templateRenderer->render($response, 'authentication/login.html.php'); - } // Validation Exception has to be caught here and not middleware as the token, - // and id have to be added to php view + } // Validation Exception has to be caught here and not middleware as the token, and id are added to php view catch (ValidationException $validationException) { // Render reset-password form with token, and id so that it can be submitted again $flash->add('error', $validationException->getMessage()); diff --git a/src/Application/Action/Authentication/Page/PasswordResetPageAction.php b/src/Application/Action/Authentication/Page/PasswordResetPageAction.php index 9431e81e..32833c2c 100644 --- a/src/Application/Action/Authentication/Page/PasswordResetPageAction.php +++ b/src/Application/Action/Authentication/Page/PasswordResetPageAction.php @@ -36,7 +36,9 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res 'id' => $queryParams['id'], ]); } - + // Replace token from query params with *** + $queryParams['token'] = '***'; + // Log error $this->logger->error( 'GET request malformed: ' . json_encode($queryParams, JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR) ); diff --git a/src/Application/Action/Client/Ajax/ClientCreateAction.php b/src/Application/Action/Client/Ajax/ClientCreateAction.php index bab50ff0..70be30ba 100644 --- a/src/Application/Action/Client/Ajax/ClientCreateAction.php +++ b/src/Application/Action/Client/Ajax/ClientCreateAction.php @@ -22,17 +22,15 @@ public function __invoke( ): ResponseInterface { $clientValues = (array)$request->getParsedBody(); - // Validation and Forbidden exception caught in respective middlewares $insertId = $this->clientCreator->createClient($clientValues); if (0 !== $insertId) { return $this->jsonResponder->encodeAndAddToResponse($response, ['status' => 'success', 'data' => null], 201); } - $response = $this->jsonResponder->encodeAndAddToResponse($response, [ + + return $this->jsonResponder->encodeAndAddToResponse($response, [ 'status' => 'warning', 'message' => 'Client not created', ]); - - return $response->withAddedHeader('Warning', 'The client could not be created'); } } diff --git a/src/Application/Action/Client/Ajax/FetchDropdownOptionsForClientCreateAction.php b/src/Application/Action/Client/Ajax/ClientCreateDropdownOptionsFetchAction.php similarity index 94% rename from src/Application/Action/Client/Ajax/FetchDropdownOptionsForClientCreateAction.php rename to src/Application/Action/Client/Ajax/ClientCreateDropdownOptionsFetchAction.php index 1407989d..5cf13bcc 100644 --- a/src/Application/Action/Client/Ajax/FetchDropdownOptionsForClientCreateAction.php +++ b/src/Application/Action/Client/Ajax/ClientCreateDropdownOptionsFetchAction.php @@ -7,7 +7,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -final readonly class FetchDropdownOptionsForClientCreateAction +final readonly class ClientCreateDropdownOptionsFetchAction { public function __construct( private JsonResponder $jsonResponder, diff --git a/src/Application/Action/Client/Ajax/ClientDeleteAction.php b/src/Application/Action/Client/Ajax/ClientDeleteAction.php index 47709a00..a99b0d15 100644 --- a/src/Application/Action/Client/Ajax/ClientDeleteAction.php +++ b/src/Application/Action/Client/Ajax/ClientDeleteAction.php @@ -24,7 +24,7 @@ public function __invoke( ): ResponseInterface { $clientId = (int)$args['client_id']; - // Delete client logic + // Delete client $deleted = $this->clientDeleter->deleteClient($clientId); $flash = $this->session->getFlash(); @@ -38,11 +38,11 @@ public function __invoke( $response = $this->jsonResponder->encodeAndAddToResponse( $response, - ['status' => 'warning', 'message' => 'Client not deleted.'] + ['status' => 'warning', 'message' => 'Client has not been deleted.'] ); // If not deleted, inform user $flash->add('warning', 'The client was not deleted'); - return $response->withAddedHeader('Warning', 'The client was not deleted'); + return $response->withAddedHeader('Warning', 'The client has not been deleted.'); } } diff --git a/src/Application/Action/Client/Ajax/ApiClientCreateAction.php b/src/Application/Action/Client/Api/ApiClientCreateAction.php similarity index 76% rename from src/Application/Action/Client/Ajax/ApiClientCreateAction.php rename to src/Application/Action/Client/Api/ApiClientCreateAction.php index 068db48b..8e562d3b 100644 --- a/src/Application/Action/Client/Ajax/ApiClientCreateAction.php +++ b/src/Application/Action/Client/Api/ApiClientCreateAction.php @@ -1,6 +1,6 @@ jsonResponder->encodeAndAddToResponse($response, ['status' => 'success', 'data' => null], 201); } - $response = $this->jsonResponder->encodeAndAddToResponse($response, [ + return $this->jsonResponder->encodeAndAddToResponse($response, [ 'status' => 'warning', - 'message' => 'Client not created', + 'message' => 'Client was not created', ]); - - return $response->withAddedHeader('Warning', 'The client could not be created'); } } diff --git a/src/Application/Action/Common/TranslateAction.php b/src/Application/Action/Common/TranslateAction.php index 46a6774d..bcabac05 100644 --- a/src/Application/Action/Common/TranslateAction.php +++ b/src/Application/Action/Common/TranslateAction.php @@ -13,6 +13,14 @@ public function __construct( ) { } + /** + * Returns strings provided in requests as translated strings. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @return ResponseInterface + */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $queryParams = $request->getQueryParams(); diff --git a/src/Application/Action/Dashboard/PhpDevTestAction.php b/src/Application/Action/Dashboard/PhpDevTestAction.php deleted file mode 100644 index de948380..00000000 --- a/src/Application/Action/Dashboard/PhpDevTestAction.php +++ /dev/null @@ -1,24 +0,0 @@ -jsonResponder->encodeAndAddToResponse( $response, - ['status' => 'warning', 'message' => 'Note not deleted.'] + ['status' => 'warning', 'message' => 'Note has not been deleted.'] ); return $response->withAddedHeader('Warning', 'The note was not deleted'); diff --git a/src/Application/Action/User/Ajax/UserCreateAction.php b/src/Application/Action/User/Ajax/UserCreateAction.php index 4d663b46..114a6070 100644 --- a/src/Application/Action/User/Ajax/UserCreateAction.php +++ b/src/Application/Action/User/Ajax/UserCreateAction.php @@ -34,12 +34,18 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res if ($insertId !== false) { $this->logger->info('User "' . $userValues['email'] . '" created'); - } else { - $this->logger->info('Account creation tried with existing email: "' . $userValues['email'] . '"'); - $response = $response->withAddedHeader('Warning', 'The user was not created'); + + return $this->jsonResponder->encodeAndAddToResponse( + $response, + ['status' => 'success', 'data' => null], + 201 + ); } - return $this->jsonResponder->encodeAndAddToResponse($response, ['status' => 'success', 'data' => null], 201); + return $this->jsonResponder->encodeAndAddToResponse($response, [ + 'status' => 'warning', + 'message' => 'User not created', + ]); } catch (TransportExceptionInterface $e) { // Flash message has to be added in the frontend as form is submitted via Ajax $this->logger->error('Mailer exception: ' . $e->getMessage()); diff --git a/src/Application/Action/User/Ajax/FetchDropdownOptionsForUserCreateAction.php b/src/Application/Action/User/Ajax/UserCreateDropdownOptionsFetchAction.php similarity index 91% rename from src/Application/Action/User/Ajax/FetchDropdownOptionsForUserCreateAction.php rename to src/Application/Action/User/Ajax/UserCreateDropdownOptionsFetchAction.php index 2bacabc9..66a8e802 100644 --- a/src/Application/Action/User/Ajax/FetchDropdownOptionsForUserCreateAction.php +++ b/src/Application/Action/User/Ajax/UserCreateDropdownOptionsFetchAction.php @@ -7,7 +7,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -final readonly class FetchDropdownOptionsForUserCreateAction +final readonly class UserCreateDropdownOptionsFetchAction { public function __construct( private JsonResponder $jsonResponder, diff --git a/src/Application/Action/User/Ajax/UserFetchListAction.php b/src/Application/Action/User/Ajax/UserFetchListAction.php index 7f041cf5..d3a9d0ac 100644 --- a/src/Application/Action/User/Ajax/UserFetchListAction.php +++ b/src/Application/Action/User/Ajax/UserFetchListAction.php @@ -26,7 +26,7 @@ public function __invoke( return $this->jsonResponder->encodeAndAddToResponse($response, [ 'userResultDataArray' => $userResultDataArray, - 'statuses' => UserStatus::toTranslatedNamesArray(), + 'statuses' => UserStatus::getAllDisplayNames(), ]); } } diff --git a/src/Application/Middleware/NonFatalErrorHandlerMiddleware.php b/src/Application/Middleware/NonFatalErrorHandlerMiddleware.php deleted file mode 100644 index 23e2b99f..00000000 --- a/src/Application/Middleware/NonFatalErrorHandlerMiddleware.php +++ /dev/null @@ -1,75 +0,0 @@ -displayErrorDetails = $displayErrorDetails; - $this->logErrors = $logErrors; - } - - /** - * Invoke middleware. - * - * @param ServerRequestInterface $request The request - * @param RequestHandlerInterface $handler The handler - * - * @throws ErrorException - * - * @return ResponseInterface The response - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - // Only make notices / wantings to ErrorException's if error details should be displayed - // SLE-57 Making warnings and notices to exceptions for development - // set_error_handler only handles non-fatal errors. The function callback is not called by fatal errors. - set_error_handler( - function ($severity, $message, $file, $line) { - // Don't throw exception if error reporting is turned off. - // '&' checks if a particular error level is included in the result of error_reporting(). - if (error_reporting() & $severity) { - // Log non fatal errors if logging is enabled - if ($this->logErrors) { - // If error is warning - if ($severity === E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING) { - $this->logger->warning("Warning [$severity] $message on line $line in file $file"); - } // If error is non-fatal and is not a warning - else { - $this->logger->notice("Notice [$severity] $message on line $line in file $file"); - } - } - if ($this->displayErrorDetails === true) { - // Throw ErrorException to stop script execution and have access to more error details - // Logging for fatal errors happens in DefaultErrorHandler.php - throw new ErrorException($message, 0, $severity, $file, $line); - } - } - - return true; - } - ); - - $response = $handler->handle($request); - - // Restore previous error handler in post-processing to satisfy PHPUnit 11 that checks for any - // leftover error handlers https://github.com/sebastianbergmann/phpunit/pull/5619 - restore_error_handler(); - - return $response; - } -} diff --git a/src/Application/Middleware/PhpViewMiddleware.php b/src/Application/Middleware/PhpViewMiddleware.php index 9d5e8232..d57564fb 100644 --- a/src/Application/Middleware/PhpViewMiddleware.php +++ b/src/Application/Middleware/PhpViewMiddleware.php @@ -78,6 +78,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * Check if the user is allowed to see the user list. * * @param int $loggedInUserId + * * @return bool */ private function checkUserListAuthorization(int $loggedInUserId): bool diff --git a/src/Common/Trait/EnumToArray.php b/src/Common/Trait/EnumToArray.php index c4dd1624..102b7dbd 100644 --- a/src/Common/Trait/EnumToArray.php +++ b/src/Common/Trait/EnumToArray.php @@ -12,12 +12,6 @@ public static function names(): array return array_column(self::cases(), 'name'); } - public static function translatedNames(): array - { - // Run the translation function __() over each name - return array_map('__', self::names()); - } - public static function values(): array { return array_column(self::cases(), 'value'); @@ -28,33 +22,39 @@ public static function toArray(): array return array_combine(self::values(), self::names()); } - public static function toTranslatedNamesArray(): array - { - // Returns enum cases with value as key and translated name as value - return array_combine(self::values(), self::translatedNames()); - } - - public static function toArrayWithPrettyNames(): array + /** + * Creates an array with the enum values as keys and the translated names as values. + * Requires the getDisplayName method to be implemented in the enum. + * + * @return array + */ + public static function getAllDisplayNames(): array { - return array_combine(self::values(), self::prettifyNames(self::names())); + // Creates an array by using one array for keys and another for its values + return array_combine(self::values(), self::getDisplayNamesArray(self::cases())); } /** - * All letters lowercase except first capital letter - * and replaces underscores with spaces. + * Returns array with all enum values as names that can be displayed by the frontend. + * Requires the getDisplayName() method to be implemented in the enum. * - * @param array $names + * @param self[] $enumCases * * @return array */ - private static function prettifyNames(array $names): array + private static function getDisplayNamesArray(array $enumCases): array { - $prettyNames = []; - foreach ($names as $name) { - // String is a key in the enum function getTranslatedValues so __() knows how to translate - $prettyNames[] = __(str_replace('_', ' ', ucfirst(mb_strtolower($name)))); + $displayNames = []; + foreach ($enumCases as $enumCase) { + // If the enum case has a getDisplayName method, use it to get the display name otherwise use the case name + /** @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/7599 */ + if (method_exists($enumCase, 'getDisplayName')) { + $displayNames[] = $enumCase->getDisplayName(); + } else { + $displayNames[] = $enumCase->name; + } } - return $prettyNames; + return $displayNames; } } diff --git a/src/Domain/Authentication/Repository/UserRoleFinderRepository.php b/src/Domain/Authentication/Repository/UserRoleFinderRepository.php index 3508fee3..b4c9470d 100644 --- a/src/Domain/Authentication/Repository/UserRoleFinderRepository.php +++ b/src/Domain/Authentication/Repository/UserRoleFinderRepository.php @@ -111,7 +111,7 @@ public function findAllUserRolesForDropdown(): array $resultRows = $query->execute()->fetchAll('assoc') ?: []; $userRoles = []; foreach ($resultRows as $resultRow) { - $userRoles[(int)$resultRow['id']] = UserRole::from($resultRow['name'])->roleNameForDropdown(); + $userRoles[(int)$resultRow['id']] = UserRole::from($resultRow['name'])->getDisplayName(); } return $userRoles; diff --git a/src/Domain/Client/Data/ClientDropdownValuesData.php b/src/Domain/Client/Data/ClientDropdownValuesData.php index 48d4ffd0..3d5e521e 100644 --- a/src/Domain/Client/Data/ClientDropdownValuesData.php +++ b/src/Domain/Client/Data/ClientDropdownValuesData.php @@ -22,7 +22,7 @@ public function __construct( ) { $this->statuses = $statuses; $this->users = $users; - $this->sexes = SexOption::toArrayWithPrettyNames(); - $this->vigilanceLevel = ClientVigilanceLevel::toArrayWithPrettyNames(); + $this->sexes = SexOption::getAllDisplayNames(); + $this->vigilanceLevel = ClientVigilanceLevel::getAllDisplayNames(); } } diff --git a/src/Domain/Client/Enum/ClientStatus.php b/src/Domain/Client/Enum/ClientStatus.php index 7b3e3f8c..0e704fd7 100644 --- a/src/Domain/Client/Enum/ClientStatus.php +++ b/src/Domain/Client/Enum/ClientStatus.php @@ -10,20 +10,17 @@ enum ClientStatus: string case CANNOT_HELP = 'Cannot help'; /** - * This function is not designed to be used. - * In order for the enum values to be acknowledged by the - * translation tool Poedit, they each - * have to be called with the __() method. + * Get the enum case name that can be displayed by the frontend. * - * @return array + * @return string */ - private function getTranslatedValues(): array + public function getDisplayName(): string { - return [ - __('Action pending'), - __('In care'), - __('Helped'), - __('Cannot help'), - ]; + return match ($this) { + self::ACTION_PENDING => __('Action pending'), + self::IN_CARE => __('In care'), + self::HELPED => __('Helped'), + self::CANNOT_HELP => __('Cannot help'), + }; } } diff --git a/src/Domain/Client/Enum/ClientVigilanceLevel.php b/src/Domain/Client/Enum/ClientVigilanceLevel.php index 76e65d55..7bd74dbc 100644 --- a/src/Domain/Client/Enum/ClientVigilanceLevel.php +++ b/src/Domain/Client/Enum/ClientVigilanceLevel.php @@ -13,35 +13,16 @@ enum ClientVigilanceLevel: string case HIGH = 'high'; /** - * Calling the translation function __() for each enum value - * so that poedit recognizes them to be translated. - * When using the enum values, __() will work as it's - * setup here and translations are in the .mo files. - * - * @return array - */ - public static function getTranslatedValues(): array - { - return [ - __('Low'), - __('Medium'), - __('High'), - ]; - } - - /** - * All letters lowercase except first capital letter - * and replaces underscores with spaces. - * - * Would love this function to be global / be in a trait that could be used - * but don't know the best way to implement it right now as there is no access - * to "this" in a trait for instance + * Get the enum case name that can be displayed by the frontend. * * @return string */ - public function prettyName(): string + public function getDisplayName(): string { - // Resulting string is a key in the function getTranslatedValues so __() knows how to translate - return __(str_replace('_', ' ', ucfirst(mb_strtolower($this->value)))); + return match ($this) { + self::LOW => __('Low'), + self::MEDIUM => __('Medium'), + self::HIGH => __('High'), + }; } } diff --git a/src/Domain/Client/Repository/ClientStatus/ClientStatusFinderRepository.php b/src/Domain/Client/Repository/ClientStatus/ClientStatusFinderRepository.php index 4e2cc4dc..41b2573d 100644 --- a/src/Domain/Client/Repository/ClientStatus/ClientStatusFinderRepository.php +++ b/src/Domain/Client/Repository/ClientStatus/ClientStatusFinderRepository.php @@ -48,7 +48,8 @@ public function findAllClientStatusesMappedByIdName(bool $withoutTranslation = f foreach ($resultRows as $resultRow) { // If status is required without the translation provide value directly from db // Translation key is created in ClientStatus enum - $statusName = $withoutTranslation ? $resultRow['name'] : __($resultRow['name']); + $statusName = $withoutTranslation ? $resultRow['name'] + : ClientStatus::from($resultRow['name'])->getDisplayName(); $statuses[(int)$resultRow['id']] = $statusName; } diff --git a/src/Domain/Client/Service/ClientListFilter/ClientListFilterChipProvider.php b/src/Domain/Client/Service/ClientListFilter/ClientListFilterChipProvider.php index 912d7477..e1245fbb 100644 --- a/src/Domain/Client/Service/ClientListFilter/ClientListFilterChipProvider.php +++ b/src/Domain/Client/Service/ClientListFilter/ClientListFilterChipProvider.php @@ -80,7 +80,7 @@ private function getClientListFilters(): array $clientStatuses = $this->clientStatusFinderRepository->findAllClientStatusesMappedByIdName(); foreach ($clientStatuses as $id => $name) { $allClientFilters["status_$id"] = new FilterData([ - 'name' => __($name), + 'name' => $name, 'paramName' => 'status', 'paramValue' => $id, 'category' => 'Status', diff --git a/src/Domain/General/Enum/SexOption.php b/src/Domain/General/Enum/SexOption.php index c16f17fd..96fbf66f 100644 --- a/src/Domain/General/Enum/SexOption.php +++ b/src/Domain/General/Enum/SexOption.php @@ -15,19 +15,16 @@ enum SexOption: string // case NULL = ''; /** - * Calling the translation function __() for each enum value - * so that poedit recognizes them to be translated. - * When using the enum values, __() will work as it's - * setup here and translations are in the .mo files. + * Get the enum case name that can be displayed by the frontend. * - * @return array + * @return string */ - private function getTranslatedValues(): array + public function getDisplayName(): string { - return [ - __('Male'), - __('Female'), - __('Other'), - ]; + return match ($this) { + self::MALE => __('Male'), + self::FEMALE => __('Female'), + self::OTHER => __('Other'), + }; } } diff --git a/src/Domain/User/Enum/UserActivity.php b/src/Domain/User/Enum/UserActivity.php index d46dd631..5c59b5e1 100644 --- a/src/Domain/User/Enum/UserActivity.php +++ b/src/Domain/User/Enum/UserActivity.php @@ -9,21 +9,13 @@ enum UserActivity: string case DELETED = 'deleted'; case READ = 'read'; - /** - * Calling the translation function __() for each enum value - * so that poedit recognizes them to be translated. - * When using the enum values, __() will work anywhere as it's - * setup here and translations are in the .mo files. - * - * @return array - */ - private function getTranslatedValues(): array + public function getDisplayName(): string { - return [ - __('created'), - __('updated'), - __('deleted'), - __('read'), - ]; + return match ($this) { + self::CREATED => __('created'), + self::UPDATED => __('updated'), + self::DELETED => __('deleted'), + self::READ => __('read'), + }; } } diff --git a/src/Domain/User/Enum/UserLang.php b/src/Domain/User/Enum/UserLang.php index 17167395..ee7e7f21 100644 --- a/src/Domain/User/Enum/UserLang.php +++ b/src/Domain/User/Enum/UserLang.php @@ -7,6 +7,7 @@ enum UserLang: string { use EnumToArray; + // Case names are used as label names for the radio buttons hence the upper case first letter // It isn't ideal however as only ASCII chars are allowed and "Français" already has a non-ASCII char, // so it would probably be a lot better if name and value was switched BUT unfortunately PHP does not diff --git a/src/Domain/User/Enum/UserRole.php b/src/Domain/User/Enum/UserRole.php index f27030d9..c19ccb0a 100644 --- a/src/Domain/User/Enum/UserRole.php +++ b/src/Domain/User/Enum/UserRole.php @@ -2,8 +2,12 @@ namespace App\Domain\User\Enum; +use App\Common\Trait\EnumToArray; + enum UserRole: string { + use EnumToArray; + // Value is `user_role`.`name` case NEWCOMER = 'newcomer'; case ADVISOR = 'advisor'; @@ -11,31 +15,17 @@ enum UserRole: string case ADMIN = 'admin'; /** - * Calling the translation function __() for each enum value - * so that poedit recognizes them to be translated. - * When using the enum values, __() will work as it's - * setup here and translations are in the .mo files. - * - * @return array - */ - public static function getTranslatedValues(): array - { - return [ - __('Newcomer'), - __('Advisor'), - __('Managing advisor'), - __('Admin'), - ]; - } - - /** - * Removes underscore and adds capital first letter. + * Get the enum case name that can be displayed by the frontend. * * @return string */ - public function roleNameForDropdown(): string + public function getDisplayName(): string { - // Resulting string is a key in the function getTranslatedValues so __() knows how to translate - return __(ucfirst(str_replace('_', ' ', $this->value))); + return match ($this) { + self::NEWCOMER => __('Newcomer'), + self::ADVISOR => __('Advisor'), + self::MANAGING_ADVISOR => __('Managing advisor'), + self::ADMIN => __('Admin'), + }; } } diff --git a/src/Domain/User/Enum/UserStatus.php b/src/Domain/User/Enum/UserStatus.php index 88080fd2..7f4100f2 100644 --- a/src/Domain/User/Enum/UserStatus.php +++ b/src/Domain/User/Enum/UserStatus.php @@ -20,19 +20,17 @@ enum UserStatus: string // UserStatus::toArray() returns array for dropdown /** - * Each enum case has to be in the function below with - * called by the translation function __() so that poedit - * recognizes the strings to translate. + * Get the enum case name that can be displayed by the frontend. * - * @return array + * @return string */ - public static function getTranslatedValues(): array + public function getDisplayName(): string { - return [ - __('Unverified'), - __('Active'), - __('Locked'), - __('Suspended'), - ]; + return match ($this) { + self::Unverified => __('Unverified'), + self::Active => __('Active'), + self::Locked => __('Locked'), + self::Suspended => __('Suspended'), + }; } } diff --git a/src/Domain/User/Service/UserUtilFinder.php b/src/Domain/User/Service/UserUtilFinder.php index 446459d1..44fcf8b2 100644 --- a/src/Domain/User/Service/UserUtilFinder.php +++ b/src/Domain/User/Service/UserUtilFinder.php @@ -22,7 +22,7 @@ public function findUserDropdownValues(): array { return [ 'userRoles' => $this->authorizedUserRoleFilterer->filterAuthorizedUserRoles(), - 'statuses' => UserStatus::toTranslatedNamesArray(), + 'statuses' => UserStatus::getAllDisplayNames(), 'languages' => UserLang::toArray(), ]; } diff --git a/src/Domain/UserActivity/Service/UserActivityFinder.php b/src/Domain/UserActivity/Service/UserActivityFinder.php index c5efca02..9d51bb27 100644 --- a/src/Domain/UserActivity/Service/UserActivityFinder.php +++ b/src/Domain/UserActivity/Service/UserActivityFinder.php @@ -78,7 +78,7 @@ private function findUserActivitiesGroupedByDate(int|string|array $userIds): arr $userActivity->pageUrl = null; } // Add the time and action name - $actionVal = __($userActivity->action->value); + $actionVal = $userActivity->action->getDisplayName(); // ucfirst does not work for non english chars. Below is an equivalent that also works for german chars. $ucFirstActionValue = mb_strtoupper(mb_substr($actionVal, 0, 1)) . mb_substr($actionVal, 1); $userActivity->timeAndActionName = $userActivity->datetime?->format('H:i') . ': ' . $ucFirstActionValue; diff --git a/src/Domain/UserActivity/Service/UserActivityLogger.php b/src/Domain/UserActivity/Service/UserActivityLogger.php index 899999f3..a8a6a5f8 100644 --- a/src/Domain/UserActivity/Service/UserActivityLogger.php +++ b/src/Domain/UserActivity/Service/UserActivityLogger.php @@ -20,7 +20,7 @@ public function __construct( * * @param UserActivity $userActivityAction * @param string $table A better name should be found as the service should not know the table name - * @param int $rowId + * @param int|null $rowId * @param array|null $data * @param int|null $userId in case there is no session like on login * diff --git a/templates/client/client-read.html.php b/templates/client/client-read.html.php index c83d5140..d9b1da1f 100644 --- a/templates/client/client-read.html.php +++ b/templates/client/client-read.html.php @@ -276,7 +276,7 @@ class="contenteditable-edit-icon cursor-pointer" alt="Edit" vigilanceLevel ? html($clientReadData->vigilanceLevel->prettyName()) + >vigilanceLevel ? html($clientReadData->vigilanceLevel->getDisplayName()) : '' ?> diff --git a/tests/Integration/Authentication/LogoutActionTest.php b/tests/Integration/Authentication/LogoutActionTest.php new file mode 100644 index 00000000..187bb92b --- /dev/null +++ b/tests/Integration/Authentication/LogoutActionTest.php @@ -0,0 +1,62 @@ +insertFixture(UserFixture::class); + // Authenticate user + $this->container->get(SessionInterface::class)->set('user_id', $user['id']); + + // Create request + $request = $this->createRequest('GET', $this->urlFor('logout')); + $response = $this->app->handle($request); + + // Assert: 302 Found (redirect) + self::assertSame(302, $response->getStatusCode()); + + // Assert that session user_id is not set + self::assertNull($this->container->get(SessionInterface::class)->get('user_id')); + } + + /** + * Test that when user is not active but still logged in and tries + * to access a protected route, the user is logged out. + * + * @return void + */ + public function testLogoutWhenUserNotActive(): void + { + // Insert user fixture + $user = $this->insertFixture(UserFixture::class, ['status' => UserStatus::Suspended->value]); + // Authenticate user + $this->container->get(SessionInterface::class)->set('user_id', $user['id']); + + // Create request + $request = $this->createRequest('GET', $this->urlFor('profile-page')); + $response = $this->app->handle($request); + + // Assert: 302 Found (redirect) + self::assertSame(302, $response->getStatusCode()); + + // Assert that session user_id is not set + self::assertNull($this->container->get(SessionInterface::class)->get('user_id')); + } +} diff --git a/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php b/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php index 36a38baa..5df9e48a 100644 --- a/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php +++ b/tests/Integration/Authentication/PasswordForgottenEmailSubmitActionTest.php @@ -6,6 +6,7 @@ use App\Test\Trait\AppTestTrait; use Fig\Http\Message\StatusCodeInterface; use Odan\Session\SessionInterface; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use TestTraits\Trait\DatabaseTestTrait; use TestTraits\Trait\FixtureTestTrait; @@ -39,7 +40,7 @@ public function testPasswordForgottenEmailSubmit(): void $userId = $userRow['id']; $email = $userRow['email']; - // Simulate logged-in user with id 2 + // Simulate logged-in user $this->container->get(SessionInterface::class)->set('user_id', $userId); $request = $this->createFormRequest( @@ -102,6 +103,60 @@ public function testPasswordForgottenEmailSubmit(): void ); } + /** + * Assert that verification email is not sent if the email threshold is reached. + * + * @param int|string $delay + * @param int $emailLogAmountInTimeSpan + * @param array $securitySettings + * + * @return void + */ + #[DataProviderExternal(\App\Test\Provider\Security\EmailRequestProvider::class, 'individualEmailThrottlingTestCases')] + public function testPasswordForgottenEmailSubmitSecurityThrottling( + int|string $delay, + int $emailLogAmountInTimeSpan, + array $securitySettings + ): void { + // Insert user + $userRow = $this->insertFixture(UserFixture::class); + $userId = $userRow['id']; + $email = $userRow['email']; + + // Insert email log entries + for ($i = 0; $i < $emailLogAmountInTimeSpan; $i++) { + $this->insertFixtureRow( + 'email_log', + ['to_email' => $email, 'created_at' => (new \DateTime())->format('Y-m-d H:i:s')] + ); + } + + // Simulate logged-in user + $this->container->get(SessionInterface::class)->set('user_id', $userId); + + $request = $this->createFormRequest( + 'POST', // Request to change password + $this->urlFor('password-forgotten-email-submit'), + [ + 'email' => $email, + ] + ); + + $response = $this->app->handle($request); + + // Assert: 422 Unprocessable Entity + self::assertSame(StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, $response->getStatusCode()); + + // Email assertions + // Assert email was not sent + $this->assertEmailCount(0); + + // Database assertions + + // Assert that there is no verification token in the database + $this->assertTableRowCount(0, 'user_verification'); + } + /** * Test that nothing special is done if user does not exist. */ @@ -136,7 +191,7 @@ public function testPasswordForgottenEmailSubmitInvalidData(): void // Insert user $userRow = $this->insertFixture(UserFixture::class); - // Simulate logged-in user with id 2 + // Simulate logged-in user $this->container->get(SessionInterface::class)->set('user_id', $userRow['id']); $request = $this->createFormRequest( diff --git a/tests/Integration/Authentication/RegisterVerifyActionTest.php b/tests/Integration/Authentication/RegisterVerifyActionTest.php index 499f7c81..60001065 100644 --- a/tests/Integration/Authentication/RegisterVerifyActionTest.php +++ b/tests/Integration/Authentication/RegisterVerifyActionTest.php @@ -125,6 +125,43 @@ public function testRegisterVerificationAlreadyVerified( self::assertNull($session->get('user_id')); } + #[DataProviderExternal(\App\Test\Provider\Authentication\UserVerificationProvider::class, 'userVerificationProvider')] + public function testRegisterVerificationAlreadyVerifiedAndAuthenticated( + UserVerificationData $verification, + string $clearTextToken + ): void { + // User needed to insert verification + $userRow = $this->insertFixture( + UserFixture::class, + ['id' => $verification->userId, 'status' => UserStatus::Active->value], + ); + // Insert user verification + $this->insertFixtureRow('user_verification', $verification->toArrayForDatabase()); + // Test redirect param to this page + $redirectLocation = $this->urlFor('profile-page'); + $queryParams = [ + // To test redirect + 'redirect' => $redirectLocation, + 'token' => $clearTextToken, + 'id' => (string)$verification->id, + ]; + + // Authenticate user + $this->container->get(SessionInterface::class)->set('user_id', $userRow['id']); + + $request = $this->createRequest('GET', $this->urlFor('register-verification', [], $queryParams)); + + $response = $this->app->handle($request); + + // Assert that redirect worked when logged in + self::assertSame($redirectLocation, $response->getHeaderLine('Location')); + self::assertSame(StatusCodeInterface::STATUS_FOUND, $response->getStatusCode()); + + // Assert that info flash message is set that informs user that they are already logged in + $flash = $this->container->get(SessionInterface::class)->getFlash()->all(); + self::assertStringContainsString('You are already logged in', $flash['info'][0]); + } + /** * Link in email contains the verification db entry id and if this id is incorrect (token not found) * according exception should be thrown and user redirected to register page. diff --git a/tests/Integration/Client/ClientCreateDropdownOptions.php b/tests/Integration/Client/ClientCreateDropdownOptionsTest.php similarity index 98% rename from tests/Integration/Client/ClientCreateDropdownOptions.php rename to tests/Integration/Client/ClientCreateDropdownOptionsTest.php index d374ce34..230851d2 100644 --- a/tests/Integration/Client/ClientCreateDropdownOptions.php +++ b/tests/Integration/Client/ClientCreateDropdownOptionsTest.php @@ -16,7 +16,7 @@ use TestTraits\Trait\HttpTestTrait; use TestTraits\Trait\RouteTestTrait; -class ClientCreateDropdownOptions extends TestCase +class ClientCreateDropdownOptionsTest extends TestCase { use AppTestTrait; use HttpTestTrait; diff --git a/tests/Integration/Client/ClientDeleteActionTest.php b/tests/Integration/Client/ClientDeleteActionTest.php index 310e1d97..291c8c4b 100644 --- a/tests/Integration/Client/ClientDeleteActionTest.php +++ b/tests/Integration/Client/ClientDeleteActionTest.php @@ -3,8 +3,10 @@ namespace App\Test\Integration\Client; use App\Domain\User\Enum\UserActivity; +use App\Domain\User\Enum\UserRole; use App\Test\Fixture\ClientFixture; use App\Test\Fixture\ClientStatusFixture; +use App\Test\Fixture\UserFixture; use App\Test\Trait\AppTestTrait; use App\Test\Trait\AuthorizationTestTrait; use Fig\Http\Message\StatusCodeInterface; @@ -97,6 +99,38 @@ public function testClientSubmitDeleteActionAuthorization( $this->assertJsonData($expectedResult['jsonResponse'], $response); } + /** + * Delete request from authorized user but client does not exist. + * + * @return void + */ + public function testClientSubmitDeleteError(): void + { + // Insert authenticated authorized user + $userRow = $this->insertFixture( + UserFixture::class, + $this->addUserRoleId(['user_role_id' => UserRole::ADMIN]) + ); + + // Not inserting client + + // Simulate logged-in user + $this->container->get(SessionInterface::class)->set('user_id', $userRow['id']); + + $request = $this->createJsonRequest( + 'DELETE', + $this->urlFor('client-delete-submit', ['client_id' => '1']), + ); + + $response = $this->app->handle($request); + + // Assert response HTTP status code: 200 + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + + // Assert response json content + $this->assertJsonData(['status' => 'warning', 'message' => 'Client has not been deleted.'], $response); + } + /** * Test that when user is not logged in 401 Unauthorized is returned. * @@ -120,6 +154,4 @@ public function testClientSubmitDeleteActionUnauthenticated(): void // Assert that response contains correct login url $this->assertJsonData(['loginUrl' => $expectedLoginUrl], $response); } - - // Unchanged content test not done as it's not being used by the frontend } diff --git a/tests/Integration/Client/ClientListActionTest.php b/tests/Integration/Client/ClientListActionTest.php index 89dc5a51..5a12a475 100644 --- a/tests/Integration/Client/ClientListActionTest.php +++ b/tests/Integration/Client/ClientListActionTest.php @@ -61,6 +61,9 @@ public function testClientListPageActionAuthorization(): void UserFixture::class, $this->addUserRoleId(['user_role_id' => UserRole::NEWCOMER]), ); + // Insert other user and status to fully load the different filter chip options + $this->insertFixture(UserFixture::class); + $this->insertFixture(ClientStatusFixture::class); $request = $this->createRequest('GET', $this->urlFor('client-list-page')); // Simulate logged-in user by setting the user_id session variable diff --git a/tests/Integration/Client/ClientReadPageActionTest.php b/tests/Integration/Client/ClientReadPageActionTest.php index ee87e44a..6a82603d 100644 --- a/tests/Integration/Client/ClientReadPageActionTest.php +++ b/tests/Integration/Client/ClientReadPageActionTest.php @@ -4,10 +4,11 @@ use App\Test\Fixture\ClientFixture; use App\Test\Fixture\ClientStatusFixture; -use App\Test\Fixture\UserFixture; use App\Test\Trait\AppTestTrait; +use App\Test\Trait\AuthorizationTestTrait; use Fig\Http\Message\StatusCodeInterface; use Odan\Session\SessionInterface; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use TestTraits\Trait\DatabaseTestTrait; use TestTraits\Trait\FixtureTestTrait; @@ -26,32 +27,39 @@ class ClientReadPageActionTest extends TestCase use RouteTestTrait; use DatabaseTestTrait; use FixtureTestTrait; + use AuthorizationTestTrait; + + #[DataProviderExternal(\App\Test\Provider\Client\ClientReadProvider::class, 'clientReadAuthorizationCases')] + public function testClientReadPageActionAuthorization( + array $userRow, + array $authenticatedUserRow, + bool $clientIsDeleted, + int $expectedStatusCode, + ): void { + // Insert tested and authenticated user + $this->insertUserFixturesWithAttributes($authenticatedUserRow, $userRow); - /** - * Normal page action while being authenticated. - * - * @return void - */ - public function testClientReadPageActionAuthenticated(): void - { - // Add needed database values to correctly display the page - // Insert authenticated user permitted to see client read page - $userId = $this->insertFixture(UserFixture::class)['id']; // Insert linked client status $clientStatusId = $this->insertFixture(ClientStatusFixture::class)['id']; // Insert client linked to user to be sure that the user is permitted to see the client read page $clientRow = $this->insertFixture( ClientFixture::class, - ['user_id' => $userId, 'client_status_id' => $clientStatusId], + [ + 'user_id' => $userRow['id'], + 'client_status_id' => $clientStatusId, + 'deleted_at' => $clientIsDeleted ? (new \DateTime())->format('Y-m-d H:i:s') : null, + ], ); - $request = $this->createRequest('GET', $this->urlFor('client-read-page', ['client_id' => $clientRow['id']])); // Simulate logged-in user by setting the user_id session variable - $this->container->get(SessionInterface::class)->set('user_id', $clientRow['user_id']); + $this->container->get(SessionInterface::class)->set('user_id', $authenticatedUserRow['id']); + + $request = $this->createRequest('GET', $this->urlFor('client-read-page', ['client_id' => $clientRow['id']])); $response = $this->app->handle($request); - // Assert 200 OK - self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + + // Assert 200 OK - code only reaches here if no exception is thrown + self::assertSame($expectedStatusCode, $response->getStatusCode()); } /** diff --git a/tests/Integration/Client/ClientUpdateActionTest.php b/tests/Integration/Client/ClientUpdateActionTest.php index 7ad4104c..36a899b3 100644 --- a/tests/Integration/Client/ClientUpdateActionTest.php +++ b/tests/Integration/Client/ClientUpdateActionTest.php @@ -220,10 +220,10 @@ public function testClientSubmitUpdateActionUnauthenticated(): void } /** - * Test that if user makes update request but the content has not changed - * compared to what's in the database, the response contains the warning. + * Test that if user makes update request but the content is the same + * as what's in the database, the response contains the warning. * - * @throws ContainerExceptionInterface|NotFoundExceptionInterface + *@throws ContainerExceptionInterface|NotFoundExceptionInterface * * @return void */ diff --git a/tests/Integration/Dashboard/DashboardTogglePanelActionTest.php b/tests/Integration/Dashboard/DashboardTogglePanelActionTest.php index 6cad0400..834cecb7 100644 --- a/tests/Integration/Dashboard/DashboardTogglePanelActionTest.php +++ b/tests/Integration/Dashboard/DashboardTogglePanelActionTest.php @@ -75,4 +75,36 @@ public function testDashboardTogglePanelActionAuthenticated(): void 'assigned-to-me-panel' ); } + + /** + * Test dashboard toggle panel action when request body is malformed. + * + * @return void + */ + public function testDashboardTogglePanelMalformedRequestBody(): void + { + // Insert linked and authenticated user + $userId = $this->insertFixture(UserFixture::class)['id']; + + // Simulate logged-in user by setting the user_id session variable + $this->container->get(SessionInterface::class)->set('user_id', $userId); + + $request = $this->createJsonRequest( + 'PUT', + $this->urlFor('dashboard-toggle-panel'), + [ + 'invalid' => 'oops', + ] + ); + + $this->expectException(\Slim\Exception\HttpBadRequestException::class); + + $this->app->handle($request); + + // Assert flash message + $flash = $this->container->get(SessionInterface::class)->getFlash()->all(); + self::assertStringContainsString('Malformed request body syntax', $flash['info'][0]); + + $this->assertTableRowCount(0, 'user_filter_setting'); + } } diff --git a/tests/Integration/Note/NoteCreateActionTest.php b/tests/Integration/Note/NoteCreateActionTest.php index 57a87035..5c216544 100644 --- a/tests/Integration/Note/NoteCreateActionTest.php +++ b/tests/Integration/Note/NoteCreateActionTest.php @@ -68,7 +68,7 @@ public function testNoteSubmitCreateActionAuthorization( $noteMessage = 'Test note'; $request = $this->createJsonRequest( 'POST', - $this->urlFor('note-submit-creation'), + $this->urlFor('note-create-submit'), [ 'message' => $noteMessage, 'client_id' => $clientRow['id'], @@ -130,7 +130,7 @@ public function testNoteSubmitCreateActionAuthorization( */ public function testNoteSubmitCreateActionUnauthenticated(): void { - $request = $this->createJsonRequest('POST', $this->urlFor('note-submit-creation')); + $request = $this->createJsonRequest('POST', $this->urlFor('note-create-submit')); // Make request $response = $this->app->handle($request); // Assert response HTTP status code: 401 Unauthorized @@ -182,7 +182,7 @@ public function testNoteCreateSubmitActionInvalid( $request = $this->createJsonRequest( 'POST', - $this->urlFor('note-submit-creation'), + $this->urlFor('note-create-submit'), $invalidRequestBody ); $response = $this->app->handle($request); diff --git a/tests/Integration/Note/NoteDeleteActionTest.php b/tests/Integration/Note/NoteDeleteActionTest.php index c2c575cf..c7fb0420 100644 --- a/tests/Integration/Note/NoteDeleteActionTest.php +++ b/tests/Integration/Note/NoteDeleteActionTest.php @@ -3,9 +3,11 @@ namespace App\Test\Integration\Note; use App\Domain\User\Enum\UserActivity; +use App\Domain\User\Enum\UserRole; use App\Test\Fixture\ClientFixture; use App\Test\Fixture\ClientStatusFixture; use App\Test\Fixture\NoteFixture; +use App\Test\Fixture\UserFixture; use App\Test\Trait\AppTestTrait; use App\Test\Trait\AuthorizationTestTrait; use Fig\Http\Message\StatusCodeInterface; @@ -86,7 +88,7 @@ public function testNoteSubmitDeleteActionAuthorization( // --- *NORMAL NOTE REQUEST --- $normalNoteRequest = $this->createJsonRequest( 'DELETE', - $this->urlFor('note-submit-delete', ['note_id' => $normalNoteData['id']]), + $this->urlFor('note-delete-submit', ['note_id' => $normalNoteData['id']]), ); // Make request $normalNoteResponse = $this->app->handle($normalNoteRequest); @@ -124,7 +126,7 @@ public function testNoteSubmitDeleteActionAuthorization( // Create request to edit main note $mainNoteRequest = $this->createJsonRequest( 'DELETE', - $this->urlFor('note-submit-delete', ['note_id' => $mainNoteData['id']]), + $this->urlFor('note-delete-submit', ['note_id' => $mainNoteData['id']]), ); // Make request @@ -142,6 +144,33 @@ public function testNoteSubmitDeleteActionAuthorization( $this->assertTableRow(['deleted_at' => null], 'note', $mainNoteData['id']); } + public function testNoteSubmitDeleteError(): void + { + // Insert authenticated authorized user + $userRow = $this->insertFixture( + UserFixture::class, + $this->addUserRoleId(['user_role_id' => UserRole::ADMIN]) + ); + + // Not inserting note + + // Simulate logged-in user + $this->container->get(SessionInterface::class)->set('user_id', $userRow['id']); + + $request = $this->createJsonRequest( + 'DELETE', + $this->urlFor('note-delete-submit', ['note_id' => '1']), + ); + + $response = $this->app->handle($request); + + // Assert response HTTP status code: 200 + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + + // Assert response json content + $this->assertJsonData(['status' => 'warning', 'message' => 'Note has not been deleted.'], $response); + } + /** * Test note deletion on client-read page while being unauthenticated. * @@ -151,7 +180,7 @@ public function testNoteSubmitDeleteActionUnauthenticated(): void { $request = $this->createJsonRequest( 'DELETE', - $this->urlFor('note-submit-delete', ['note_id' => '1']), + $this->urlFor('note-delete-submit', ['note_id' => '1']), ); // Make request diff --git a/tests/Integration/Note/NoteUpdateActionTest.php b/tests/Integration/Note/NoteUpdateActionTest.php index 93eb1e93..0ed8aa68 100644 --- a/tests/Integration/Note/NoteUpdateActionTest.php +++ b/tests/Integration/Note/NoteUpdateActionTest.php @@ -83,7 +83,7 @@ public function testNoteSubmitUpdateActionAuthorization( // Create request to edit main note $mainNoteRequest = $this->createJsonRequest( 'PUT', - $this->urlFor('note-submit-modification', ['note_id' => $mainNoteRow['id']]), + $this->urlFor('note-update-submit', ['note_id' => $mainNoteRow['id']]), ['message' => $newNoteMessage, 'is_main' => 1] ); // Make request @@ -108,7 +108,7 @@ public function testNoteSubmitUpdateActionAuthorization( // --- *NORMAL NOTE REQUEST --- $normalNoteRequest = $this->createJsonRequest( 'PUT', - $this->urlFor('note-submit-modification', ['note_id' => $normalNoteRow['id']]), + $this->urlFor('note-update-submit', ['note_id' => $normalNoteRow['id']]), // Change the two values that may be changed ['message' => $newNoteMessage, 'hidden' => 1] ); @@ -145,6 +145,47 @@ public function testNoteSubmitUpdateActionAuthorization( $this->assertJsonData($expectedResult['modification']['normalNote']['jsonResponse'], $normalNoteResponse); } + /** + * Test that if user makes update request but the content is the same + * as what's in the database, the response contains the warning. + */ + public function testNoteSubmitUpdateUnchangedContent(): void + { + // Insert authorized user + $userId = $this->insertFixture( + UserFixture::class, + $this->addUserRoleId(['user_role_id' => UserRole::ADMIN]), + )['id']; + // Insert linked client status + $clientStatusId = $this->insertFixture(ClientStatusFixture::class)['id']; + // Insert client row + $clientRow = $this->insertFixture( + ClientFixture::class, + ['user_id' => $userId, 'client_status_id' => $clientStatusId], + ); + + // Insert note linked to client and user + $noteData = $this->insertFixture( + NoteFixture::class, + ['client_id' => $clientRow['id'], 'user_id' => $userId, 'is_main' => 0], + ); + + // Simulate logged-in user with same user as linked to client + $this->container->get(SessionInterface::class)->set('user_id', $userId); + + $request = $this->createJsonRequest( + 'PUT', + $this->urlFor('note-update-submit', ['note_id' => $noteData['id']]), + ['message' => $noteData['message']] + ); + $response = $this->app->handle($request); + + // Assert 200 OK + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + // Assert that response contains warning + $this->assertJsonData(['status' => 'warning', 'message' => 'The note was not updated.'], $response); + } + /** * Test note modification on client-read page while being unauthenticated. * @@ -154,7 +195,7 @@ public function testNoteSubmitUpdateActionUnauthenticated(): void { $request = $this->createJsonRequest( 'PUT', - $this->urlFor('note-submit-modification', ['note_id' => '1']), + $this->urlFor('note-update-submit', ['note_id' => '1']), ['message' => 'New test message'] ); @@ -203,7 +244,7 @@ public function testNoteSubmitUpdateActionInvalid(array $invalidRequestBody, arr $request = $this->createJsonRequest( 'PUT', - $this->urlFor('note-submit-modification', ['note_id' => $noteData['id']]), + $this->urlFor('note-update-submit', ['note_id' => $noteData['id']]), $invalidRequestBody ); $response = $this->app->handle($request); diff --git a/tests/Integration/User/UserCreateDropdownOptionsTest.php b/tests/Integration/User/UserCreateDropdownOptionsTest.php new file mode 100644 index 00000000..e9d20028 --- /dev/null +++ b/tests/Integration/User/UserCreateDropdownOptionsTest.php @@ -0,0 +1,76 @@ +insertFixture( + UserFixture::class, + $this->addUserRoleId($authenticatedUserAttributes) + ); + + // Simulate logged-in user with same user as linked to client + $this->container->get(SessionInterface::class)->set('user_id', $authenticatedUserRow['id']); + + $request = $this->createJsonRequest( + 'GET', + $this->urlFor('user-create-dropdown'), + ); + + // Handle request after defining expected exceptions + $response = $this->app->handle($request); + + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + + $responseJson = $this->getJsonData($response); + // Assert equals without taking into account user id as it is not known in data provider + self::assertEqualsCanonicalizing($expectedUserRoles, $responseJson['userRoles']); + + // Assert statuses + self::assertEqualsCanonicalizing(UserStatus::getAllDisplayNames(), $responseJson['statuses']); + + // Assert languages + self::assertEqualsCanonicalizing(UserLang::toArray(), $responseJson['languages']); + } +} diff --git a/tests/Integration/User/UserDeleteActionTest.php b/tests/Integration/User/UserDeleteActionTest.php index 38c157a8..b5cafd27 100644 --- a/tests/Integration/User/UserDeleteActionTest.php +++ b/tests/Integration/User/UserDeleteActionTest.php @@ -107,8 +107,4 @@ public function testUserSubmitDeleteActionUnauthenticated(): void // Assert that response contains correct login url $this->assertJsonData(['loginUrl' => $this->urlFor('login-page')], $response); } - - // Unchanged content test not done as it's not being used by the frontend - // Malformed request body also not so relevant as there is no body for deletion - // Invalid data not relevant either as there is no data in the request body } diff --git a/tests/Integration/User/UserListActionTest.php b/tests/Integration/User/UserListActionTest.php index 70c3970c..04a4a323 100644 --- a/tests/Integration/User/UserListActionTest.php +++ b/tests/Integration/User/UserListActionTest.php @@ -2,6 +2,7 @@ namespace App\Test\Integration\User; +use App\Domain\User\Enum\UserRole; use App\Domain\User\Enum\UserStatus; use App\Test\Trait\AppTestTrait; use App\Test\Trait\AuthorizationTestTrait; @@ -60,7 +61,7 @@ public function testUserListAuthorization( // Init expected response array general format $expectedResponseArray = [ 'userResultDataArray' => [], - 'statuses' => UserStatus::toTranslatedNamesArray(), + 'statuses' => UserStatus::getAllDisplayNames(), ]; // Add response array of authenticated user to the expected userResultDataArray @@ -104,7 +105,7 @@ public function testUserListAuthorization( * Change array of UserRole Enum cases to expected availableUserRoles * array from the server with id and capitalized role name [{id} => {Role name}]. * - * @param array $userRoles user roles with Enum cases array + * @param UserRole[] $userRoles user roles with Enum cases array * * @return array */ @@ -113,7 +114,7 @@ protected function formatAvailableUserRoles(array $userRoles): array $formattedRoles = []; foreach ($userRoles as $userRole) { // Key is role id and value the name for the dropdown - $formattedRoles[$this->getUserRoleIdByEnum($userRole)] = $userRole->roleNameForDropdown(); + $formattedRoles[$this->getUserRoleIdByEnum($userRole)] = $userRole->getDisplayName(); } return $formattedRoles; diff --git a/tests/Integration/User/UserReadPageActionTest.php b/tests/Integration/User/UserReadPageActionTest.php index 99409ffe..0a6528ce 100644 --- a/tests/Integration/User/UserReadPageActionTest.php +++ b/tests/Integration/User/UserReadPageActionTest.php @@ -2,6 +2,7 @@ namespace App\Test\Integration\User; +use App\Test\Fixture\UserFixture; use App\Test\Trait\AppTestTrait; use App\Test\Trait\AuthorizationTestTrait; use Fig\Http\Message\StatusCodeInterface; @@ -51,6 +52,26 @@ public function testUserReadPageActionAuthorization( self::assertSame($expectedResult[StatusCodeInterface::class], $response->getStatusCode()); } + /** + * Test that http not found exception is thrown when + * user tries to read non-existing user page. + * + * @return void + */ + public function testUserReadPageActionNotExisting(): void + { + $userRow = $this->insertFixture(UserFixture::class); + + // Simulate logged-in user by setting the user_id session variable + $this->container->get(SessionInterface::class)->set('user_id', $userRow['id']); + + $request = $this->createRequest('GET', $this->urlFor('user-read-page', ['user_id' => '99'])); + + $this->expectException(\Slim\Exception\HttpNotFoundException::class); + + $response = $this->app->handle($request); + } + /** * Test that user has to be logged in to display the page. * diff --git a/tests/Provider/Client/ClientCreateProvider.php b/tests/Provider/Client/ClientCreateProvider.php index 15b5a2c1..e962057c 100644 --- a/tests/Provider/Client/ClientCreateProvider.php +++ b/tests/Provider/Client/ClientCreateProvider.php @@ -12,8 +12,8 @@ class ClientCreateProvider * * Each test case is an array with the following structure: * - 'authenticatedUserRow': An array of attributes for the authenticated user. This includes 'first_name' and 'user_role_id'. - * - 'other_user': An array of attributes for another user. This includes 'first_name' and 'user_role_id'. - * - 'expected_user_names': An array of expected user names. This is used to verify the output of the function being tested. + * - 'otherUserRow': An array of attributes for another user. This includes 'first_name' and 'user_role_id'. + * - 'expectedUserNames': An array of expected names. This is used to verify the output of the function being tested. * * @return array an array of test cases */ @@ -31,20 +31,20 @@ public static function clientCreationDropdownOptionsCases(): array // "owner" means from the perspective of the authenticated user [ // ? Newcomer - not allowed so nothing should be returned 'authenticatedUserRow' => $newcomerAttributes, - 'other_user' => $advisorAttributes, - 'expected_user_names' => [], + 'otherUserRow' => $advisorAttributes, + 'expectedUserNames' => [], ], [ // ? Advisor - should return only himself 'authenticatedUserRow' => $advisorAttributes, - 'other_user' => $newcomerAttributes, + 'otherUserRow' => $newcomerAttributes, // id not relevant only name - 'expected_user_names' => [$advisorAttributes['first_name']], + 'expectedUserNames' => [$advisorAttributes['first_name']], ], [ // ? Managing advisor - should return all available users 'authenticatedUserRow' => $managingAdvisorAttributes, - 'other_user' => $newcomerAttributes, + 'otherUserRow' => $newcomerAttributes, // All available users are authenticated manager advisor and newcomer as the "other" user - 'expected_user_names' => [$managingAdvisorAttributes['first_name'], $newcomerAttributes['first_name']], + 'expectedUserNames' => [$managingAdvisorAttributes['first_name'], $newcomerAttributes['first_name']], ], ]; } diff --git a/tests/Provider/Client/ClientReadProvider.php b/tests/Provider/Client/ClientReadProvider.php new file mode 100644 index 00000000..556b6215 --- /dev/null +++ b/tests/Provider/Client/ClientReadProvider.php @@ -0,0 +1,39 @@ + UserRole::ADMIN]; + $managingAdvisorAttr = ['user_role_id' => UserRole::MANAGING_ADVISOR]; + $advisorAttr = ['user_role_id' => UserRole::ADVISOR]; + + // Testing authorization: with the lowest allowed privilege and with highest not allowed + return [ // User owner is the user itself + [// ? advisor not owner - allowed to read undeleted client + 'userRow' => $managingAdvisorAttr, + 'authenticatedUserRow' => $advisorAttr, + 'clientIsDeleted' => false, + 'expectedStatusCode' => StatusCodeInterface::STATUS_OK, + ], + [// ? advisor owner - not allowed to read deleted client + 'userRow' => $advisorAttr, + 'authenticatedUserRow' => $advisorAttr, + 'clientIsDeleted' => true, + 'expectedStatusCode' => StatusCodeInterface::STATUS_FORBIDDEN, + ], + [// ? managing advisor not owner - allowed to read deleted client + 'userRow' => $adminAttr, + 'authenticatedUserRow' => $managingAdvisorAttr, + 'clientIsDeleted' => true, + 'expectedStatusCode' => StatusCodeInterface::STATUS_OK, + ], + ]; + } +} diff --git a/tests/Provider/User/UserCreateProvider.php b/tests/Provider/User/UserCreateProvider.php index 1c8c4528..c0f35ab6 100644 --- a/tests/Provider/User/UserCreateProvider.php +++ b/tests/Provider/User/UserCreateProvider.php @@ -173,4 +173,31 @@ public static function invalidUserCreateCases(): array ], ]; } + + public static function userCreationDropdownOptionsCases(): array + { + // Set users with different roles + $adminAttributes = ['user_role_id' => UserRole::ADMIN]; + $managingAdvisorAttributes = ['user_role_id' => UserRole::MANAGING_ADVISOR]; + $advisorAttributes = ['user_role_id' => UserRole::ADVISOR]; + + // Newcomer must not receive any available user + // Advisor is allowed to create client but only assign it to himself or leave user_id empty + // Managing advisor and higher should receive all available users + return [ + [ // ? Advisor - not allowed to create user so no available roles + 'authenticatedUserAttributes' => $advisorAttributes, + // id not relevant only name + 'expectedUserRoles' => [], + ], + [ // ? Managing advisor - should return advisor and newcomer + 'authenticatedUserAttributes' => $managingAdvisorAttributes, + 'expectedUserRoles' => [UserRole::ADVISOR->getDisplayName(), UserRole::NEWCOMER->getDisplayName()], + ], + [ // ? Admin - should return all available users + 'authenticatedUserAttributes' => $adminAttributes, + 'expectedUserRoles' => UserRole::getAllDisplayNames(), + ], + ]; + } } diff --git a/tests/Provider/User/UserListProvider.php b/tests/Provider/User/UserListProvider.php index d84fc948..fa915dd7 100644 --- a/tests/Provider/User/UserListProvider.php +++ b/tests/Provider/User/UserListProvider.php @@ -21,7 +21,7 @@ public static function userListAuthorizationCases(): array $advisorAttr = ['user_role_id' => UserRole::ADVISOR]; $newcomerAttr = ['user_role_id' => UserRole::NEWCOMER]; - // General testing rule: test allowed with the lowest privilege and not allowed with highest not allowed + // Testing authorization: with the lowest allowed privilege and with highest not allowed return [ // User owner is the user itself [// ? advisor owner and newcomer other - not allowed to read other - only allowed to read own status and role 'userRow' => $newcomerAttr, diff --git a/tests/Provider/User/UserReadProvider.php b/tests/Provider/User/UserReadProvider.php index cf5b0274..3806a722 100644 --- a/tests/Provider/User/UserReadProvider.php +++ b/tests/Provider/User/UserReadProvider.php @@ -9,8 +9,6 @@ class UserReadProvider { /** * Provides authenticated and other user which is requested to be read. - * Only status code can be asserted as expected result as page is rendered - * by the server, and we can't test a rendered template. */ public static function userReadAuthorizationCases(): array { @@ -20,7 +18,7 @@ public static function userReadAuthorizationCases(): array $advisorAttr = ['user_role_id' => UserRole::ADVISOR]; $newcomerAttr = ['user_role_id' => UserRole::NEWCOMER]; - // General testing rule: test allowed with the lowest privilege and not allowed with highest not allowed + // Testing authorization: with the lowest allowed privilege and with highest not allowed return [ // User owner is the user itself [// ? newcomer owner - other is same user - allowed to read own 'userRow' => $newcomerAttr,