diff --git a/README.md b/README.md index 2f77ef5..db2f735 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,41 @@ # REST API with Phalcon v6 -A REST API developed with Phalcon v6 +A REST API developed with Phalcon v6. This document explains how the project is organised, how the main components interact, and the important design decisions to keep in mind when extending the codebase. + +## Introduction + +Our goal is to build a REST API that has: +- Slim/efficient design +- Middleware +- JSON (or other) responses +- Action/Domain/Responder implementation +- JWT token authentication + +> This is not **THE** way to build a REST API with Phalcon. It is simply **A** way to do that. You can adopt this implementation if you wish, parts of it or none of it. + +This application has evolved significantly with every video release. Several areas were implemented in one way and later refactored to demonstrate the design trade-offs and how earlier choices affect the codebase. + +The main takeaways that we want to convey to developers are: +- The code has to be easy to read and understand +- Each component must do one thing and one thing only +- Components can be swapped out with others so the use of interfaces is essential +- Static analysis tools (PHPStan) must not produce errors +- Code coverage for tests must be at 100% + +Part 1 https://youtu.be/f3wP_M_NFKc +Part 2 https://youtu.be/VEZvUf_PdSY +Part 3 https://youtu.be/LP64Doh0t4g +Part 4 https://youtu.be/jCEZ2WMil8Q +Part 5 https://youtu.be/syU_3cIXFMM +Part 6 https://youtu.be/AgCbqW-leCM +Part 7 https://youtu.be/tGV4pSyVLdI +Part 8 https://youtu.be/GaJhNnw_1cE +Part 9 https://youtu.be/CWofDyTdToI + +## Directory Structure The directory structure for this projects follows the recommendations of [pds/skeleton][pds_skeleton] -# Folders The folders contain: - `bin`: empty for now, we might use it later on @@ -16,23 +47,61 @@ The folders contain: - `storage`: various storage data such as application logs - `tests`: tests -# Code organization +## High-level architecture + The application follows the [ADR pattern][adr_pattern] where the application is split into an `Action` layer, the `Domain` layer and a `Responder` layer. -## Action -The folder (under `src/`) contains the handler that is responsible for receiving data from the Domain and injecting it to the Responder so that the response can be generated. It also collects all the Input supplied by the user and injects it into the Domain for further processing. +- `Action` — receives HTTP input, collects and sanitizes request data, and calls a Domain service. +- `Domain` — contains the application logic. Implements small components, services that map to endpoints, validators, repositories and helpers. +- `Responder` — builds and emits the HTTP response from a `Payload`. -## Domain +Core files live under `src/` and are registered in the DI container in `src/Domain/Components/Container.php`. -The Domain is organized in folders based on their use. The `Components` folder contains components that are essential to the operation of the application, while the `Services` folder contains classes that map to endpoints of the application +## Main components -### Container +### `Action` layer -The application uses the `Phalcon\Di\Di` container with minimal components lazy loaded. Each non "core" component is also registered there (i.e. domain services, responder etc.) and all necessary dependencies are injected based on the service definitions. +Contains a handler that translate HTTP requests into Domain calls. For example, actions route requests to `LoginPostService`, `LogoutPostService`, `RefreshPostService` etc. -Additionally there are two `Providers` that are also registered in the DI container for further functionality. The `ErrorHandlerProvider` which caters for the starting up/shut down of the application and error logging, and the very important `RoutesProvider` which handles registering all the routes that the application serves. +### `Domain` layer + +#### `ADR` + +- `Payload`: A uniform result object used across Domain → Responder. +- `Input`: Class collecting request input and used to pass it to the domain +- Interfaces for domain and `Input` + +#### `Infrastructure` + +##### `Constants` + +Classes with constants and helper methods used throughout the application + +##### `DataSource` + +**`Auth`** -### Enums +Contains Data Transfer Objects (DTOs) to move data from input to domain and from database back to domain. A Facade is available for orchestration, sanitizer for input as well as validators. + +**`Interfaces`** + +Mapper and Sanitizer interfaces + +**`User`** + +Contains Data Transfer Objects (DTOs) to move data from input to domain and from database back to domain. A Facade is available for orchestration, a repository for database operations, sanitizer for input as well as validators. + +**`Validation`** + +Contains the `ValidatorInterface` for all validators, a `Result` object for returning back validation results/errors and the `AbsInt` validator to check the id for `Put` operations. + +##### `Encryption` + +Contains components for JWT handling and passwords. The `Security` component is a wrapper for the `password_*` PHP classes, which are used for password hashing and verification. + +The `TokenManager` offers methods to issue, refresh and revoke tokens. It works in conjunction with the `TokenCache` to store or invalidate tokens stored in Cache (Redis) + +##### `Enums` There are several enumerations present in the application. Those help with common values for tasks. For example the `FlagsEnum` holds the values for the `co_users.usr_status_flag` field. We could certainly introduce a lookup table in the database for "status" and hold the values there, joining it to the `co_users` table with a lookup table. However, this will introduce an extra join in our query which will inevitable reduce performance. Since the `FlagsEnum` can keep the various statuses, we keep everything in code instead of the database. Thorough tests for enumerations ensure that if a change is made in the future, tests will fail, so that database integrity can be kept. @@ -40,27 +109,85 @@ The `RoutesEnum` holds the various routes of the application. Every route is rep Finally, the `RoutesEnum` also holds the middleware array, which defines their execution and the "hook" they will execute in (before/after). -### Middleware +##### `Env` -There are several middleware registered for this application and they are being executed one after another (order matters) before the action is executed. As a result, the application will stop executing if an error occurs, or if certain validations fail. +The environment manager and adapters. It reads environment variables using [DotEnv][dotenv] as the main adapter but can be extended if necessary. -The middleware execution order is defined in the `RoutesEnum`. The available middleware is: +##### `Exceptions` + +Exception classes used in the application. + +##### `Container` + +The application uses the `Phalcon\Di\Di` container with minimal components lazy loaded. Each non "core" component is also registered there (i.e. domain services, responder etc.) and all necessary dependencies are injected based on the service definitions. + +Additionally there are two `Providers` that are also registered in the DI container for further functionality. The `ErrorHandlerProvider` which caters for the starting up/shut down of the application and error logging, and the very important `RoutesProvider` which handles registering all the routes that the application serves. + +#### `Services`: + +Separated also in `User` and `Auth` it contains the classes that the action handler will invoke. The naming of these services shows what endpoint they are targeting and what HTTP method will invoke them. For example the `LoginPostService` will be a `POST` to the `/auth/login`. + +### `Responder` + +The `JsonResponder` responder is responsible for constructing the response with the desired output, and emitting it back to the caller. For the moment we have only implemented a JSON response with a specified array as the payload to be sent back. + +The responder receives the outcome of the Domain, by means of a `Payload` object. The object contains all the data necessary to inject in the response. + +#### Response payload + +The application responds always with a specific JSON payload. The payload contains the following nodes: +- `data` - contains any data that are returned back (can be empty) +- `errors` - contains any errors occurred (can be empty) +- `meta` - array of information regarding the payload + - `code` - the application code returned + - `hash` - a `sha1` hash of the `data`, `errors` and timestamp + - `message` - `success` or `error` + - `timestamp` - the time in UTC format + + +## Request flow (example: login) + +1. Route matches and middleware runs (see Middleware section below). +2. `Action` extracts request body and calls `LoginPostService->handle($data)`. +3. `LoginPostService` calls the `AuthFacade->authenticate($input, $loginValidator)` (method injection). +4. `AuthFacade`: + - Builds DTO via `AuthInput`. + - Calls the supplied validator (`AuthLoginValidator`) which returns a `Result`. + - On success, fetches user via repository and verifies credentials (`Security`). + - Issues tokens via `TokenManager`. + - Returns a `Payload::success(...)`. +5. `Responder` builds JSON and returns HTTP response. + +## Validators -- [NotFoundMiddleware.php](src/Domain/Components/Middleware/NotFoundMiddleware.php) -- [HealthMiddleware.php](src/Domain/Components/Middleware/HealthMiddleware.php) -- [ValidateTokenClaimsMiddleware.php](src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php) -- [ValidateTokenPresenceMiddleware.php](src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php) -- [ValidateTokenRevokedMiddleware.php](src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php) -- [ValidateTokenStructureMiddleware.php](src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php) -- [ValidateTokenUserMiddleware.php](src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php) +- Specific validators exist for each potential input that needs to be validated +- Method injection is used for validators: the `AuthFacade` does not require a single validator in its constructor. Instead, callers pass the appropriate validator to each method: login uses `AuthLoginValidator`, logout/refresh use `AuthTokenValidator`. +- The validation `Result` supports `meta` data. Token validators may perform repository lookups and attach the resolved `User` to `ValidationResult->meta['user']` to avoid repeating DB queries. The facade reads that meta on success. +## Token management and cache +- `TokenManager` depends on a domain-specific `TokenCacheInterface` rather than a raw PSR cache. This keeps token-specific operations discoverable and testable. +- `TokenCache` enhances the Cache operations by providing token specific operations for storing and invalidating tokens. +- `TokenCacheInterface` defines token operations like `storeTokenInCache` and `invalidateForUser`. + +## Middleware sequence + +There are several middleware registered for this application and they are being executed one after another (order matters) before the action is executed. As a result, the application will stop executing if an error occurs, or if certain validations fail. Middleware returns early with a `Payload` error when validation fails. + +The middleware execution order is defined in the `RoutesEnum`. The available middleware is: + +- [NotFoundMiddleware.php](src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php) +- [HealthMiddleware.php](src/Domain/Infrastructure/Middleware/HealthMiddleware.php) +- [ValidateTokenClaimsMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php) +- [ValidateTokenPresenceMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php) +- [ValidateTokenRevokedMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php) +- [ValidateTokenStructureMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php) +- [ValidateTokenUserMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php) **NotFoundMiddleware** Checks if the route has been matched. If not, it will return a `Resource Not Found` payload - **HealthMiddleware** Invoked when the `/health` endpoint is called and returns a `OK` payload @@ -86,22 +213,6 @@ Checks all the claims of the JWT token to ensure that it validates. For instance Checks if the token has been revoked. If it has, an error is returned -## Responder -The responder is responsible for constructing the response with the desired output, and emitting it back to the caller. For the moment we have only implemented a JSON response with a specified array as the payload to be sent back. - -The responder receives the outcome of the Domain, by means of a `Payload` object. The object contains all the data necessary to inject in the response. - -### Response payload - -The application responds always with a specific JSON payload. The payload contains the following nodes: -- `data` - contains any data that are returned back (can be empty) -- `errors` - contains any errors occurred (can be empty) -- `meta` - array of information regarding the payload - - `code` - the application code returned - - `hash` - a `sha1` hash of the `data`, `errors` and timestamp - - `message` - `success` or `error` - - `timestamp` - the time in UTC format - - [adr_pattern]: https://github.com/pmjones/adr [pds_skeleton]: https://github.com/php-pds/skeleton +[dotenv]: https://github.com/vlucas/phpdotenv diff --git a/phpstan.neon b/phpstan.neon index 776ccd8..92dcb57 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,4 @@ parameters: level: max paths: - src + tmpDir: tests/_output/ diff --git a/public/index.php b/public/index.php index 9d75c6d..921e41d 100644 --- a/public/index.php +++ b/public/index.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider; -use Phalcon\Api\Domain\Components\Providers\RouterProvider; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Providers\ErrorHandlerProvider; +use Phalcon\Api\Domain\Infrastructure\Providers\RouterProvider; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Mvc\Micro; diff --git a/src/Action/ActionHandler.php b/src/Action/ActionHandler.php index d169ae1..ffeefe8 100644 --- a/src/Action/ActionHandler.php +++ b/src/Action/ActionHandler.php @@ -21,7 +21,7 @@ use Phalcon\Http\ResponseInterface; /** - * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TAuthLoginInput from InputTypes * @phpstan-import-type TUserInput from InputTypes */ final readonly class ActionHandler implements ActionInterface @@ -37,8 +37,8 @@ public function __construct( public function __invoke(): void { $input = new Input(); - /** @var TLoginInput|TUserInput $data */ - $data = $input->__invoke($this->request); + /** @var TAuthLoginInput|TUserInput $data */ + $data = $input->__invoke($this->request); $this->responder->__invoke( $this->response, diff --git a/src/Domain/ADR/DomainInterface.php b/src/Domain/ADR/DomainInterface.php index 52be69d..d6c1d2b 100644 --- a/src/Domain/ADR/DomainInterface.php +++ b/src/Domain/ADR/DomainInterface.php @@ -13,16 +13,14 @@ namespace Phalcon\Api\Domain\ADR; -use Phalcon\Domain\Payload; - /** - * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TAuthLoginInput from InputTypes * @phpstan-import-type TUserInput from InputTypes */ interface DomainInterface { /** - * @param TLoginInput|TUserInput $input + * @param TAuthLoginInput|TUserInput $input * * @return Payload */ diff --git a/src/Domain/ADR/InputTypes.php b/src/Domain/ADR/InputTypes.php index b68da4d..43a1471 100644 --- a/src/Domain/ADR/InputTypes.php +++ b/src/Domain/ADR/InputTypes.php @@ -14,16 +14,18 @@ namespace Phalcon\Api\Domain\ADR; /** - * @phpstan-type TLoginInput array{ + * @phpstan-type TAuthInput TAuthLoginInput|TAuthLogoutInput + * + * @phpstan-type TAuthLoginInput array{ * email?: string, * password?: string * } * - * @phpstan-type TLogoutInput array{ + * @phpstan-type TAuthLogoutInput array{ * token?: string * } * - * @phpstan-type TRefreshInput array{ + * @phpstan-type TAuthRefreshInput array{ * token?: string * } * @@ -47,46 +49,9 @@ * updatedUserId?: int, * } * - * @phpstan-type TUserSanitizedInsertInput array{ - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * createdDate: string, - * createdUserId: int, - * updatedDate: string, - * updatedUserId: int, - * } - * - * @phpstan-type TUserSanitizedUpdateInput array{ - * id: int, - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * updatedDate: string, - * updatedUserId: int, - * } - * * @phpstan-type TRequestQuery array - * - * @phpstan-type TValidationErrors array> + * @phpstan-type TValidatorErrors array{}|array> + * @phpstan-type TInputSanitize TUserInput|TAuthInput */ final class InputTypes { diff --git a/src/Domain/ADR/Payload.php b/src/Domain/ADR/Payload.php new file mode 100644 index 0000000..aad2cfb --- /dev/null +++ b/src/Domain/ADR/Payload.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\ADR; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Responder\ResponderTypes; +use Phalcon\Domain\Payload as PhalconPayload; + +use function array_key_exists; +use function var_dump; + +/** + * @phpstan-import-type TData from ResponderTypes + * @phpstan-import-type TErrors from ResponderTypes + * @phpstan-import-type TResponsePayload from ResponderTypes + * @phpstan-type TPayloadDataInput TData|TResponsePayload + * @phpstan-type TPayloadErrorInput TErrors|TResponsePayload + */ +final class Payload extends PhalconPayload +{ + /** + * @param string $status + * @param TPayloadDataInput $data + * @param TPayloadDataInput $errors + */ + private function __construct( + string $status, + array $data = [], + array $errors = [] + ) { + $result = []; + $result = $this->mergePart($result, $data, 'data'); + $result = $this->mergePart($result, $errors, 'errors'); + + parent::__construct($status, $result); + } + + /** + * @param TData $data + * + * @return self + */ + public static function created(array $data): self + { + return new self(DomainStatus::CREATED, $data); + } + + /** + * @param TData $data + * + * @return self + */ + public static function deleted(array $data): self + { + return new self(DomainStatus::DELETED, $data); + } + + /** + * @param TErrors $errors + * + * @return self + */ + public static function error(array $errors): self + { + return new self(status: DomainStatus::ERROR, errors: $errors); + } + + /** + * @param TErrors $errors + * + * @return self + */ + public static function invalid(array $errors): self + { + return new self(status: DomainStatus::INVALID, errors: $errors); + } + + /** + * @return self + */ + public static function notFound(): self + { + return new self( + status: DomainStatus::NOT_FOUND, + errors: [ + 'code' => HttpCodesEnum::NotFound->value, + 'message' => HttpCodesEnum::NotFound->text(), + 'data' => [], + 'errors' => [['Record(s) not found']], + ] + ); + } + + /** + * @param TData $data + * + * @return self + */ + public static function success(array $data): self + { + return new self(DomainStatus::SUCCESS, $data); + } + + /** + * @param TErrors $errors + * + * @return self + */ + public static function unauthorized(array $errors): self + { + return new self( + status: DomainStatus::UNAUTHORIZED, + errors:[ + 'code' => HttpCodesEnum::Unauthorized->value, + 'message' => HttpCodesEnum::Unauthorized->text(), + 'data' => [], + 'errors' => $errors, + ] + ); + } + + /** + * @param TData $data + * + * @return self + */ + public static function updated(array $data): self + { + return new self( + DomainStatus::UPDATED, + [ + 'data' => $data + ] + ); + } + + /** + * Merge a part into the result. If the part already contains the + * $key, assume it's a preformatted payload and return it. Otherwise + * attach the part under the $key. + * + * @param TPayloadDataInput $existing + * @param TPayloadDataInput $element + * @param string $key + * + * @return TPayloadDataInput + */ + private function mergePart( + array $existing, + array $element, + string $key + ): array { + if (empty($element)) { + return $existing; + } + + if (array_key_exists($key, $element)) { + return $element; + } + + /** + * preserve any existing keys in $existing and add the new named key + */ + $existing[$key] = $element; + + return $existing; + } +} diff --git a/src/Domain/Components/Cache/Cache.php b/src/Domain/Components/Cache/Cache.php deleted file mode 100644 index 1d1dcbd..0000000 --- a/src/Domain/Components/Cache/Cache.php +++ /dev/null @@ -1,134 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Domain\Components\Cache; - -use DateTimeImmutable; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Cache\Adapter\Redis; -use Phalcon\Cache\Cache as PhalconCache; -use Psr\SimpleCache\InvalidArgumentException; - -use function sha1; - -class Cache extends PhalconCache -{ - /** @var int */ - public const CACHE_LIFETIME_DAY = 86400; - /** @var int */ - public const CACHE_LIFETIME_HOUR = 3600; - /** - * Cache Timeouts - */ - /** @var int */ - public const CACHE_LIFETIME_MINUTE = 60; - /** @var int */ - public const CACHE_LIFETIME_MONTH = 2592000; - /** - * Default token expiry - 4 hours - */ - /** @var int */ - public const CACHE_TOKEN_EXPIRY = 14400; - /** - * Cache masks - */ - /** @var string */ - private const MASK_TOKEN_USER = 'tk-%s-%s'; - - /** - * @param UserTransport $domainUser - * @param string $token - * - * @return string - */ - public function getCacheTokenKey(UserTransport $domainUser, string $token): string - { - $tokenString = ''; - if (true !== empty($token)) { - $tokenString = sha1($token); - } - - return sprintf( - self::MASK_TOKEN_USER, - $domainUser->getId(), - $tokenString - ); - } - - /** - * @param EnvManager $env - * @param UserTransport $domainUser - * - * @return bool - * @throws InvalidArgumentException - */ - public function invalidateForUser( - EnvManager $env, - UserTransport $domainUser - ): bool { - /** - * We could store the tokens in the database but this way is faster - * and Redis also has a TTL which auto expires elements. - * - * To get all the keys for a user, we use the underlying adapter - * of the cache which is Redis and call the `getKeys()` on it. The - * keys will come back with the prefix defined in the adapter. In order - * to delete them, we need to remove the prefix because `delete()` will - * automatically prepend each key with it. - */ - /** @var Redis $redis */ - $redis = $this->getAdapter(); - $pattern = $this->getCacheTokenKey($domainUser, ''); - $keys = $redis->getKeys($pattern); - /** @var string $prefix */ - $prefix = $env->get('CACHE_PREFIX', '-rest-', 'string'); - $newKeys = []; - /** @var string $key */ - foreach ($keys as $key) { - $newKeys[] = str_replace($prefix, '', $key); - } - - return $this->deleteMultiple($newKeys); - } - - /** - * @param EnvManager $env - * @param UserTransport $domainUser - * @param string $token - * - * @return bool - * @throws InvalidArgumentException - */ - public function storeTokenInCache( - EnvManager $env, - UserTransport $domainUser, - string $token - ): bool { - $cacheKey = $this->getCacheTokenKey($domainUser, $token); - /** @var int $expiration */ - $expiration = $env->get('TOKEN_EXPIRATION', self::CACHE_TOKEN_EXPIRY, 'int'); - $expirationDate = (new DateTimeImmutable()) - ->modify('+' . $expiration . ' seconds') - ->format(Dates::DATE_TIME_FORMAT) - ; - - $payload = [ - 'token' => $token, - 'expiry' => $expirationDate, - ]; - - return $this->set($cacheKey, $payload, $expiration); - } -} diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php deleted file mode 100644 index a8e97c5..0000000 --- a/src/Domain/Components/Container.php +++ /dev/null @@ -1,419 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Domain\Components; - -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; -use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenClaimsMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenPresenceMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenRevokedMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenStructureMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenUserMiddleware; -use Phalcon\Api\Domain\Services\Auth\LoginPostService; -use Phalcon\Api\Domain\Services\Auth\LogoutPostService; -use Phalcon\Api\Domain\Services\Auth\RefreshPostService; -use Phalcon\Api\Domain\Services\User\UserDeleteService; -use Phalcon\Api\Domain\Services\User\UserGetService; -use Phalcon\Api\Domain\Services\User\UserPostService; -use Phalcon\Api\Domain\Services\User\UserPutService; -use Phalcon\Api\Responder\JsonResponder; -use Phalcon\Cache\AdapterFactory; -use Phalcon\DataMapper\Pdo\Connection; -use Phalcon\Di\Di; -use Phalcon\Di\Service; -use Phalcon\Events\Manager as EventsManager; -use Phalcon\Filter\FilterFactory; -use Phalcon\Http\Request; -use Phalcon\Http\Response; -use Phalcon\Logger\Adapter\Stream; -use Phalcon\Logger\Logger; -use Phalcon\Mvc\Router; -use Phalcon\Storage\SerializerFactory; - -class Container extends Di -{ - /** @var string */ - public const APPLICATION = 'application'; - /** @var string */ - public const CACHE = 'cache'; - /** @var string */ - public const CONNECTION = 'connection'; - /** @var string */ - public const ENV = 'env'; - /** @var string */ - public const EVENTS_MANAGER = 'eventsManager'; - /** @var string */ - public const FILTER = 'filter'; - /** @var string */ - public const JWT_TOKEN = 'jwt.token'; - /** @var string */ - public const LOGGER = 'logger'; - /** @var string */ - public const REQUEST = 'request'; - /** @var string */ - public const RESPONSE = 'response'; - /** @var string */ - public const ROUTER = 'router'; - /** @var string */ - public const SECURITY = Security::class; - /** @var string */ - public const TIME = 'time'; - /** - * Services - */ - public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; - public const AUTH_LOGOUT_POST_SERVICE = 'service.auth.logout.post'; - public const AUTH_REFRESH_POST_SERVICE = 'service.auth.refresh.post'; - public const USER_DELETE_SERVICE = 'service.user.delete'; - public const USER_GET_SERVICE = 'service.user.get'; - public const USER_POST_SERVICE = 'service.user.post'; - public const USER_PUT_SERVICE = 'service.user.put'; - /** - * Middleware - */ - public const MIDDLEWARE_HEALTH = HealthMiddleware::class; - public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_CLAIMS = ValidateTokenClaimsMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_PRESENCE = ValidateTokenPresenceMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; - /** - * Repositories - */ - public const REPOSITORY = 'repository'; - public const REPOSITORY_TRANSPORT = TransportRepository::class; - /** - * Responders - */ - public const RESPONDER_JSON = JsonResponder::class; - - public function __construct() - { - $this->services = [ - self::CACHE => $this->getServiceCache(), - self::CONNECTION => $this->getServiceConnection(), - self::ENV => $this->getServiceEnv(), - self::EVENTS_MANAGER => $this->getServiceEventsManger(), - self::FILTER => $this->getServiceFilter(), - self::JWT_TOKEN => $this->getServiceJWTToken(), - self::LOGGER => $this->getServiceLogger(), - self::REQUEST => new Service(Request::class, true), - self::RESPONSE => new Service(Response::class, true), - self::ROUTER => $this->getServiceRouter(), - - self::REPOSITORY => $this->getServiceRepository(), - - self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthPost(LoginPostService::class), - self::AUTH_LOGOUT_POST_SERVICE => $this->getServiceAuthPost(LogoutPostService::class), - self::AUTH_REFRESH_POST_SERVICE => $this->getServiceAuthPost(RefreshPostService::class), - self::USER_DELETE_SERVICE => $this->getServiceUser(UserDeleteService::class), - self::USER_GET_SERVICE => $this->getServiceUser(UserGetService::class), - self::USER_POST_SERVICE => $this->getServiceUser(UserPostService::class), - self::USER_PUT_SERVICE => $this->getServiceUser(UserPutService::class), - ]; - - parent::__construct(); - } - - /** - * @param class-string $className - * - * @return Service - */ - private function getServiceAuthPost(string $className): Service - { - return new Service( - [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::REPOSITORY_TRANSPORT, - ], - [ - 'type' => 'service', - 'name' => self::CACHE, - ], - [ - 'type' => 'service', - 'name' => self::ENV, - ], - [ - 'type' => 'service', - 'name' => self::JWT_TOKEN, - ], - [ - 'type' => 'service', - 'name' => self::FILTER, - ], - [ - 'type' => 'service', - 'name' => self::SECURITY, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceCache(): Service - { - return new Service( - function () { - /** @var EnvManager $env */ - $env = $this->getShared(self::ENV); - - /** @var string $prefix */ - $prefix = $env->get('CACHE_PREFIX', '-rest-'); - /** @var string $host */ - $host = $env->get('CACHE_HOST', 'localhost'); - /** @var int $lifetime */ - $lifetime = $env->get('CACHE_LIFETIME', Cache::CACHE_LIFETIME_DAY, 'int'); - /** @var int $index */ - $index = $env->get('CACHE_INDEX', 0, 'int'); - /** @var int $port */ - $port = $env->get('CACHE_PORT', 6379, 'int'); - - $options = [ - 'host' => $host, - 'index' => $index, - 'lifetime' => $lifetime, - 'prefix' => $prefix, - 'port' => $port, - 'uniqueId' => $prefix, - ]; - - /** @var string $adapter */ - $adapter = $env->get('CACHE_ADAPTER', 'redis'); - - $serializerFactory = new SerializerFactory(); - $adapterFactory = new AdapterFactory($serializerFactory); - $cacheAdapter = $adapterFactory->newInstance($adapter, $options); - - return new Cache($cacheAdapter); - }, - true - ); - } - - /** - * @return Service - */ - private function getServiceConnection(): Service - { - return new Service( - function () { - /** @var EnvManager $env */ - $env = $this->getShared(self::ENV); - - /** @var string $dbName */ - $dbName = $env->get('DB_NAME', 'phalcon'); - /** @var string $host */ - $host = $env->get('DB_HOST', 'rest-db'); - /** @var string $password */ - $password = $env->get('DB_PASS', 'secret'); - /** @var int $port */ - $port = $env->get('DB_PORT', 3306, 'int'); - /** @var string $username */ - $username = $env->get('DB_USER', 'root'); - /** @var string $encoding */ - $encoding = $env->get('DB_CHARSET', 'utf8'); - $queries = ['SET NAMES utf8mb4']; - $dsn = sprintf( - 'mysql:host=%s;port=%s;dbname=%s;charset=%s', - $host, - $port, - $dbName, - $encoding - ); - - return new Connection( - $dsn, - $username, - $password, - [], - $queries - ); - }, - true - ); - } - - /** - * @return Service - */ - private function getServiceEnv(): Service - { - return new Service( - function () { - return new EnvManager(); - }, - true - ); - } - - /** - * @return Service - */ - private function getServiceEventsManger(): Service - { - return new Service( - function () { - $evm = new EventsManager(); - $evm->enablePriorities(true); - - return $evm; - }, - true - ); - } - - /** - * @return Service - */ - private function getServiceFilter(): Service - { - return new Service( - function () { - return (new FilterFactory())->newInstance(); - }, - true - ); - } - - /** - * @return Service - */ - private function getServiceJWTToken(): Service - { - return new Service( - [ - 'className' => JWTToken::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::ENV, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceLogger(): Service - { - return new Service( - function () { - /** @var EnvManager $env */ - $env = $this->getShared(self::ENV); - - /** @var string $logName */ - $logName = $env->get('LOG_FILENAME', 'rest-api'); - /** @var string $logPath */ - $logPath = $env->get('LOG_PATH', 'storage/logs/'); - $logFile = $env->appPath($logPath) . '/' . $logName . '.log'; - - return new Logger( - $logName, - [ - 'main' => new Stream($logFile), - ] - ); - } - ); - } - - /** - * @return Service - */ - private function getServiceRepository(): Service - { - return new Service( - [ - 'className' => QueryRepository::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::CONNECTION, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceRouter(): Service - { - return new Service( - [ - 'className' => Router::class, - 'arguments' => [ - [ - 'type' => 'parameter', - 'value' => false, - ], - ], - ] - ); - } - - /** - * @param class-string $className - * - * @return Service - */ - private function getServiceUser(string $className): Service - { - return new Service( - [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::REPOSITORY_TRANSPORT, - ], - [ - 'type' => 'service', - 'name' => self::FILTER, - ], - [ - 'type' => 'service', - 'name' => self::SECURITY, - ], - ], - ] - ); - } -} diff --git a/src/Domain/Components/DataSource/QueryRepository.php b/src/Domain/Components/DataSource/QueryRepository.php deleted file mode 100644 index 5e5f58b..0000000 --- a/src/Domain/Components/DataSource/QueryRepository.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Domain\Components\DataSource; - -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\DataMapper\Pdo\Connection; - -class QueryRepository -{ - private ?UserRepository $user = null; - - /** - * @param Connection $connection - */ - public function __construct( - private readonly Connection $connection, - ) { - } - - /** - * @return UserRepository - */ - public function user(): UserRepository - { - if (null === $this->user) { - $this->user = new UserRepository($this->connection); - } - - return $this->user; - } -} diff --git a/src/Domain/Components/DataSource/TransportRepository.php b/src/Domain/Components/DataSource/TransportRepository.php deleted file mode 100644 index c7edf02..0000000 --- a/src/Domain/Components/DataSource/TransportRepository.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Domain\Components\DataSource; - -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Encryption\Security\JWT\Token\Token; - -/** - * @phpstan-import-type TLoginResponsePayload from UserTypes - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TUserRecord from UserTypes - * - * Note: The 'readonly' keyword was intentionally removed from this class. - * Properties $sessionToken and $sessionUser are mutable to support session - * management, allowing updates to the current session state. This change - * removes immutability guarantees, but is necessary for the intended use. - */ -final class TransportRepository -{ - private ?Token $sessionToken = null; - - private ?UserTransport $sessionUser = null; - - /** - * Returns the session token. - * - * @return Token|null - */ - public function getSessionToken(): ?Token - { - return $this->sessionToken; - } - - /** - * Returns the session user. - * - * @return UserTransport|null - */ - public function getSessionUser(): ?UserTransport - { - return $this->sessionUser; - } - - /** - * @param UserTransport $user - * @param string $token - * @param string $refreshToken - * - * @return TLoginResponsePayload - */ - public function newLoginUser( - UserTransport $user, - string $token, - string $refreshToken - ): array { - return [ - 'authenticated' => true, - 'user' => [ - 'id' => $user->getId(), - 'name' => $user->getFullName(), - 'email' => $user->getEmail(), - ], - 'jwt' => [ - 'token' => $token, - 'refreshToken' => $refreshToken, - ], - ]; - } - - /** - * @param TUserRecord $dbUser - * - * @return UserTransport - */ - public function newUser(array $dbUser): UserTransport - { - return new UserTransport($dbUser); - } - - /** - * Sets the session Token - * - * @param Token $token - * - * @return void - */ - public function setSessionToken(Token $token): void - { - $this->sessionToken = $token; - } - - /** - * Populates the session user with the user data. - * - * @param TUserDbRecord $user - * - * @return void - */ - public function setSessionUser(array $user): void - { - $this->sessionUser = $this->newUser($user); - } -} diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php deleted file mode 100644 index 36d903a..0000000 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ /dev/null @@ -1,149 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Domain\Components\DataSource\User; - -use Phalcon\Api\Domain\Components\DataSource\AbstractRepository; -use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; -use Phalcon\DataMapper\Query\Insert; -use Phalcon\DataMapper\Query\Update; - -/** - * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserInsert from UserTypes - * @phpstan-import-type TUserUpdate from UserTypes - * - * The 'final' keyword was intentionally removed from this class to allow - * extension for testing purposes (e.g., mocking in unit tests). - * - * Please avoid extending this class in production code unless absolutely necessary. - */ -class UserRepository extends AbstractRepository -{ - /** - * @var string - */ - protected string $idField = 'usr_id'; - /** - * @var string - */ - protected string $table = 'co_users'; - - /** - * @param string $email - * - * @return TUserRecord - */ - public function findByEmail(string $email): array - { - $result = []; - if (true !== empty($email)) { - return $this->findOneBy( - [ - 'usr_email' => $email, - 'usr_status_flag' => FlagsEnum::Active->value, - ] - ); - } - - return $result; - } - - /** - * @param TUserInsert $userData - * - * @return int - */ - public function insert(array $userData): int - { - $createdUserId = $userData['createdUserId']; - $updatedUserId = $userData['updatedUserId']; - - $columns = [ - 'usr_status_flag' => $userData['status'], - 'usr_email' => $userData['email'], - 'usr_password' => $userData['password'], - 'usr_name_prefix' => $userData['namePrefix'], - 'usr_name_first' => $userData['nameFirst'], - 'usr_name_middle' => $userData['nameMiddle'], - 'usr_name_last' => $userData['nameLast'], - 'usr_name_suffix' => $userData['nameSuffix'], - 'usr_issuer' => $userData['issuer'], - 'usr_token_password' => $userData['tokenPassword'], - 'usr_token_id' => $userData['tokenId'], - 'usr_preferences' => $userData['preferences'], - 'usr_created_date' => $userData['createdDate'], - 'usr_updated_date' => $userData['updatedDate'], - ]; - - $insert = Insert::new($this->connection); - - $insert - ->into($this->table) - ->columns($columns) - ; - - if ($createdUserId > 0) { - $insert->column('usr_created_usr_id', $createdUserId); - } - if ($updatedUserId > 0) { - $insert->column('usr_updated_usr_id', $updatedUserId); - } - - $insert->perform(); - - return (int)$insert->getLastInsertId(); - } - - /** - * @param TUserUpdate $userData - * - * @return int - */ - public function update(array $userData): int - { - $userId = $userData['id']; - $updatedUserId = $userData['updatedUserId']; - - $columns = [ - 'usr_status_flag' => $userData['status'], - 'usr_email' => $userData['email'], - 'usr_password' => $userData['password'], - 'usr_name_prefix' => $userData['namePrefix'], - 'usr_name_first' => $userData['nameFirst'], - 'usr_name_middle' => $userData['nameMiddle'], - 'usr_name_last' => $userData['nameLast'], - 'usr_name_suffix' => $userData['nameSuffix'], - 'usr_issuer' => $userData['issuer'], - 'usr_token_password' => $userData['tokenPassword'], - 'usr_token_id' => $userData['tokenId'], - 'usr_preferences' => $userData['preferences'], - 'usr_updated_date' => $userData['updatedDate'], - ]; - - $update = Update::new($this->connection); - $update - ->table($this->table) - ->columns($columns) - ->where('usr_id = ', $userId) - ; - - if ($updatedUserId > 0) { - $update->column('usr_updated_usr_id', $updatedUserId); - } - - $update->perform(); - - return (int)$userId; - } -} diff --git a/src/Domain/Components/DataSource/User/UserTransport.php b/src/Domain/Components/DataSource/User/UserTransport.php deleted file mode 100644 index 98490cf..0000000 --- a/src/Domain/Components/DataSource/User/UserTransport.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Domain\Components\DataSource\User; - -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; - -/** - * The domain representation of a user. - * - * @method int getId() - * @method int getTenantId() - * @method string getStatus() - * @method string getEmail() - * @method string getPassword() - * @method string getNamePrefix() - * @method string getNameFirst() - * @method string getNameMiddle() - * @method string getNameLast() - * @method string getNameSuffix() - * @method string getIssuer() - * @method string getTokenPassword() - * @method string getTokenId() - * @method string getPreferences() - * @method string getCreatedDate() - * @method int|null getCreatedUserId() - * @method string getUpdatedDate() - * @method int|null getUpdatedUserId() - * - * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserTransport from UserTypes - */ -final class UserTransport -{ - /** @var TUserTransport */ - private array $store; - - /** - * @param TUserRecord $input - */ - public function __construct(array $input) - { - $this->store = [ - 'id' => (int)($input['usr_id'] ?? 0), - 'status' => (int)($input['usr_status_flag'] ?? 0), - 'email' => (string)($input['usr_email'] ?? ''), - 'password' => (string)($input['usr_password'] ?? ''), - 'namePrefix' => (string)($input['usr_name_prefix'] ?? ''), - 'nameFirst' => (string)($input['usr_name_first'] ?? ''), - 'nameMiddle' => (string)($input['usr_name_middle'] ?? ''), - 'nameLast' => (string)($input['usr_name_last'] ?? ''), - 'nameSuffix' => (string)($input['usr_name_suffix'] ?? ''), - 'issuer' => (string)($input['usr_issuer'] ?? ''), - 'tokenPassword' => (string)($input['usr_token_password'] ?? ''), - 'tokenId' => (string)($input['usr_token_id'] ?? ''), - 'preferences' => (string)($input['usr_preferences'] ?? ''), - 'createdDate' => (string)($input['usr_created_date'] ?? ''), - 'createdUserId' => (int)($input['usr_created_usr_id'] ?? 0), - 'updatedDate' => (string)($input['usr_updated_date'] ?? ''), - 'updatedUserId' => (int)($input['usr_updated_usr_id'] ?? 0), - ]; - - $this->store['fullName'] = $this->getFullName(); - } - - /** - * @param string $name - * @param array $arguments - * - * @return int|string - */ - public function __call(string $name, array $arguments): int | string - { - return match ($name) { - 'getId' => $this->store['id'], - 'getStatus' => $this->store['status'], - 'getEmail' => $this->store['email'], - 'getPassword' => $this->store['password'], - 'getNamePrefix' => $this->store['namePrefix'], - 'getNameFirst' => $this->store['nameFirst'], - 'getNameMiddle' => $this->store['nameMiddle'], - 'getNameLast' => $this->store['nameLast'], - 'getNameSuffix' => $this->store['nameSuffix'], - 'getIssuer' => $this->store['issuer'], - 'getTokenPassword' => $this->store['tokenPassword'], - 'getTokenId' => $this->store['tokenId'], - 'getPreferences' => $this->store['preferences'], - 'getCreatedDate' => $this->store['createdDate'], - 'getCreatedUserId' => $this->store['createdUserId'], - 'getUpdatedDate' => $this->store['updatedDate'], - 'getUpdatedUserId' => $this->store['updatedUserId'], - default => throw InvalidConfigurationArgumentException::new( - 'The ' . $name . ' method is not supported. [' - . json_encode($arguments) . ']', - ), - }; - } - - /** - * @return string - */ - public function getFullName(): string - { - return trim( - $this->getNameLast() - . ', ' - . $this->getNameFirst() - . ' ' - . $this->getNameMiddle() - ); - } - - public function isEmpty(): bool - { - return 0 === $this->store['id']; - } - - /** - * @return array - */ - public function toArray(): array - { - return [$this->store['id'] => $this->store]; - } -} diff --git a/src/Domain/Components/DataSource/User/UserTypes.php b/src/Domain/Components/DataSource/User/UserTypes.php deleted file mode 100644 index d42f2e4..0000000 --- a/src/Domain/Components/DataSource/User/UserTypes.php +++ /dev/null @@ -1,118 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Domain\Components\DataSource\User; - -/** - * @phpstan-type TLoginResponsePayload array{ - * authenticated: true, - * user: array{ - * id: int, - * name: string, - * email: string - * }, - * jwt: array{ - * token: string, - * refreshToken: string, - * } - * } - * - * @phpstan-type TUserDbRecord array{ - * usr_id: int, - * usr_status_flag: int, - * usr_email: string, - * usr_password: string, - * usr_name_prefix: string, - * usr_name_first: string, - * usr_name_middle: string, - * usr_name_last: string, - * usr_name_suffix: string, - * usr_issuer: string, - * usr_token_password: string, - * usr_token_id: string, - * usr_preferences: string, - * usr_created_date: string, - * usr_created_usr_id: int, - * usr_updated_date: string, - * usr_updated_usr_id: int, - * } - * - * @phpstan-type TUserTokenDbRecord array{ - * usr_id: int, - * usr_issuer: string, - * usr_token_password: string, - * usr_token_id: string - * } - * - * @phpstan-type TUserRecord array{}|TUserDbRecord - * - * @phpstan-type TUserTransport array{ - * id: int, - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * createdDate: string, - * createdUserId: int, - * updatedDate: string, - * updatedUserId: int, - * } - * - * @phpstan-type TUserInsert array{ - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * createdDate: string, - * createdUserId: int, - * updatedDate: string, - * updatedUserId: int, - * } - * - * @phpstan-type TUserUpdate array{ - * id: int, - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * updatedDate: string, - * updatedUserId: int, - * } - */ -final class UserTypes -{ -} diff --git a/src/Domain/Infrastructure/Constants/Cache.php b/src/Domain/Infrastructure/Constants/Cache.php new file mode 100644 index 0000000..73f691c --- /dev/null +++ b/src/Domain/Infrastructure/Constants/Cache.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Constants; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; + +use function sha1; + +class Cache +{ + /** @var int */ + public const CACHE_LIFETIME_DAY = 86400; + /** @var int */ + public const CACHE_LIFETIME_HOUR = 3600; + /** + * Cache Timeouts + */ + /** @var int */ + public const CACHE_LIFETIME_MINUTE = 60; + /** @var int */ + public const CACHE_LIFETIME_MONTH = 2592000; + /** + * Default token expiry - 4 hours + */ + /** @var int */ + public const CACHE_TOKEN_EXPIRY = 14400; + /** + * Cache masks + */ + /** @var string */ + private const MASK_TOKEN_USER = 'tk-%s-%s'; + + /** + * @param User $domainUser + * @param string $token + * + * @return string + */ + public static function getCacheTokenKey(User $domainUser, string $token): string + { + $tokenString = ''; + if (true !== empty($token)) { + $tokenString = sha1($token); + } + + return sprintf( + self::MASK_TOKEN_USER, + $domainUser->id, + $tokenString + ); + } +} diff --git a/src/Domain/Components/Constants/Dates.php b/src/Domain/Infrastructure/Constants/Dates.php similarity index 94% rename from src/Domain/Components/Constants/Dates.php rename to src/Domain/Infrastructure/Constants/Dates.php index c3f1f71..b80248a 100644 --- a/src/Domain/Components/Constants/Dates.php +++ b/src/Domain/Infrastructure/Constants/Dates.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Constants; +namespace Phalcon\Api\Domain\Infrastructure\Constants; use DateTimeImmutable; use DateTimeZone; diff --git a/src/Domain/Infrastructure/Container.php b/src/Domain/Infrastructure/Container.php new file mode 100644 index 0000000..6c290d7 --- /dev/null +++ b/src/Domain/Infrastructure/Container.php @@ -0,0 +1,341 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure; + +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\AuthDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\CommonDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\UserDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenClaimsMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenPresenceMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenRevokedMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenStructureMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenUserMiddleware; +use Phalcon\Api\Responder\JsonResponder; +use Phalcon\Cache\AdapterFactory; +use Phalcon\Cache\Cache; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\Di\Di; +use Phalcon\Di\Service; +use Phalcon\Filter\FilterFactory; +use Phalcon\Filter\Validation; +use Phalcon\Http\Request; +use Phalcon\Http\Response; +use Phalcon\Logger\Adapter\Stream; +use Phalcon\Logger\Logger; +use Phalcon\Storage\SerializerFactory; +use Phalcon\Support\Registry; + +use function sprintf; + +/** + * + * @phpstan-type TServiceParameter array{ + * type: 'parameter', + * value: mixed + * } + * @phpstan-type TServiceService array{ + * type: 'service', + * name: string + * } + * @phpstan-type TServiceArguments array + * @phpstan-type TServiceCall array{ + * method: string, + * arguments: TServiceArguments + * } + * + * @phpstan-type TService array{ + * className: string, + * arguments?: TServiceArguments, + * calls?: array + * } + */ +class Container extends Di +{ + /** @var string */ + public const APPLICATION = 'application'; + /** @var string */ + public const CACHE = 'cache'; + /** @var string */ + public const CONNECTION = 'connection'; + /** @var string */ + public const ENV = 'env'; + /** @var string */ + public const EVENTS_MANAGER = 'eventsManager'; + /** @var string */ + public const FILTER = 'filter'; + /** @var string */ + public const JWT_TOKEN = 'jwt.token'; + /** @var string */ + public const JWT_TOKEN_CACHE = 'jwt.token.cache'; + /** @var string */ + public const JWT_TOKEN_MANAGER = 'jwt.token.manager'; + /** @var string */ + public const LOGGER = 'logger'; + /** @var string */ + public const REGISTRY = 'registry'; + /** @var string */ + public const REQUEST = 'request'; + /** @var string */ + public const RESPONSE = 'response'; + /** @var string */ + public const ROUTER = 'router'; + /** @var string */ + public const SECURITY = Security::class; + /** @var string */ + public const TIME = 'time'; + /** @var string */ + public const VALIDATION = Validation::class; + /** + * Middleware + */ + public const MIDDLEWARE_HEALTH = HealthMiddleware::class; + public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_CLAIMS = ValidateTokenClaimsMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_PRESENCE = ValidateTokenPresenceMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; + /** + * Facades + */ + public const AUTH_FACADE = 'auth.facade'; + public const USER_FACADE = 'user.facade'; + public const USER_FACADE_UPDATE = 'user.facade.update'; + /** + * Services + */ + public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; + public const AUTH_LOGOUT_POST_SERVICE = 'service.auth.logout.post'; + public const AUTH_REFRESH_POST_SERVICE = 'service.auth.refresh.post'; + public const USER_DELETE_SERVICE = 'service.user.delete'; + public const USER_GET_SERVICE = 'service.user.get'; + public const USER_POST_SERVICE = 'service.user.post'; + public const USER_PUT_SERVICE = 'service.user.put'; + /** + * Mappers + */ + public const USER_MAPPER = UserMapper::class; + /** + * Responders + */ + public const RESPONDER_JSON = JsonResponder::class; + /** + * Repositories + */ + public const USER_REPOSITORY = 'user.repository'; + /** + * Sanitizers + */ + public const AUTH_SANITIZER = 'auth.sanitizer'; + public const USER_SANITIZER = 'user.sanitizer'; + /** + * Validators + */ + public const AUTH_LOGIN_VALIDATOR = 'auth.validator.login'; + public const AUTH_TOKEN_VALIDATOR = 'auth.validator.token'; + public const USER_VALIDATOR = 'user.validator.insert'; + public const USER_VALIDATOR_UPDATE = 'user.validator.update'; + + public function __construct() + { + $this->services = [ + /** + * Base services + */ + self::CACHE => $this->getServiceCache(), + self::CONNECTION => $this->getServiceConnection(), + self::ENV => new Service(EnvManager::class, true), + self::EVENTS_MANAGER => new Service(CommonDefinitionsEnum::EventsManager->definition(), true), + self::FILTER => $this->getServiceFilter(), + self::JWT_TOKEN => new Service(CommonDefinitionsEnum::JWTToken->definition(), true), + self::JWT_TOKEN_CACHE => new Service(CommonDefinitionsEnum::JWTTokenCache->definition(), true), + self::JWT_TOKEN_MANAGER => new Service(CommonDefinitionsEnum::JWTTokenManager->definition(), true), + self::LOGGER => $this->getServiceLogger(), + self::REGISTRY => new Service(Registry::class, true), + self::REQUEST => new Service(Request::class, true), + self::RESPONSE => new Service(Response::class, true), + self::ROUTER => new Service(CommonDefinitionsEnum::Router->definition(), true), + + /** + * Facades + */ + self::AUTH_FACADE => new Service(AuthDefinitionsEnum::AuthFacade->definition()), + self::USER_FACADE => new Service(UserDefinitionsEnum::UserFacade->definition()), + self::USER_FACADE_UPDATE => new Service(UserDefinitionsEnum::UserFacadeUpdate->definition()), + + /** + * Repositories + */ + self::USER_REPOSITORY => new Service(UserDefinitionsEnum::UserRepository->definition()), + + /** + * Sanitizers + */ + self::AUTH_SANITIZER => new Service(AuthDefinitionsEnum::AuthSanitizer->definition()), + self::USER_SANITIZER => new Service(UserDefinitionsEnum::UserSanitizer->definition()), + + /** + * Validators + */ + self::AUTH_LOGIN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthLoginValidator->definition()), + self::AUTH_TOKEN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthTokenValidator->definition()), + self::USER_VALIDATOR => new Service(UserDefinitionsEnum::UserValidator->definition()), + self::USER_VALIDATOR_UPDATE => new Service(UserDefinitionsEnum::UserValidatorUpdate->definition()), + + /** + * Services + */ + self::AUTH_LOGIN_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLoginPost->definition()), + self::AUTH_LOGOUT_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLogoutPost->definition()), + self::AUTH_REFRESH_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthRefreshPost->definition()), + self::USER_DELETE_SERVICE => new Service(UserDefinitionsEnum::UserDelete->definition()), + self::USER_GET_SERVICE => new Service(UserDefinitionsEnum::UserGet->definition()), + self::USER_POST_SERVICE => new Service(UserDefinitionsEnum::UserPost->definition()), + self::USER_PUT_SERVICE => new Service(UserDefinitionsEnum::UserPut->definition()), + ]; + + parent::__construct(); + } + + /** + * @return Service + */ + private function getServiceCache(): Service + { + return new Service( + function () { + /** @var EnvManager $env */ + $env = $this->getShared(self::ENV); + + /** @var string $prefix */ + $prefix = $env->get('CACHE_PREFIX', '-rest-'); + /** @var string $host */ + $host = $env->get('CACHE_HOST', 'localhost'); + /** @var int $lifetime */ + $lifetime = $env->get('CACHE_LIFETIME', CacheConstants::CACHE_LIFETIME_DAY, 'int'); + /** @var int $index */ + $index = $env->get('CACHE_INDEX', 0, 'int'); + /** @var int $port */ + $port = $env->get('CACHE_PORT', 6379, 'int'); + + $options = [ + 'host' => $host, + 'index' => $index, + 'lifetime' => $lifetime, + 'prefix' => $prefix, + 'port' => $port, + 'uniqueId' => $prefix, + ]; + + /** @var string $adapter */ + $adapter = $env->get('CACHE_ADAPTER', 'redis'); + + $serializerFactory = new SerializerFactory(); + $adapterFactory = new AdapterFactory($serializerFactory); + $cacheAdapter = $adapterFactory->newInstance($adapter, $options); + + return new Cache($cacheAdapter); + }, + true + ); + } + + /** + * @return Service + */ + private function getServiceConnection(): Service + { + return new Service( + function () { + /** @var EnvManager $env */ + $env = $this->getShared(self::ENV); + + /** @var string $dbName */ + $dbName = $env->get('DB_NAME', 'phalcon'); + /** @var string $host */ + $host = $env->get('DB_HOST', 'rest-db'); + /** @var string $password */ + $password = $env->get('DB_PASS', 'secret'); + /** @var int $port */ + $port = $env->get('DB_PORT', 3306, 'int'); + /** @var string $username */ + $username = $env->get('DB_USER', 'root'); + /** @var string $encoding */ + $encoding = $env->get('DB_CHARSET', 'utf8'); + $queries = ['SET NAMES utf8mb4']; + $dsn = sprintf( + 'mysql:host=%s;port=%s;dbname=%s;charset=%s', + $host, + $port, + $dbName, + $encoding + ); + + return new Connection( + $dsn, + $username, + $password, + [], + $queries + ); + }, + true + ); + } + + /** + * @return Service + */ + private function getServiceFilter(): Service + { + return new Service( + function () { + return (new FilterFactory())->newInstance(); + }, + true + ); + } + + /** + * @return Service + */ + private function getServiceLogger(): Service + { + return new Service( + function () { + /** @var EnvManager $env */ + $env = $this->getShared(self::ENV); + + /** @var string $logName */ + $logName = $env->get('LOG_FILENAME', 'rest-api'); + /** @var string $logPath */ + $logPath = $env->get('LOG_PATH', 'storage/logs/'); + $logFile = $env->appPath($logPath) . '/' . $logName . '.log'; + + return new Logger( + $logName, + [ + 'main' => new Stream($logFile), + ] + ); + } + ); + } +} diff --git a/src/Domain/Components/DataSource/AbstractRepository.php b/src/Domain/Infrastructure/DataSource/AbstractRepository.php similarity index 53% rename from src/Domain/Components/DataSource/AbstractRepository.php rename to src/Domain/Infrastructure/DataSource/AbstractRepository.php index 34c6314..4ac90dc 100644 --- a/src/Domain/Components/DataSource/AbstractRepository.php +++ b/src/Domain/Infrastructure/DataSource/AbstractRepository.php @@ -11,15 +11,14 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource; +namespace Phalcon\Api\Domain\Infrastructure\DataSource; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\DataMapper\Query\Delete; -use Phalcon\DataMapper\Query\Select; /** - * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TCriteria from UserTypes */ abstract class AbstractRepository { @@ -38,7 +37,7 @@ public function __construct( } /** - * @param array $criteria + * @param TCriteria $criteria * * @return int */ @@ -68,43 +67,4 @@ public function deleteById(int $recordId): int ] ); } - - /** - * @param int $recordId - * - * @return TUserRecord - */ - public function findById(int $recordId): array - { - $result = []; - if ($recordId > 0) { - return $this->findOneBy( - [ - $this->idField => $recordId, - ] - ); - } - - return $result; - } - - - /** - * @param array $criteria - * - * @return TUserRecord - */ - public function findOneBy(array $criteria): array - { - $select = Select::new($this->connection); - - /** @var TUserRecord $result */ - $result = $select - ->from($this->table) - ->whereEquals($criteria) - ->fetchOne() - ; - - return $result; - } } diff --git a/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php new file mode 100644 index 0000000..bd61f7d --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\SanitizersEnumInterface; +use Phalcon\Filter\FilterInterface; + +/** + * @phpstan-import-type TInputSanitize from InputTypes + */ +abstract class AbstractSanitizer implements SanitizerInterface +{ + protected string $enum = ''; + + public function __construct( + private readonly FilterInterface $filter, + ) { + } + + /** + * @param TInputSanitize $input + * + * @return TInputSanitize + */ + public function sanitize(array $input): array + { + $enum = $this->enum; + /** @var SanitizersEnumInterface[] $fields */ + $fields = $enum::cases(); + + /** + * Set defaults + */ + $sanitized = []; + + /** + * Sanitize all fields. The fields can be `null` meaning they + * were not defined in the input array. If the value exists + * then it will be sanitized. + * + * If there is no sanitizer defined, the value will be left intact. + */ + /** @var SanitizersEnumInterface $field */ + foreach ($fields as $field) { + $value = $input[$field->name] ?? $field->default(); + + if (null !== $value) { + $sanitizer = $field->sanitizer(); + if (true !== empty($sanitizer)) { + $value = $this->filter->sanitize($value, $sanitizer); + } + } + + $sanitized[$field->name] = $value; + } + + /** + * Return sanitized array + */ + /** @var TInputSanitize $sanitized */ + return $sanitized; + } +} diff --git a/src/Domain/Infrastructure/DataSource/AbstractValueObject.php b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php new file mode 100644 index 0000000..b3be385 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; + +/** + * Base factory for value objects such as input DTOs. + * + * Concrete DTOs must implement protected static function + * + * `fromArray(array $sanitized): static` + * + * and keep themselves immutable/read\-only. + * + * @phpstan-import-type TInputSanitize from InputTypes + */ +abstract class AbstractValueObject +{ + /** + * Factory that accepts a SanitizerInterface and returns the concrete DTO. + * + * @param SanitizerInterface $sanitizer + * @param TInputSanitize $input + * + * @return static + */ + public static function new(SanitizerInterface $sanitizer, array $input): static + { + $sanitized = $sanitizer->sanitize($input); + + return static::fromArray($sanitized); + } + + /** + * Build the concrete DTO from a sanitized array. + * + * @param TInputSanitize $sanitized + * + * @return static + */ + abstract protected static function fromArray(array $sanitized): static; +} diff --git a/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php b/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php new file mode 100644 index 0000000..6194bd8 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; + +/** + * @phpstan-import-type TAuthInput from InputTypes + */ +final class AuthInput extends AbstractValueObject +{ + /** + * @param string|null $email + * @param string|null $password + * @param string|null $token + */ + public function __construct( + public readonly ?string $email, + public readonly ?string $password, + public readonly ?string $token + ) { + } + + /** + * Build the concrete DTO from a sanitized array. + * + * @param TAuthInput $sanitized + * + * @return static + */ + protected static function fromArray(array $sanitized): static + { + $email = $sanitized['email'] ?? null; + $password = $sanitized['password'] ?? null; + $token = $sanitized['token'] ?? null; + + return new self($email, $password, $token); + } +} diff --git a/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php b/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php new file mode 100644 index 0000000..c4f24a8 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; + +/** + * @phpstan-import-type TAuthLoginInput from InputTypes + * @phpstan-import-type TAuthLogoutInput from InputTypes + * @phpstan-import-type TAuthRefreshInput from InputTypes + */ +final class AuthFacade +{ + /** + * @param UserRepositoryInterface $repository + * @param SanitizerInterface $sanitizer + * @param TokenManagerInterface $tokenManager + * @param Security $security + */ + public function __construct( + private readonly UserRepositoryInterface $repository, + private readonly SanitizerInterface $sanitizer, + private readonly TokenManagerInterface $tokenManager, + private readonly Security $security, + ) { + } + + /** + * Authenticates users (login) + * + * @param TAuthLoginInput $input + * @param ValidatorInterface $validator + * + * @return Payload + */ + public function authenticate( + array $input, + ValidatorInterface $validator + ): Payload { + /** + * Data Transfer Object + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Validate + */ + $validation = $validator->validate($dto); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * Find the user by email + */ + /** @var string $email */ + $email = $dto->email; + $domainUser = $this->repository->findByEmail($email); + if (null === $domainUser) { + return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); + } + + /** + * Verify the password + */ + /** @var string $suppliedPassword */ + $suppliedPassword = $dto->password; + /** @var string $dbPassword */ + $dbPassword = $domainUser->password; + if (true !== $this->security->verify($suppliedPassword, $dbPassword)) { + return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); + } + + /** + * Issue a new set of tokens + */ + $tokens = $this->tokenManager->issue($domainUser); + + /** + * Construct the response + */ + $results = [ + 'authenticated' => true, + 'user' => [ + 'id' => $domainUser->id, + 'name' => $domainUser->fullName(), + 'email' => $domainUser->email, + ], + 'jwt' => [ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ], + ]; + + return Payload::success($results); + } + + /** + * Logout: revoke refresh token after parsing/validation. + * + * @param TAuthLogoutInput $input + * @param ValidatorInterface $validator + * + * @return Payload + */ + public function logout( + array $input, + ValidatorInterface $validator + ): Payload { + /** + * Data Transfer Object + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Validate + */ + $validation = $validator->validate($dto); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * If we are here validation has passed and the Result object + * has the user in the meta store + */ + /** @var User $domainUser */ + $domainUser = $validation->getMeta('user'); + + $this->tokenManager->revoke($domainUser); + + return Payload::success(['authenticated' => false]); + } + + /** + * Refresh: validate refresh token, issue new tokens via TokenManager. + * + * @param TAuthLogoutInput $input + * @param ValidatorInterface $validator + * + * @return Payload + */ + public function refresh( + array $input, + ValidatorInterface $validator + ): Payload { + /** + * Data Transfer Object + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Validate + */ + $validation = $validator->validate($dto); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * If we are here validation has passed and the Result object + * has the user in the meta store + */ + /** @var User $domainUser */ + $domainUser = $validation->getMeta('user'); + + $tokens = $this->tokenManager->refresh($domainUser); + + return Payload::success([ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ]); + } +} diff --git a/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php b/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php new file mode 100644 index 0000000..8f8c6ac --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers; + +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\AuthSanitizersEnum; + +final class AuthSanitizer extends AbstractSanitizer +{ + protected string $enum = AuthSanitizersEnum::class; +} diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php new file mode 100644 index 0000000..a987221 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\AuthLoginValidatorEnum; + +final class AuthLoginValidator extends AbstractValidator +{ + protected string $fields = AuthLoginValidatorEnum::class; + + /** + * Validate a AuthInput and return an array of errors. + * Empty array means valid. + * + * @param AuthInput $input + * + * @return Result + */ + public function validate(mixed $input): Result + { + $errors = $this->runValidations($input); + if (true !== empty($errors)) { + return Result::error( + [HttpCodesEnum::AppIncorrectCredentials->error()] + ); + } + + return Result::success(); + } +} diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php new file mode 100644 index 0000000..6b2ed17 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\AuthTokenValidatorEnum; +use Phalcon\Encryption\Security\JWT\Token\Token; +use Phalcon\Filter\Validation\ValidationInterface; + +final class AuthTokenValidator extends AbstractValidator +{ + protected string $fields = AuthTokenValidatorEnum::class; + + public function __construct( + private TokenManagerInterface $tokenManager, + private UserRepositoryInterface $userRepository, + ValidationInterface $validator, + ) { + parent::__construct($validator); + } + + /** + * Validate a AuthInput and return an array of errors. + * Empty array means valid. + * + * @param AuthInput $input + * + * @return Result + */ + public function validate(mixed $input): Result + { + $errors = $this->runValidations($input); + if (true !== empty($errors)) { + return Result::error([HttpCodesEnum::AppTokenNotPresent->error()]); + } + + /** @var string $token */ + $token = $input->token; + $tokenObject = $this->tokenManager->getObject($token); + if (null === $tokenObject) { + return Result::error([HttpCodesEnum::AppTokenNotValid->error()]); + } + + if ($this->tokenIsNotRefresh($tokenObject)) { + return Result::error([HttpCodesEnum::AppTokenNotValid->error()]); + } + + $domainUser = $this->tokenManager->getUser($this->userRepository, $tokenObject); + if (null === $domainUser) { + return Result::error([HttpCodesEnum::AppTokenInvalidUser->error()]); + } + + $errors = $this->tokenManager->validate($tokenObject, $domainUser); + if (!empty($errors)) { + return Result::error($errors); + } + + $result = Result::success(); + $result->setMeta('user', $domainUser); + + return $result; + } + + /** + * Return if the token is a refresh one or not + * + * @param Token $tokenObject + * + * @return bool + */ + private function tokenIsNotRefresh(Token $tokenObject): bool + { + $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); + + return false === $isRefresh; + } +} diff --git a/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php b/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php new file mode 100644 index 0000000..4392806 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +/** + * Contract for mapping between domain DTO/objects and persistence arrays. + * + * @phpstan-import-type TUser from UserTypes + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + */ +interface MapperInterface +{ + /** + * Map Domain User -> DB row (usr_*) + * + * @return TUserDomainToDbRecord + */ + public function db(UserInput $user): array; + + /** + * Map DB row (usr_*) -> Domain User + * + * @param TUserDbRecord|array{} $row + */ + public function domain(array $row): User; + + /** + * Map input row -> Domain User + * + * @param TUser $row + */ + public function input(array $row): User; +} diff --git a/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php b/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php new file mode 100644 index 0000000..543430d --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces; + +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TInputSanitize from InputTypes + */ +interface SanitizerInterface +{ + /** + * Return a sanitized array of the input + * + * @param TInputSanitize $input + * + * @return TInputSanitize + */ + public function sanitize(array $input): array; +} diff --git a/src/Domain/Infrastructure/DataSource/User/DTO/User.php b/src/Domain/Infrastructure/DataSource/User/DTO/User.php new file mode 100644 index 0000000..ef3cb39 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/DTO/User.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +use function get_object_vars; +use function trim; + +/** + * @phpstan-import-type TUser from UserTypes + */ +final class User +{ + public function __construct( + public readonly int $id, + public readonly int $status, + public readonly string $email, + public readonly string $password, + public readonly ?string $namePrefix, + public readonly ?string $nameFirst, + public readonly ?string $nameMiddle, + public readonly ?string $nameLast, + public readonly ?string $nameSuffix, + public readonly ?string $issuer, + public readonly ?string $tokenPassword, + public readonly ?string $tokenId, + public readonly ?string $preferences, + public readonly ?string $createdDate, + public readonly ?int $createdUserId, + public readonly ?string $updatedDate, + public readonly ?int $updatedUserId, + ) { + } + + public function fullName(): string + { + return trim( + ($this->nameLast ?? '') . ', ' . ($this->nameFirst ?? '') . ' ' . ($this->nameMiddle ?? '') + ); + } + + /** + * @return TUser + */ + public function toArray(): array + { + /** @var TUser $vars */ + $vars = get_object_vars($this); + + return $vars; + } +} diff --git a/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php b/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php new file mode 100644 index 0000000..cce2cca --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +use function get_object_vars; + +/** + * Value object for input + * + * @phpstan-import-type TUserInput from InputTypes + * @phpstan-import-type TUser from UserTypes + */ +final class UserInput extends AbstractValueObject +{ + /** + * @param int $id + * @param int $status + * @param string|null $email + * @param string|null $password + * @param string|null $namePrefix + * @param string|null $nameFirst + * @param string|null $nameMiddle + * @param string|null $nameLast + * @param string|null $nameSuffix + * @param string|null $issuer + * @param string|null $tokenPassword + * @param string|null $tokenId + * @param string|null $preferences + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + */ + public function __construct( + public readonly int $id, + public readonly int $status, + public readonly ?string $email, + public readonly ?string $password, + public readonly ?string $namePrefix, + public readonly ?string $nameFirst, + public readonly ?string $nameMiddle, + public readonly ?string $nameLast, + public readonly ?string $nameSuffix, + public readonly ?string $issuer, + public readonly ?string $tokenPassword, + public readonly ?string $tokenId, + public readonly ?string $preferences, + public readonly ?string $createdDate, + public readonly ?int $createdUserId, + public readonly ?string $updatedDate, + public readonly ?int $updatedUserId, + ) { + } + + /** + * @return TUser + */ + public function toArray(): array + { + /** @var TUser $vars */ + $vars = get_object_vars($this); + + return $vars; + } + + /** + * Build the concrete DTO from a sanitized array. + * + * @param TUserInput $sanitized + * + * @return static + */ + protected static function fromArray(array $sanitized): static + { + $id = isset($sanitized['id']) ? (int)$sanitized['id'] : 0; + $status = isset($sanitized['status']) ? (int)$sanitized['status'] : 0; + + $createdUserId = isset($sanitized['createdUserId']) ? (int)$sanitized['createdUserId'] : 0; + $updatedUserId = isset($sanitized['updatedUserId']) ? (int)$sanitized['updatedUserId'] : 0; + + return new self( + $id, + $status, + isset($sanitized['email']) ? (string)$sanitized['email'] : null, + isset($sanitized['password']) ? (string)$sanitized['password'] : null, + isset($sanitized['namePrefix']) ? (string)$sanitized['namePrefix'] : null, + isset($sanitized['nameFirst']) ? (string)$sanitized['nameFirst'] : null, + isset($sanitized['nameMiddle']) ? (string)$sanitized['nameMiddle'] : null, + isset($sanitized['nameLast']) ? (string)$sanitized['nameLast'] : null, + isset($sanitized['nameSuffix']) ? (string)$sanitized['nameSuffix'] : null, + isset($sanitized['issuer']) ? (string)$sanitized['issuer'] : null, + isset($sanitized['tokenPassword']) ? (string)$sanitized['tokenPassword'] : null, + isset($sanitized['tokenId']) ? (string)$sanitized['tokenId'] : null, + isset($sanitized['preferences']) ? (string)$sanitized['preferences'] : null, + isset($sanitized['createdDate']) ? (string)$sanitized['createdDate'] : null, + $createdUserId, + isset($sanitized['updatedDate']) ? (string)$sanitized['updatedDate'] : null, + $updatedUserId + ); + } +} diff --git a/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php b/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php new file mode 100644 index 0000000..574cbc7 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php @@ -0,0 +1,350 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades; + +use PDOException; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; + +use function array_filter; + +/** + * Orchestration for workflow + * + * - Sanitization + * - DTO creation + * - Validation + * - Pre-operation checks (when necessary) + * - Repository operation + * + * @phpstan-import-type TUserInput from InputTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +final class UserFacade +{ + /** + * @param SanitizerInterface $sanitizer + * @param ValidatorInterface $validator + * @param MapperInterface $mapper + * @param UserRepositoryInterface $repository + * @param Security $security + */ + public function __construct( + private readonly SanitizerInterface $sanitizer, + private readonly ValidatorInterface $validator, + private readonly MapperInterface $mapper, + private readonly UserRepositoryInterface $repository, + private readonly Security $security, + ) { + } + + /** + * Delete a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function delete(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + $userId = $dto->id; + + /** + * Success + */ + if ($userId > 0) { + $rowCount = $this->repository->deleteById($userId); + + if (0 !== $rowCount) { + return Payload::deleted( + [ + 'Record deleted successfully [#' . $userId . '].', + ], + ); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } + + /** + * Get a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function get(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + $userId = $dto->id; + + /** + * Success + */ + if ($userId > 0) { + $user = $this->repository->findById($userId); + + if (null !== $user) { + return Payload::success([$user->id => $user->toArray()]); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } + + /** + * Create a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function insert(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + + $validation = $this->validator->validate($dto); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Array for inserting + */ + $user = $this->mapper->db($dto); + + /** + * Pre-insert checks and manipulations + */ + $user = $this->preInsert($user); + + /** + * Insert the record + */ + try { + $userId = $this->repository->insert($user); + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($userId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the user from the database + */ + /** @var User $domainUser */ + $domainUser = $this->repository->findById($userId); + + /** + * Return the user back + */ + return Payload::created([$domainUser->id => $domainUser->toArray()]); + } + + /** + * Create a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function update(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + + $validation = $this->validator->validate($dto); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Check if the user exists, If not, return an error + */ + /** @var int $userId */ + $userId = $dto->id; + $domainUser = $this->repository->findById($userId); + + if (null === $domainUser) { + return Payload::notFound(); + } + + /** + * Array for updating + */ + $user = $this->mapper->db($dto); + + /** + * Pre-update checks and manipulations + */ + $user = $this->preUpdate($user); + + /** + * Update the record + */ + try { + $userId = $this->repository->update($userId, $user); + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($userId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the user from the database + */ + /** @var User $domainUser */ + $domainUser = $this->repository->findById($userId); + + /** + * Return the user back + */ + return Payload::updated([$domainUser->id => $domainUser->toArray()]); + } + + /** + * @param TUserDbRecordOptional $row + * + * @return TUserDbRecordOptional + */ + private function cleanupFields(array $row): array + { + unset($row['usr_id']); + + return array_filter( + $row, + static fn($v) => $v !== null && $v !== '' + ); + } + + /** + * @param string $message + * + * @return Payload + */ + private function getErrorPayload( + HttpCodesEnum $item, + string $message + ): Payload { + return Payload::error([[$item->text() . $message]]); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDbRecordOptional + */ + private function preInsert(array $input): array + { + $result = $this->processPassword($input); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + + /** + * Set the created/updated dates if need be + */ + if (true === empty($result['usr_created_date'])) { + $result['usr_created_date'] = $now; + } + if (true === empty($result['usr_updated_date'])) { + $result['usr_updated_date'] = $now; + } + + /** @var TUserDbRecordOptional $result */ + return $this->cleanupFields($result); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDbRecordOptional + */ + private function preUpdate(array $input): array + { + $result = $this->processPassword($input); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + + /** + * Set updated date to now if it has not been set + */ + if (true === empty($result['usr_updated_date'])) { + $result['usr_updated_date'] = $now; + } + + /** + * Remove createdDate and createdUserId - cannot be changed. This + * needs to be here because we don't want to touch those fields. + */ + unset($result['usr_created_date'], $result['usr_created_usr_id']); + + + return $this->cleanupFields($result); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDomainToDbRecord + */ + private function processPassword(array $input): array + { + if (null !== $input['usr_password']) { + $plain = $input['usr_password']; + $hashed = $this->security->hash($plain); + + $input['usr_password'] = $hashed; + } + + return $input; + } +} diff --git a/src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php b/src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php new file mode 100644 index 0000000..cc6b0c6 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +/** + * @phpstan-import-type TUser from UserTypes + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + */ +final class UserMapper implements MapperInterface +{ + /** + * Map Domain User -> DB row (usr_*) + * + * @return TUserDomainToDbRecord + */ + public function db(UserInput $user): array + { + return [ + 'usr_id' => $user->id, + 'usr_status_flag' => $user->status, + 'usr_email' => $user->email, + 'usr_password' => $user->password, + 'usr_name_prefix' => $user->namePrefix, + 'usr_name_first' => $user->nameFirst, + 'usr_name_middle' => $user->nameMiddle, + 'usr_name_last' => $user->nameLast, + 'usr_name_suffix' => $user->nameSuffix, + 'usr_issuer' => $user->issuer, + 'usr_token_password' => $user->tokenPassword, + 'usr_token_id' => $user->tokenId, + 'usr_preferences' => $user->preferences, + 'usr_created_date' => $user->createdDate, + 'usr_created_usr_id' => $user->createdUserId, + 'usr_updated_date' => $user->updatedDate, + 'usr_updated_usr_id' => $user->updatedUserId, + ]; + } + + /** + * Map DB row (usr_*) -> Domain User + * + * @param TUserDbRecord|array{} $row + */ + public function domain(array $row): User + { + return new User( + (int)($row['usr_id'] ?? 0), + (int)($row['usr_status_flag'] ?? 0), + (string)($row['usr_email'] ?? ''), + $row['usr_password'] ?? '', + $row['usr_name_prefix'] ?? null, + $row['usr_name_first'] ?? null, + $row['usr_name_middle'] ?? null, + $row['usr_name_last'] ?? null, + $row['usr_name_suffix'] ?? null, + $row['usr_issuer'] ?? null, + $row['usr_token_password'] ?? null, + $row['usr_token_id'] ?? null, + $row['usr_preferences'] ?? null, + $row['usr_created_date'] ?? null, + isset($row['usr_created_usr_id']) ? (int)$row['usr_created_usr_id'] : null, + $row['usr_updated_date'] ?? null, + isset($row['usr_updated_usr_id']) ? (int)$row['usr_updated_usr_id'] : null, + ); + } + + /** + * Map input row -> Domain User + * + * @param TUser $row + */ + public function input(array $row): User + { + return new User( + $row['id'], + $row['status'], + (string)$row['email'], + (string)$row['password'], + $row['namePrefix'], + $row['nameFirst'], + $row['nameMiddle'], + $row['nameLast'], + $row['nameSuffix'], + $row['issuer'], + $row['tokenPassword'], + $row['tokenId'], + $row['preferences'], + $row['createdDate'], + $row['createdUserId'], + $row['updatedDate'], + $row['updatedUserId'], + ); + } +} diff --git a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php new file mode 100644 index 0000000..7052084 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories; + +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\DataMapper\Query\Insert; +use Phalcon\DataMapper\Query\Select; +use Phalcon\DataMapper\Query\Update; + +/** + * @phpstan-import-type TCriteria from UserTypes + * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +class UserRepository extends AbstractRepository implements UserRepositoryInterface +{ + /** + * @var string + */ + protected string $idField = 'usr_id'; + /** + * @var string + */ + protected string $table = 'co_users'; + + public function __construct( + Connection $connection, + private readonly MapperInterface $mapper, + ) { + parent::__construct($connection); + } + + + /** + * @param string $email + * + * @return User|null + */ + public function findByEmail(string $email): ?User + { + if (true !== empty($email)) { + return $this->findOneBy( + [ + 'usr_email' => $email, + 'usr_status_flag' => FlagsEnum::Active->value, + ] + ); + } + + return null; + } + + /** + * @param int $recordId + * + * @return User|null + */ + public function findById(int $recordId): ?User + { + if ($recordId > 0) { + return $this->findOneBy( + [ + $this->idField => $recordId, + ] + ); + } + + return null; + } + + + /** + * @param TCriteria $criteria + * + * @return User|null + */ + public function findOneBy(array $criteria): ?User + { + $select = Select::new($this->connection); + + /** @var TUserRecord $result */ + $result = $select + ->from($this->table) + ->whereEquals($criteria) + ->fetchOne() + ; + + if (empty($result)) { + return null; + } + + return $this->mapper->domain($result); + } + + + /** + * + * @param TUserDbRecordOptional $columns + * + * @return int + */ + public function insert(array $columns): int + { + $insert = Insert::new($this->connection); + $insert + ->into($this->table) + ->columns($columns) + ->perform() + ; + + return (int)$insert->getLastInsertId(); + } + + /** + * @param int $recordId + * @param TUserDbRecordOptional $columns + * + * @return int + */ + public function update(int $recordId, array $columns): int + { + $update = Update::new($this->connection); + $update + ->table($this->table) + ->columns($columns) + ->where($this->idField . ' = ', $recordId) + ->perform() + ; + + return $recordId; + } +} diff --git a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php new file mode 100644 index 0000000..a8a1572 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +/** + * @phpstan-import-type TCriteria from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +interface UserRepositoryInterface +{ + /** + * @param TCriteria $criteria + * + * @return int + */ + public function deleteBy(array $criteria): int; + + /** + * @param int $recordId + * + * @return int + */ + public function deleteById(int $recordId): int; + + /** + * @param string $email + * + * @return User|null + */ + public function findByEmail(string $email): ?User; + + /** + * @param int $recordId + * + * @return User|null + */ + public function findById(int $recordId): ?User; + + /** + * @param TCriteria $criteria + * + * @return User|null + */ + public function findOneBy(array $criteria): ?User; + + /** + * @param TUserDbRecordOptional $columns + * + * @return int + */ + public function insert(array $columns): int; + + /** + * @param int $recordId + * @param TUserDbRecordOptional $columns + * + * @return int + */ + public function update(int $recordId, array $columns): int; +} diff --git a/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php b/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php new file mode 100644 index 0000000..f158eb4 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers; + +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\UserSanitizersEnum; + +final class UserSanitizer extends AbstractSanitizer +{ + protected string $enum = UserSanitizersEnum::class; +} diff --git a/src/Domain/Infrastructure/DataSource/User/UserTypes.php b/src/Domain/Infrastructure/DataSource/User/UserTypes.php new file mode 100644 index 0000000..f875630 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/UserTypes.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User; + +/** + * @phpstan-type TLoginResponsePayload array{ + * authenticated: true, + * user: array{ + * id: int, + * name: string, + * email: string + * }, + * jwt: array{ + * token: string, + * refreshToken: string, + * } + * } + * + * @phpstan-type TUserDbRecord array{ + * usr_id: int, + * usr_status_flag: int, + * usr_email: string, + * usr_password: string, + * usr_name_prefix: string, + * usr_name_first: string, + * usr_name_middle: string, + * usr_name_last: string, + * usr_name_suffix: string, + * usr_issuer: string, + * usr_token_password: string, + * usr_token_id: string, + * usr_preferences: string, + * usr_created_date: string, + * usr_created_usr_id: int, + * usr_updated_date: string, + * usr_updated_usr_id: int, + * } + * + * @phpstan-type TUserDomainToDbRecord array{ + * usr_id: int, + * usr_status_flag: int, + * usr_email: ?string, + * usr_password: ?string, + * usr_name_prefix: ?string, + * usr_name_first: ?string, + * usr_name_middle: ?string, + * usr_name_last: ?string, + * usr_name_suffix: ?string, + * usr_issuer: ?string, + * usr_token_password: ?string, + * usr_token_id: ?string, + * usr_preferences: ?string, + * usr_created_date: ?string, + * usr_created_usr_id: ?int, + * usr_updated_date: ?string, + * usr_updated_usr_id: ?int, + * } + * + * @phpstan-type TUserDbRecordOptional array{ + * usr_id?: int, + * usr_status_flag?: int, + * usr_email?: ?string, + * usr_password?: ?string, + * usr_name_prefix?: ?string, + * usr_name_first?: ?string, + * usr_name_middle?: ?string, + * usr_name_last?: ?string, + * usr_name_suffix?: ?string, + * usr_issuer?: ?string, + * usr_token_password?: ?string, + * usr_token_id?: ?string, + * usr_preferences?: ?string, + * usr_created_date?: ?string, + * usr_created_usr_id?: ?int, + * usr_updated_date?: ?string, + * usr_updated_usr_id?: ?int, + * } + * + * @phpstan-type TUser array{ + * id: int, + * status: int, + * email: ?string, + * password: ?string, + * namePrefix: ?string, + * nameFirst: ?string, + * nameMiddle: ?string, + * nameLast: ?string, + * nameSuffix: ?string, + * issuer: ?string, + * tokenPassword: ?string, + * tokenId: ?string, + * preferences: ?string, + * createdDate: ?string, + * createdUserId: ?int, + * updatedDate: ?string, + * updatedUserId: ?int, + * } + * + * @phpstan-type TUserTokenDbRecord array{ + * usr_id: int, + * usr_issuer: string, + * usr_token_password: string, + * usr_token_id: string + * } + * + * @phpstan-type TUserRecord array{}|TUserDbRecord + * + * @phpstan-type TCriteria array + */ +final class UserTypes +{ +} diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php new file mode 100644 index 0000000..729aee7 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\UserInsertEnum; + +final class UserValidator extends AbstractValidator +{ + use UserValidatorTrait; + + protected string $fields = UserInsertEnum::class; +} diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php new file mode 100644 index 0000000..69fd2b9 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; + +trait UserValidatorTrait +{ + /** + * Validate a AuthInput and return an array of errors. + * Empty array means valid. + * + * @param AuthInput $input + * + * @return Result + */ + public function validate(mixed $input): Result + { + $errors = $this->runValidations($input); + if (true !== empty($errors)) { + return Result::error($errors); + } + + return Result::success(); + } + + /** + * @param AuthInput $input + * + * @return array + */ + abstract protected function runValidations(mixed $input): array; +} diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php new file mode 100644 index 0000000..291eea1 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\UserUpdateEnum; + +final class UserValidatorUpdate extends AbstractValidator +{ + use UserValidatorTrait; + + protected string $fields = UserUpdateEnum::class; +} diff --git a/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php new file mode 100644 index 0000000..2344291 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; + +use Phalcon\Filter\Validation; +use Phalcon\Filter\Validation\Validator\Numericality; + +final class AbsInt extends Numericality +{ + /** + * @var string|null + */ + protected string | null $template = "Field :field is not a valid absolute integer and greater than 0"; + + /** + * Executes the validation + * + * @param Validation $validation + * @param string $field + * + * @return bool + * @throws Validation\Exception + */ + public function validate(Validation $validation, string $field): bool + { + $result = parent::validate($validation, $field); + + if (false === $result) { + return false; + } + + /** @var string $value */ + $value = $validation->getValue($field); + + if ($value <= 0) { + $validation->appendMessage( + $this->messageFactory($validation, $field) + ); + + return false; + } + + return true; + } +} diff --git a/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php new file mode 100644 index 0000000..2048065 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\ValidatorEnumInterface; +use Phalcon\Filter\Validation\ValidationInterface; +use Phalcon\Filter\Validation\ValidatorInterface as PhalconValidator; + +abstract class AbstractValidator implements ValidatorInterface +{ + protected string $fields = ''; + + public function __construct( + private ValidationInterface $validation + ) { + } + + /** + * @param AuthInput $input + * + * @return list> + */ + protected function runValidations(mixed $input): array + { + $enum = $this->fields; + /** @var ValidatorEnumInterface[] $elements */ + $elements = $enum::cases(); + + /** @var ValidatorEnumInterface $element */ + foreach ($elements as $element) { + $validators = $element->validators(); + foreach ($validators as $validator) { + /** @var PhalconValidator $validatorObject */ + $validatorObject = new $validator(); + $this->validation->add($element->name, $validatorObject); + } + } + + $this->validation->validate($input); + $messages = $this->validation->getMessages(); + + $results = []; + foreach ($messages as $message) { + $results[] = [$message->getMessage()]; + } + + + return $results; + } +} diff --git a/src/Domain/Infrastructure/DataSource/Validation/Result.php b/src/Domain/Infrastructure/DataSource/Validation/Result.php new file mode 100644 index 0000000..064d357 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Validation/Result.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; + +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TValidatorErrors from InputTypes + * @phpstan-type TResultMeta array + */ +final class Result +{ + /** + * @param TValidatorErrors $errors + * @param TResultMeta $meta + */ + public function __construct( + private array $errors = [], + private array $meta = [], + ) { + } + + /** + * Create a failure result. + * + * @param TValidatorErrors $errors + * + * @return self + */ + public static function error(array $errors): self + { + return new self($errors); + } + + /** + * @return TValidatorErrors + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * @param string $key + * @param mixed|null $defaultValue + * + * @return mixed + */ + public function getMeta(string $key, mixed $defaultValue = null): mixed + { + return $this->meta[$key] ?? $defaultValue; + } + + /** + * @return bool + */ + public function isValid(): bool + { + return $this->errors === []; + } + + /** + * @param string $key + * @param mixed $value + * + * @return void + */ + public function setMeta(string $key, mixed $value): void + { + $this->meta[$key] = $value; + } + + /** + * @return self + */ + public static function success(): self + { + return new self([]); + } +} diff --git a/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php new file mode 100644 index 0000000..e79bacd --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; + +/** + * Validator contract. Accepts a DTO or input and returns a Result. + */ +interface ValidatorInterface +{ + /** + * Validate a DTO or input structure. + * + * @param mixed $input DTO or array + * + * @return Result + */ + public function validate(mixed $input): Result; +} diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Infrastructure/Encryption/JWTToken.php similarity index 71% rename from src/Domain/Components/Encryption/JWTToken.php rename to src/Domain/Infrastructure/Encryption/JWTToken.php index c4c067b..a00026e 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Infrastructure/Encryption/JWTToken.php @@ -11,18 +11,18 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; use DateTimeImmutable; use InvalidArgumentException; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Encryption\Security\JWT\Builder; use Phalcon\Encryption\Security\JWT\Signer\Hmac; use Phalcon\Encryption\Security\JWT\Token\Parser; @@ -31,9 +31,7 @@ use Phalcon\Support\Helper\Json\Decode; /** - * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserTokenDbRecord from UserTypes - * @phpstan-type TValidatorErrors array{}|array + * @phpstan-import-type TValidatorErrors from InputTypes * * Removed the final declaration so that this class can be mocked. This * class should not be extended @@ -53,11 +51,11 @@ public function __construct( /** * Returns the string token * - * @param TUserTokenDbRecord $user + * @param User $user * * @return string */ - public function getForUser(array $user): string + public function getForUser(User $user): string { return $this->generateTokenForUser($user); } @@ -87,25 +85,25 @@ public function getObject(string $token): Token /** * Returns the string token * - * @param TUserTokenDbRecord $user + * @param User $user * * @return string */ - public function getRefreshForUser(array $user): string + public function getRefreshForUser(User $user): string { return $this->generateTokenForUser($user, true); } /** - * @param QueryRepository $repository - * @param Token $token + * @param UserRepositoryInterface $repository + * @param Token $token * - * @return TUserRecord + * @return User|null */ public function getUser( - QueryRepository $repository, + UserRepositoryInterface $repository, Token $token, - ): array { + ): ?User { /** @var string $issuer */ $issuer = $token->getClaims()->get(JWTEnum::Issuer->value); /** @var string $tokenId */ @@ -120,37 +118,43 @@ public function getUser( 'usr_token_id' => $tokenId, ]; - /** @var TUserRecord $user */ - $user = $repository->user()->findOneBy($criteria); - - return $user; + return $repository->findOneBy($criteria); } /** * Returns an array with the validation errors for this token * - * @param Token $tokenObject - * @param UserTransport $user + * @param Token $tokenObject + * @param User $user * * @return TValidatorErrors */ public function validate( Token $tokenObject, - UserTransport $user + User $user ): array { $validator = new Validator($tokenObject); $signer = new Hmac(); $now = new DateTimeImmutable(); + /** @var string $tokenId */ + $tokenId = $user->tokenId; + /** @var string $issuer */ + $issuer = $user->issuer; + /** @var string $tokenPassword */ + $tokenPassword = $user->tokenPassword; + /** @var int $userId */ + $userId = $user->id; + $validator - ->validateId($user->getTokenId()) + ->validateId($tokenId) ->validateAudience($this->getTokenAudience()) - ->validateIssuer($user->getIssuer()) + ->validateIssuer($issuer) ->validateNotBefore($now->getTimestamp()) ->validateIssuedAt($now->getTimestamp()) ->validateExpiration($now->getTimestamp()) - ->validateSignature($signer, $user->getTokenPassword()) - ->validateClaim(JWTEnum::UserId->value, $user->getId()) + ->validateSignature($signer, $tokenPassword) + ->validateClaim(JWTEnum::UserId->value, $userId) ; /** @var TValidatorErrors $errors */ @@ -162,19 +166,19 @@ public function validate( /** * Returns the string token * - * @param TUserTokenDbRecord $user - * @param bool $isRefresh + * @param User $user + * @param bool $isRefresh * * @return string */ private function generateTokenForUser( - array $user, + User $user, bool $isRefresh = false ): string { /** @var int $expiration */ $expiration = $this->env->get( 'TOKEN_EXPIRATION', - Cache::CACHE_TOKEN_EXPIRY, + CacheConstants::CACHE_TOKEN_EXPIRY, 'int' ); @@ -187,13 +191,13 @@ private function generateTokenForUser( $tokenBuilder = new Builder(new Hmac()); /** @var string $issuer */ - $issuer = $user['usr_issuer']; + $issuer = $user->issuer; /** @var string $tokenPassword */ - $tokenPassword = $user['usr_token_password']; + $tokenPassword = $user->tokenPassword; /** @var string $tokenId */ - $tokenId = $user['usr_token_id']; - /** @var string $userId */ - $userId = $user['usr_id']; + $tokenId = $user->tokenId; + /** @var int $userId */ + $userId = $user->id; $tokenObject = $tokenBuilder ->setIssuer($issuer) diff --git a/src/Domain/Components/Encryption/Security.php b/src/Domain/Infrastructure/Encryption/Security.php similarity index 95% rename from src/Domain/Components/Encryption/Security.php rename to src/Domain/Infrastructure/Encryption/Security.php index 78ae798..3499788 100644 --- a/src/Domain/Components/Encryption/Security.php +++ b/src/Domain/Infrastructure/Encryption/Security.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; use function password_hash; use function password_verify; diff --git a/src/Domain/Infrastructure/Encryption/TokenCache.php b/src/Domain/Infrastructure/Encryption/TokenCache.php new file mode 100644 index 0000000..c974469 --- /dev/null +++ b/src/Domain/Infrastructure/Encryption/TokenCache.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Encryption; + +use DateTimeImmutable; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Cache\Adapter\Redis; +use Phalcon\Cache\Cache; +use Psr\SimpleCache\InvalidArgumentException; + +/** + * Small component to issue/rotate/revoke tokens and + * interact with cache. + * + * @phpstan-import-type TTokenIssue from TokenManagerInterface + * @phpstan-import-type TValidatorErrors from InputTypes + */ +final class TokenCache implements TokenCacheInterface +{ + public function __construct( + private readonly Cache $cache, + ) { + } + + /** + * @param EnvManager $env + * @param User $domainUser + * + * @return bool + * @throws InvalidArgumentException + */ + public function invalidateForUser( + EnvManager $env, + User $domainUser + ): bool { + /** + * We could store the tokens in the database but this way is faster + * and Redis also has a TTL which auto expires elements. + * + * To get all the keys for a user, we use the underlying adapter + * of the cache which is Redis and call the `getKeys()` on it. The + * keys will come back with the prefix defined in the adapter. In order + * to delete them, we need to remove the prefix because `delete()` will + * automatically prepend each key with it. + * + * NOTE: This code will work with other adapters also, since + * `getKeys()` returns the keys of the storage adapter. This method + * exists in the Cache/Storage AdapterInterface + */ + + /** @var Redis $redis */ + $redis = $this->cache->getAdapter(); + $pattern = CacheConstants::getCacheTokenKey($domainUser, ''); + $keys = $redis->getKeys($pattern); + /** @var string $prefix */ + $prefix = $env->get('CACHE_PREFIX', '-rest-', 'string'); + $newKeys = []; + /** @var string $key */ + foreach ($keys as $key) { + $newKeys[] = substr($key, strlen($prefix)); + } + + return $this->cache->deleteMultiple($newKeys); + } + + /** + * @param EnvManager $env + * @param User $domainUser + * @param string $token + * + * @return bool + * @throws InvalidArgumentException + */ + public function storeTokenInCache( + EnvManager $env, + User $domainUser, + string $token + ): bool { + $cacheKey = CacheConstants::getCacheTokenKey($domainUser, $token); + /** @var int $expiration */ + $expiration = $env->get( + 'TOKEN_EXPIRATION', + CacheConstants::CACHE_TOKEN_EXPIRY, + 'int' + ); + $expirationDate = (new DateTimeImmutable()) + ->modify('+' . $expiration . ' seconds') + ->format(Dates::DATE_TIME_FORMAT) + ; + + $payload = [ + 'token' => $token, + 'expiry' => $expirationDate, + ]; + + return $this->cache->set($cacheKey, $payload, $expiration); + } +} diff --git a/src/Domain/Infrastructure/Encryption/TokenCacheInterface.php b/src/Domain/Infrastructure/Encryption/TokenCacheInterface.php new file mode 100644 index 0000000..238d2a9 --- /dev/null +++ b/src/Domain/Infrastructure/Encryption/TokenCacheInterface.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Encryption; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Psr\SimpleCache\InvalidArgumentException; + +interface TokenCacheInterface +{ + /** + * @param EnvManager $env + * @param User $domainUser + * + * @return bool + * @throws InvalidArgumentException + */ + public function invalidateForUser( + EnvManager $env, + User $domainUser + ): bool; + + /** + * @param EnvManager $env + * @param User $domainUser + * @param string $token + * + * @return bool + * @throws InvalidArgumentException + */ + public function storeTokenInCache( + EnvManager $env, + User $domainUser, + string $token + ): bool; +} diff --git a/src/Domain/Infrastructure/Encryption/TokenManager.php b/src/Domain/Infrastructure/Encryption/TokenManager.php new file mode 100644 index 0000000..af67d06 --- /dev/null +++ b/src/Domain/Infrastructure/Encryption/TokenManager.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Encryption; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Encryption\Security\JWT\Token\Token; +use Throwable; + +/** + * Small component to issue/rotate/revoke tokens and + * interact with cache. + * + * @phpstan-import-type TTokenIssue from TokenManagerInterface + * @phpstan-import-type TValidatorErrors from InputTypes + */ +final class TokenManager implements TokenManagerInterface +{ + public function __construct( + private readonly TokenCacheInterface $tokenCache, + private readonly EnvManager $env, + private readonly JWTToken $jwtToken, + ) { + } + + /** + * Parse a token and return either null or a Token object + * + * @param string|null $token + * + * @return Token|null + */ + public function getObject(?string $token): ?Token + { + if (true === empty($token)) { + return null; + } + + try { + return $this->jwtToken->getObject($token); + } catch (Throwable) { + return null; + } + } + + /** + * Return the domain user from the database + * + * @param UserRepositoryInterface $repository + * @param Token $tokenObject + * + * @return User|null + */ + public function getUser( + UserRepositoryInterface $repository, + Token $tokenObject + ): ?User { + return $this->jwtToken->getUser($repository, $tokenObject); + } + + /** + * Issue new tokens for the user (token, refreshToken) + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function issue(User $domainUser): array + { + $token = $this->jwtToken->getForUser($domainUser); + $refresh = $this->jwtToken->getRefreshForUser($domainUser); + + $this->tokenCache->storeTokenInCache($this->env, $domainUser, $token); + $this->tokenCache->storeTokenInCache($this->env, $domainUser, $refresh); + + return [ + 'token' => $token, + 'refreshToken' => $refresh, + ]; + } + + /** + * Revoke old tokens and issue new ones. + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function refresh(User $domainUser): array + { + $this->revoke($domainUser); + + return $this->issue($domainUser); + } + + /** + * Revoke cached tokens for a user. + * + * @param User $domainUser + * + * @return void + */ + public function revoke(User $domainUser): void + { + $this->tokenCache->invalidateForUser($this->env, $domainUser); + } + + /** + * Validate token claims + * + * @param Token $tokenObject + * @param User $user + * + * @return TValidatorErrors + */ + public function validate(Token $tokenObject, User $user): array + { + return $this->jwtToken->validate($tokenObject, $user); + } +} diff --git a/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php b/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php new file mode 100644 index 0000000..48386fc --- /dev/null +++ b/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Encryption; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Encryption\Security\JWT\Token\Token; + +/** + * @phpstan-type TTokenIssue array{ + * token: string, + * refreshToken: string + * } + * + * @phpstan-import-type TValidatorErrors from InputTypes + */ +interface TokenManagerInterface +{ + /** + * Parse a token and return either null or a Token object + * + * @param string $token + * + * @return Token|null + */ + public function getObject(string $token): ?Token; + + /** + * Return the domain user from the database + * + * @param UserRepositoryInterface $repository + * @param Token $tokenObject + * + * @return User|null + */ + public function getUser( + UserRepositoryInterface $repository, + Token $tokenObject + ): ?User; + + /** + * Issue new tokens for the user (token, refreshToken) + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function issue(User $domainUser): array; + + /** + * Revoke old tokens and issue new ones. + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function refresh(User $domainUser): array; + + /** + * Revoke cached tokens for a user. + * + * @param User $domainUser + * + * @return void + */ + public function revoke(User $domainUser): void; + + /** + * Validate token claims + * + * @param Token $tokenObject + * @param User $user + * + * @return TValidatorErrors + */ + public function validate(Token $tokenObject, User $user): array; +} diff --git a/src/Domain/Components/Enums/Common/FlagsEnum.php b/src/Domain/Infrastructure/Enums/Common/FlagsEnum.php similarity index 82% rename from src/Domain/Components/Enums/Common/FlagsEnum.php rename to src/Domain/Infrastructure/Enums/Common/FlagsEnum.php index 0a0be5b..78c5785 100644 --- a/src/Domain/Components/Enums/Common/FlagsEnum.php +++ b/src/Domain/Infrastructure/Enums/Common/FlagsEnum.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Common; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Common; -use Phalcon\Api\Domain\Components\Enums\EnumsInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\EnumsInterface; enum FlagsEnum: int implements EnumsInterface { diff --git a/src/Domain/Components/Enums/Common/JWTEnum.php b/src/Domain/Infrastructure/Enums/Common/JWTEnum.php similarity index 92% rename from src/Domain/Components/Enums/Common/JWTEnum.php rename to src/Domain/Infrastructure/Enums/Common/JWTEnum.php index c37ff35..41a497c 100644 --- a/src/Domain/Components/Enums/Common/JWTEnum.php +++ b/src/Domain/Infrastructure/Enums/Common/JWTEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Common; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Common; enum JWTEnum: string { diff --git a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php new file mode 100644 index 0000000..12a0f0a --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades\AuthFacade; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthLoginValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthTokenValidator; +use Phalcon\Api\Domain\Services\Auth\LoginPostService; +use Phalcon\Api\Domain\Services\Auth\LogoutPostService; +use Phalcon\Api\Domain\Services\Auth\RefreshPostService; + +/** + * @phpstan-import-type TService from Container + */ +enum AuthDefinitionsEnum +{ + case AuthLoginPost; + case AuthLogoutPost; + case AuthRefreshPost; + case AuthFacade; + case AuthSanitizer; + case AuthLoginValidator; + case AuthTokenValidator; + + /** + * @return TService + */ + public function definition(): array + { + return match ($this) { + self::AuthLoginPost => [ + 'className' => LoginPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::AUTH_FACADE, + ], + [ + 'type' => 'service', + 'name' => Container::AUTH_LOGIN_VALIDATOR, + ], + ], + ], + self::AuthLogoutPost => $this->getService(LogoutPostService::class), + self::AuthRefreshPost => $this->getService(RefreshPostService::class), + self::AuthFacade => [ + 'className' => AuthFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::AUTH_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Container::SECURITY, + ], + ], + ], + self::AuthSanitizer => [ + 'className' => AuthSanitizer::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ], + self::AuthLoginValidator => [ + 'className' => AuthLoginValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::VALIDATION, + ], + ], + ], + self::AuthTokenValidator => [ + 'className' => AuthTokenValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::VALIDATION, + ], + ], + ], + }; + } + + /** + * @return TService + */ + private function getService(string $className): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::AUTH_FACADE, + ], + [ + 'type' => 'service', + 'name' => Container::AUTH_TOKEN_VALIDATOR, + ], + ], + ]; + } +} diff --git a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php new file mode 100644 index 0000000..c3f426f --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Events\Manager as EventsManager; +use Phalcon\Mvc\Router; + +/** + * @phpstan-import-type TService from Container + */ +enum CommonDefinitionsEnum +{ + case EventsManager; + case JWTToken; + case JWTTokenCache; + case JWTTokenManager; + case Router; + + /** + * @return TService + */ + public function definition(): array + { + return match ($this) { + self::EventsManager => [ + 'className' => EventsManager::class, + 'calls' => [ + [ + 'method' => 'enablePriorities', + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => true, + ], + ], + ], + ], + ], + self::JWTToken => [ + 'className' => JWTToken::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::ENV, + ], + ], + ], + self::JWTTokenCache => [ + 'className' => TokenCache::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::CACHE, + ], + ], + ], + self::JWTTokenManager => [ + 'className' => TokenManager::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN_CACHE, + ], + [ + 'type' => 'service', + 'name' => Container::ENV, + ], + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN, + ], + ], + ], + self::Router => [ + 'className' => Router::class, + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => false, + ], + ], + ] + }; + } +} diff --git a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php new file mode 100644 index 0000000..38e3565 --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades\UserFacade; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidatorUpdate; +use Phalcon\Api\Domain\Services\User\UserDeleteService; +use Phalcon\Api\Domain\Services\User\UserGetService; +use Phalcon\Api\Domain\Services\User\UserPostService; +use Phalcon\Api\Domain\Services\User\UserPutService; + +/** + * @phpstan-import-type TService from Container + */ +enum UserDefinitionsEnum +{ + case UserDelete; + case UserGet; + case UserPost; + case UserPut; + case UserFacade; + case UserFacadeUpdate; + case UserRepository; + case UserSanitizer; + case UserValidator; + case UserValidatorUpdate; + + /** + * @return TService + */ + public function definition(): array + { + return match ($this) { + self::UserDelete => $this->getService(UserDeleteService::class), + self::UserGet => $this->getService(UserGetService::class), + self::UserPost => $this->getService(UserPostService::class), + self::UserPut => [ + 'className' => UserPutService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_FACADE_UPDATE, + ], + ], + ], + self::UserFacade => [ + 'className' => UserFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_VALIDATOR, + ], + [ + 'type' => 'service', + 'name' => Container::USER_MAPPER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::SECURITY, + ], + ], + ], + self::UserFacadeUpdate => [ + 'className' => UserFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_VALIDATOR_UPDATE, + ], + [ + 'type' => 'service', + 'name' => Container::USER_MAPPER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::SECURITY, + ], + ], + ], + self::UserRepository => [ + 'className' => UserRepository::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::CONNECTION, + ], + [ + 'type' => 'service', + 'name' => Container::USER_MAPPER, + ], + ], + ], + self::UserSanitizer => [ + 'className' => UserSanitizer::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ], + self::UserValidator => $this->getServiceValidator(UserValidator::class), + self::UserValidatorUpdate => $this->getServiceValidator(UserValidatorUpdate::class), + }; + } + + /** + * @param class-string $className + * + * @return TService + */ + private function getService(string $className): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_FACADE, + ], + ], + ]; + } + + /** + * @param class-string $className + * + * @return TService + */ + private function getServiceValidator(string $className): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::VALIDATION, + ], + ], + ]; + } +} diff --git a/src/Domain/Components/Enums/EnumsInterface.php b/src/Domain/Infrastructure/Enums/EnumsInterface.php similarity index 89% rename from src/Domain/Components/Enums/EnumsInterface.php rename to src/Domain/Infrastructure/Enums/EnumsInterface.php index b763f88..34128e6 100644 --- a/src/Domain/Components/Enums/EnumsInterface.php +++ b/src/Domain/Infrastructure/Enums/EnumsInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums; +namespace Phalcon\Api\Domain\Infrastructure\Enums; use BackedEnum; diff --git a/src/Domain/Components/Enums/Http/HttpCodesEnum.php b/src/Domain/Infrastructure/Enums/Http/HttpCodesEnum.php similarity index 97% rename from src/Domain/Components/Enums/Http/HttpCodesEnum.php rename to src/Domain/Infrastructure/Enums/Http/HttpCodesEnum.php index cdb07e7..19f561c 100644 --- a/src/Domain/Components/Enums/Http/HttpCodesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/HttpCodesEnum.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Http; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Enums\EnumsInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\EnumsInterface; /** * Enumeration for Http Codes. These are not only the regular HTTP codes diff --git a/src/Domain/Components/Enums/Http/RoutesEnum.php b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php similarity index 97% rename from src/Domain/Components/Enums/Http/RoutesEnum.php rename to src/Domain/Infrastructure/Enums/Http/RoutesEnum.php index e28552c..32ab547 100644 --- a/src/Domain/Components/Enums/Http/RoutesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Http; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use function str_replace; @@ -22,15 +22,16 @@ */ enum RoutesEnum: int { - /** - * Methods - */ - public const DELETE = 'delete'; /** * Events */ public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; + + /** + * Methods + */ + public const DELETE = 'delete'; public const GET = 'get'; public const POST = 'post'; public const PUT = 'put'; diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php b/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php new file mode 100644 index 0000000..2018e0f --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers; + +use Phalcon\Filter\Filter; + +enum AuthSanitizersEnum implements SanitizersEnumInterface +{ + case email; + case password; + case token; + + public function default(): mixed + { + return null; + } + + public function sanitizer(): string + { + return match ($this) { + self::email => Filter::FILTER_EMAIL, + default => '' + }; + } +} diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php b/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php new file mode 100644 index 0000000..0738cfd --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers; + +use UnitEnum; + +interface SanitizersEnumInterface extends UnitEnum +{ + public function default(): mixed; + + public function sanitizer(): string; +} diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php b/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php new file mode 100644 index 0000000..ff61d68 --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers; + +use Phalcon\Filter\Filter; + +enum UserSanitizersEnum implements SanitizersEnumInterface +{ + case id; + case status; + case email; + case password; + case namePrefix; + case nameFirst; + case nameLast; + case nameMiddle; + case nameSuffix; + case issuer; + case tokenPassword; + case tokenId; + case preferences; + case createdDate; + case createdUserId; + case updatedDate; + case updatedUserId; + + public function default(): mixed + { + return match ($this) { + self::id, + self::status, + self::createdUserId, + self::updatedUserId => 0, + default => null + }; + } + + public function sanitizer(): string + { + return match ($this) { + self::id, + self::status, + self::createdUserId, + self::updatedUserId => Filter::FILTER_ABSINT, + self::email => Filter::FILTER_EMAIL, + self::password, + self::tokenId, + self::tokenPassword => '', // Password will be distorted + default => Filter::FILTER_STRING + }; + } +} diff --git a/src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php b/src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php new file mode 100644 index 0000000..cc536cf --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; + +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum AuthLoginValidatorEnum implements ValidatorEnumInterface +{ + case email; + case password; + + public function validators(): array + { + return match ($this) { + self::email => [ + PresenceOf::class, + Email::class + ], + self::password => [PresenceOf::class], + }; + } +} diff --git a/src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php b/src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php new file mode 100644 index 0000000..70b7df4 --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; + +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum AuthTokenValidatorEnum implements ValidatorEnumInterface +{ + case token; + + public function validators(): array + { + return [PresenceOf::class]; + } +} diff --git a/src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php b/src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php new file mode 100644 index 0000000..f9a11f7 --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; + +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum UserInsertEnum implements ValidatorEnumInterface +{ + case email; + case password; + case issuer; + case tokenPassword; + case tokenId; + + public function validators(): array + { + return match ($this) { + self::email => [ + PresenceOf::class, + Email::class + ], + default => [PresenceOf::class], + }; + } +} diff --git a/src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php b/src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php new file mode 100644 index 0000000..31ee31f --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum UserUpdateEnum implements ValidatorEnumInterface +{ + case id; + case email; + case password; + case issuer; + case tokenPassword; + case tokenId; + + public function validators(): array + { + return match ($this) { + self::id => [ + PresenceOf::class, + AbsInt::class + ], + self::email => [ + PresenceOf::class, + Email::class + ], + default => [PresenceOf::class], + }; + } +} diff --git a/src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php b/src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php new file mode 100644 index 0000000..3821a33 --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; + +use UnitEnum; + +interface ValidatorEnumInterface extends UnitEnum +{ + /** + * @return array + */ + public function validators(): array; +} diff --git a/src/Domain/Components/Env/Adapters/AdapterInterface.php b/src/Domain/Infrastructure/Env/Adapters/AdapterInterface.php similarity index 82% rename from src/Domain/Components/Env/Adapters/AdapterInterface.php rename to src/Domain/Infrastructure/Env/Adapters/AdapterInterface.php index bab4b30..f9ed7ff 100644 --- a/src/Domain/Components/Env/Adapters/AdapterInterface.php +++ b/src/Domain/Infrastructure/Env/Adapters/AdapterInterface.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env\Adapters; +namespace Phalcon\Api\Domain\Infrastructure\Env\Adapters; -use Phalcon\Api\Domain\Components\Env\EnvManagerTypes; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManagerTypes; /** * @phpstan-import-type TDotEnvOptions from EnvManagerTypes diff --git a/src/Domain/Components/Env/Adapters/DotEnv.php b/src/Domain/Infrastructure/Env/Adapters/DotEnv.php similarity index 85% rename from src/Domain/Components/Env/Adapters/DotEnv.php rename to src/Domain/Infrastructure/Env/Adapters/DotEnv.php index 0133a82..c349bcc 100644 --- a/src/Domain/Components/Env/Adapters/DotEnv.php +++ b/src/Domain/Infrastructure/Env/Adapters/DotEnv.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env\Adapters; +namespace Phalcon\Api\Domain\Infrastructure\Env\Adapters; use Dotenv\Dotenv as ParentDotEnv; use Exception; -use Phalcon\Api\Domain\Components\Env\EnvManagerTypes; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManagerTypes; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; /** * @phpstan-import-type TDotEnvOptions from EnvManagerTypes diff --git a/src/Domain/Components/Env/EnvFactory.php b/src/Domain/Infrastructure/Env/EnvFactory.php similarity index 82% rename from src/Domain/Components/Env/EnvFactory.php rename to src/Domain/Infrastructure/Env/EnvFactory.php index 4ce3ba1..f6044a6 100644 --- a/src/Domain/Components/Env/EnvFactory.php +++ b/src/Domain/Infrastructure/Env/EnvFactory.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env; +namespace Phalcon\Api\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Env\Adapters\AdapterInterface; -use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\AdapterInterface; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; class EnvFactory { diff --git a/src/Domain/Components/Env/EnvManager.php b/src/Domain/Infrastructure/Env/EnvManager.php similarity index 96% rename from src/Domain/Components/Env/EnvManager.php rename to src/Domain/Infrastructure/Env/EnvManager.php index 94fd405..4a2d214 100644 --- a/src/Domain/Components/Env/EnvManager.php +++ b/src/Domain/Infrastructure/Env/EnvManager.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env; +namespace Phalcon\Api\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; use Phalcon\Support\Collection; use function array_merge; diff --git a/src/Domain/Components/Env/EnvManagerTypes.php b/src/Domain/Infrastructure/Env/EnvManagerTypes.php similarity index 90% rename from src/Domain/Components/Env/EnvManagerTypes.php rename to src/Domain/Infrastructure/Env/EnvManagerTypes.php index 3acf713..b7347e0 100644 --- a/src/Domain/Components/Env/EnvManagerTypes.php +++ b/src/Domain/Infrastructure/Env/EnvManagerTypes.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env; +namespace Phalcon\Api\Domain\Infrastructure\Env; /** * @phpstan-type TDotEnvOptions array{ diff --git a/src/Domain/Components/Exceptions/ExceptionTrait.php b/src/Domain/Infrastructure/Exceptions/ExceptionTrait.php similarity index 89% rename from src/Domain/Components/Exceptions/ExceptionTrait.php rename to src/Domain/Infrastructure/Exceptions/ExceptionTrait.php index 1323f88..6a39f51 100644 --- a/src/Domain/Components/Exceptions/ExceptionTrait.php +++ b/src/Domain/Infrastructure/Exceptions/ExceptionTrait.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Exceptions; +namespace Phalcon\Api\Domain\Infrastructure\Exceptions; trait ExceptionTrait { diff --git a/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php b/src/Domain/Infrastructure/Exceptions/InvalidConfigurationArgumentException.php similarity index 87% rename from src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php rename to src/Domain/Infrastructure/Exceptions/InvalidConfigurationArgumentException.php index fe2f4a0..9ecf809 100644 --- a/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php +++ b/src/Domain/Infrastructure/Exceptions/InvalidConfigurationArgumentException.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Exceptions; +namespace Phalcon\Api\Domain\Infrastructure\Exceptions; use InvalidArgumentException; diff --git a/src/Domain/Components/Exceptions/TokenValidationException.php b/src/Domain/Infrastructure/Exceptions/TokenValidationException.php similarity index 87% rename from src/Domain/Components/Exceptions/TokenValidationException.php rename to src/Domain/Infrastructure/Exceptions/TokenValidationException.php index 1acb086..08e5284 100644 --- a/src/Domain/Components/Exceptions/TokenValidationException.php +++ b/src/Domain/Infrastructure/Exceptions/TokenValidationException.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Exceptions; +namespace Phalcon\Api\Domain\Infrastructure\Exceptions; use InvalidArgumentException; diff --git a/src/Domain/Components/Middleware/AbstractMiddleware.php b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php similarity index 90% rename from src/Domain/Components/Middleware/AbstractMiddleware.php rename to src/Domain/Infrastructure/Middleware/AbstractMiddleware.php index 6dfcee8..3fed350 100644 --- a/src/Domain/Components/Middleware/AbstractMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php @@ -11,14 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Responder\ResponderTypes; -use Phalcon\Domain\Payload; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\RequestInterface; use Phalcon\Http\Response\Exception as ResponseException; @@ -100,7 +99,7 @@ protected function halt( 'errors' => $errors, ]; - $payload = new Payload(DomainStatus::SUCCESS, $results); + $payload = Payload::success($results); $responder->__invoke($response, $payload); } diff --git a/src/Domain/Components/Middleware/HealthMiddleware.php b/src/Domain/Infrastructure/Middleware/HealthMiddleware.php similarity index 87% rename from src/Domain/Components/Middleware/HealthMiddleware.php rename to src/Domain/Infrastructure/Middleware/HealthMiddleware.php index 6e01eea..9093fdd 100644 --- a/src/Domain/Components/Middleware/HealthMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/HealthMiddleware.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/NotFoundMiddleware.php b/src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php similarity index 90% rename from src/Domain/Components/Middleware/NotFoundMiddleware.php rename to src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php index 47293c2..76a31d8 100644 --- a/src/Domain/Components/Middleware/NotFoundMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Events\Event; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php similarity index 72% rename from src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php index f659890..3dfb26e 100644 --- a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php @@ -11,17 +11,17 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; /** * Validates the token claims @@ -39,20 +39,20 @@ public function call(Micro $application): bool { /** @var JWTToken $jwtToken */ $jwtToken = $application->getSharedService(Container::JWT_TOKEN); - /** @var TransportRepository $transport */ - $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); /** * Get the token object */ /** @var Token $tokenObject */ - $tokenObject = $transport->getSessionToken(); + $tokenObject = $registry->get('token'); /** * Our used is in the transport, so we can get it without a * database call */ - /** @var UserTransport $sessionUser */ - $sessionUser = $transport->getSessionUser(); + /** @var User $sessionUser */ + $sessionUser = $registry->get('user'); /** * This is where we validate everything. Even though the user @@ -60,6 +60,7 @@ public function call(Micro $application): bool * claims of the token, we will still check the claims against the * user stored in the session */ + /** @var array $errors */ $errors = $jwtToken->validate($tokenObject, $sessionUser); if (true !== empty($errors)) { diff --git a/src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php similarity index 84% rename from src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php index dbe48ad..7c08858 100644 --- a/src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php similarity index 64% rename from src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php index 81fddca..cd93845 100644 --- a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php @@ -11,16 +11,17 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Http\RequestInterface; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; +use Psr\SimpleCache\CacheInterface; final class ValidateTokenRevokedMiddleware extends AbstractMiddleware { @@ -33,21 +34,21 @@ public function call(Micro $application): bool { /** @var RequestInterface $request */ $request = $application->getSharedService(Container::REQUEST); - /** @var Cache $cache */ + /** @var CacheInterface $cache */ $cache = $application->getSharedService(Container::CACHE); /** @var EnvManager $env */ $env = $application->getSharedService(Container::ENV); - /** @var TransportRepository $userTransport */ - $userTransport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); - /** @var UserTransport $user */ - $user = $userTransport->getSessionUser(); + /** @var User $domainUser */ + $domainUser = $registry->get('user'); /** * Get the token object */ $token = $this->getBearerTokenFromHeader($request, $env); - $cacheKey = $cache->getCacheTokenKey($user, $token); + $cacheKey = CacheConstants::getCacheTokenKey($domainUser, $token); $exists = $cache->has($cacheKey); if (true !== $exists) { diff --git a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php similarity index 73% rename from src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php index d69eee0..dc1535a 100644 --- a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php @@ -11,18 +11,18 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; final class ValidateTokenStructureMiddleware extends AbstractMiddleware { @@ -63,9 +63,9 @@ public function call(Micro $application): bool /** * If we are down here the token is an object and is valid */ - /** @var TransportRepository $transport */ - $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); - $transport->setSessionToken($token); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); + $registry->set('token', $token); return true; } diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php similarity index 57% rename from src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php index 9720a05..4dc4ec1 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php @@ -11,22 +11,19 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TUserRecord from UserTypes */ final class ValidateTokenUserMiddleware extends AbstractMiddleware { @@ -41,20 +38,19 @@ public function call(Micro $application): bool { /** @var JWTToken $jwtToken */ $jwtToken = $application->getSharedService(Container::JWT_TOKEN); - /** @var QueryRepository $repository */ - $repository = $application->getSharedService(Container::REPOSITORY); - /** @var TransportRepository $transport */ - $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + /** @var UserRepository $repository */ + $repository = $application->getSharedService(Container::USER_REPOSITORY); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); /** * Get the token object */ /** @var Token $tokenObject */ - $tokenObject = $transport->getSessionToken(); - /** @var TUserRecord $dbUser */ - $dbUser = $jwtToken->getUser($repository, $tokenObject); + $tokenObject = $registry->get('token'); + $domainUser = $jwtToken->getUser($repository, $tokenObject); - if (true === empty($dbUser)) { + if (null === $domainUser) { $this->halt( $application, HttpCodesEnum::Unauthorized->value, @@ -68,10 +64,9 @@ public function call(Micro $application): bool /** * If we are here everything is fine and we need to keep the user - * as a "session" user in the transport + * as a "session" user in the registry */ - /** @var TUserDbRecord $dbUser */ - $transport->setSessionUser($dbUser); + $registry->set('user', $domainUser); return true; } diff --git a/src/Domain/Components/Providers/ErrorHandlerProvider.php b/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php similarity index 94% rename from src/Domain/Components/Providers/ErrorHandlerProvider.php rename to src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php index 7cf3b31..1dd1ffa 100644 --- a/src/Domain/Components/Providers/ErrorHandlerProvider.php +++ b/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Providers; +namespace Phalcon\Api\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Logger\Logger; diff --git a/src/Domain/Components/Providers/RouterProvider.php b/src/Domain/Infrastructure/Providers/RouterProvider.php similarity index 95% rename from src/Domain/Components/Providers/RouterProvider.php rename to src/Domain/Infrastructure/Providers/RouterProvider.php index c17da85..2d184ae 100644 --- a/src/Domain/Components/Providers/RouterProvider.php +++ b/src/Domain/Infrastructure/Providers/RouterProvider.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Providers; +namespace Phalcon\Api\Domain\Infrastructure\Providers; use Phalcon\Api\Action\ActionHandler; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php index 3094540..8e4dd98 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -13,49 +13,15 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Domain\Payload; -use Phalcon\Filter\Filter; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades\AuthFacade; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; -/** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TLoginInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes - */ abstract class AbstractAuthService implements DomainInterface { public function __construct( - protected readonly QueryRepository $repository, - protected readonly TransportRepository $transport, - protected readonly Cache $cache, - protected readonly EnvManager $env, - protected readonly JWTToken $jwtToken, - protected readonly Filter $filter, - protected readonly Security $security, + protected readonly AuthFacade $facade, + protected readonly ValidatorInterface $validator ) { } - - /** - * @param TValidationErrors $errors - * - * @return Payload - */ - protected function getUnauthorizedPayload(array $errors): Payload - { - return new Payload( - DomainStatus::UNAUTHORIZED, - [ - 'errors' => $errors, - ] - ); - } } diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php index d6f9a4f..34b29ec 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -13,88 +13,21 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TAuthLoginInput from InputTypes */ final class LoginPostService extends AbstractAuthService { /** - * @param TLoginInput $input + * @param TAuthLoginInput $input * * @return Payload */ public function __invoke(array $input): Payload { - /** - * Get email and password from the input and sanitize them - */ - $email = (string)($input['email'] ?? ''); - $password = (string)($input['password'] ?? ''); - $email = $this->filter->string($email); - $password = $this->filter->string($password); - - /** - * Check if email or password are empty - */ - if (true === empty($email) || true === empty($password)) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppIncorrectCredentials->error()] - ); - } - - /** - * Find the user in the database - */ - $dbUser = $this->repository->user()->findByEmail($email); - $dbUserId = (int)($dbUser['usr_id'] ?? 0); - $dbPassword = $dbUser['usr_password'] ?? ''; - - /** - * Check if the user exists and if the password matches - */ - if ( - $dbUserId < 1 || - true !== $this->security->verify($password, $dbPassword) - ) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppIncorrectCredentials->error()] - ); - } - - /** - * Get a new token for this user - */ - /** @var TUserDbRecord $dbUser */ - $token = $this->jwtToken->getForUser($dbUser); - $refreshToken = $this->jwtToken->getRefreshForUser($dbUser); - $domainUser = $this->transport->newUser($dbUser); - $results = $this->transport->newLoginUser( - $domainUser, - $token, - $refreshToken - ); - - /** - * Store the token in cache - */ - $this->cache->storeTokenInCache($this->env, $domainUser, $token); - $this->cache->storeTokenInCache($this->env, $domainUser, $refreshToken); - - /** - * Send the payload back - */ - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => $results, - ] - ); + return $this->facade->authenticate($input, $this->validator); } } diff --git a/src/Domain/Services/Auth/LogoutPostService.php b/src/Domain/Services/Auth/LogoutPostService.php index 1bec8d1..2ac289b 100644 --- a/src/Domain/Services/Auth/LogoutPostService.php +++ b/src/Domain/Services/Auth/LogoutPostService.php @@ -13,96 +13,21 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TLogoutInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes + * @phpstan-import-type TAuthLogoutInput from InputTypes */ final class LogoutPostService extends AbstractAuthService { /** - * @param TLogoutInput $input + * @param TAuthLogoutInput $input * * @return Payload */ public function __invoke(array $input): Payload { - /** - * @todo common code with refresh - */ - /** - * Get the token - */ - $token = (string)($input['token'] ?? ''); - $token = $this->filter->string($token); - - /** - * Validation - * - * Empty token - */ - if (true === empty($token)) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppTokenNotPresent->error()] - ); - } - - /** - * @todo catch any exceptions here - * - * Is this the refresh token - */ - $tokenObject = $this->jwtToken->getObject($token); - $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); - if (false === $isRefresh) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppTokenNotValid->error()] - ); - } - - /** - * Get the user - if empty return error - */ - $user = $this - ->jwtToken - ->getUser($this->repository, $tokenObject) - ; - if (true === empty($user)) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppTokenInvalidUser->error()] - ); - } - - $domainUser = $this->transport->newUser($user); - - /** @var TValidationErrors $errors */ - $errors = $this->jwtToken->validate($tokenObject, $domainUser); - if (true !== empty($errors)) { - return $this->getUnauthorizedPayload($errors); - } - - /** - * Invalidate old tokens - */ - $this->cache->invalidateForUser($this->env, $domainUser); - - /** - * Send the payload back - */ - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => [ - 'authenticated' => false, - ], - ] - ); + return $this->facade->logout($input, $this->validator); } } diff --git a/src/Domain/Services/Auth/RefreshPostService.php b/src/Domain/Services/Auth/RefreshPostService.php index 2431936..7d50910 100644 --- a/src/Domain/Services/Auth/RefreshPostService.php +++ b/src/Domain/Services/Auth/RefreshPostService.php @@ -13,108 +13,21 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TRefreshInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes + * @phpstan-import-type TAuthRefreshInput from InputTypes */ final class RefreshPostService extends AbstractAuthService { /** - * @param TRefreshInput $input + * @param TAuthRefreshInput $input * * @return Payload */ public function __invoke(array $input): Payload { - /** - * Get email and password from the input and sanitize them - */ - $token = (string)($input['token'] ?? ''); - $token = $this->filter->string($token); - - /** - * Validation - * - * Empty token - */ - if (true === empty($token)) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppTokenNotPresent->error()] - ); - } - - /** - * @todo catch any exceptions here - * - * Is this the refresh token - */ - $tokenObject = $this->jwtToken->getObject($token); - $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); - if (false === $isRefresh) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppTokenNotValid->error()] - ); - } - - /** - * Get the user - if empty return error - */ - $user = $this - ->jwtToken - ->getUser($this->repository, $tokenObject) - ; - if (true === empty($user)) { - return $this->getUnauthorizedPayload( - [HttpCodesEnum::AppTokenInvalidUser->error()] - ); - } - - $domainUser = $this->transport->newUser($user); - - /** @var TValidationErrors $errors */ - $errors = $this->jwtToken->validate($tokenObject, $domainUser); - if (true !== empty($errors)) { - return $this->getUnauthorizedPayload($errors); - } - - /** - * @todo change this to be the domain user - */ - $userPayload = [ - 'usr_issuer' => $domainUser->getIssuer(), - 'usr_token_password' => $domainUser->getTokenPassword(), - 'usr_token_id' => $domainUser->getTokenId(), - 'usr_id' => $domainUser->getId(), - ]; - $newToken = $this->jwtToken->getForUser($userPayload); - $newRefreshToken = $this->jwtToken->getRefreshForUser($userPayload); - - /** - * Invalidate old tokens, store new tokens in cache - */ - $this->cache->invalidateForUser($this->env, $domainUser); - $this->cache->storeTokenInCache($this->env, $domainUser, $newToken); - $this->cache->storeTokenInCache($this->env, $domainUser, $newRefreshToken); - - /** - * Send the payload back - */ - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => [ - 'token' => $newToken, - 'refreshToken' => $newRefreshToken, - ], - ] - ); + return $this->facade->refresh($input, $this->validator); } } diff --git a/src/Domain/Services/User/AbstractUserService.php b/src/Domain/Services/User/AbstractUserService.php index f6d5c84..347ef53 100644 --- a/src/Domain/Services/User/AbstractUserService.php +++ b/src/Domain/Services/User/AbstractUserService.php @@ -14,18 +14,15 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Filter\Filter; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades\UserFacade; abstract class AbstractUserService implements DomainInterface { + /** + * @param UserFacade $facade + */ public function __construct( - protected readonly QueryRepository $repository, - protected readonly TransportRepository $transport, - protected readonly Filter $filter, - protected readonly Security $security, + protected readonly UserFacade $facade, ) { } } diff --git a/src/Domain/Services/User/UserDeleteService.php b/src/Domain/Services/User/UserDeleteService.php index 77b45bd..4ed07d0 100644 --- a/src/Domain/Services/User/UserDeleteService.php +++ b/src/Domain/Services/User/UserDeleteService.php @@ -13,9 +13,8 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TUserInput from InputTypes @@ -29,36 +28,6 @@ final class UserDeleteService extends AbstractUserService */ public function __invoke(array $input): Payload { - $userId = $this->filter->absint($input['id'] ?? 0); - - /** - * Success - */ - if ($userId > 0) { - $rows = $this->repository->user()->deleteById($userId); - - if ($rows > 0) { - return new Payload( - DomainStatus::DELETED, - [ - 'data' => [ - 'Record deleted successfully [#' . $userId . '].', - ], - ] - ); - } - } - - /** - * 404 - */ - return new Payload( - DomainStatus::NOT_FOUND, - [ - 'errors' => [ - 'Record(s) not found', - ], - ] - ); + return $this->facade->delete($input); } } diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php index c71735b..433ea29 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -13,9 +13,8 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TUserInput from InputTypes @@ -29,35 +28,6 @@ final class UserGetService extends AbstractUserService */ public function __invoke(array $input): Payload { - $userId = $this->filter->absint($input['id'] ?? 0); - - /** - * Success - */ - if ($userId > 0) { - $dbUser = $this->repository->user()->findById($userId); - $user = $this->transport->newUser($dbUser); - - if (true !== $user->isEmpty()) { - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => $user->toArray(), - ] - ); - } - } - - /** - * 404 - */ - return new Payload( - DomainStatus::NOT_FOUND, - [ - 'errors' => [ - 'Record(s) not found', - ], - ] - ); + return $this->facade->get($input); } } diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php index 499cc04..91928c0 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Services/User/UserPostService.php @@ -13,17 +13,11 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; -use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** - * @phpstan-import-type TUserSanitizedInsertInput from InputTypes * @phpstan-import-type TUserInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes */ final class UserPostService extends AbstractUserService { @@ -34,150 +28,6 @@ final class UserPostService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputData = $this->sanitizeInput($input); - $errors = $this->validateInput($inputData); - - /** - * Errors exist - return early - */ - if (!empty($errors)) { - return new Payload( - DomainStatus::INVALID, - [ - 'errors' => $errors, - ] - ); - } - - /** - * The password needs to be hashed - */ - $password = $inputData['password']; - $hashed = $this->security->hash($password); - - $inputData['password'] = $hashed; - - /** - * Insert the record - */ - try { - $userId = $this - ->repository - ->user() - ->insert($inputData) - ; - } catch (PDOException $ex) { - /** - * @todo send generic response and log the error - */ - return $this->getErrorPayload($ex->getMessage()); - } - - if ($userId < 1) { - return $this->getErrorPayload('No id returned'); - } - - /** - * Get the user from the database - */ - $dbUser = $this->repository->user()->findById($userId); - $domainUser = $this->transport->newUser($dbUser); - - /** - * Return the user back - */ - return new Payload( - DomainStatus::CREATED, - [ - 'data' => $domainUser->toArray(), - ] - ); - } - - /** - * @param string $message - * - * @return Payload - */ - private function getErrorPayload(string $message): Payload - { - return new Payload( - DomainStatus::ERROR, - [ - 'errors' => [ - HttpCodesEnum::AppCannotCreateDatabaseRecord->text() - . $message, - ], - ] - ); - } - - /** - * @param TUserInput $input - * - * @return TUserSanitizedInsertInput - */ - private function sanitizeInput(array $input): array - { - /** - * Only the fields we want - * - * @todo add sanitizers here - * @todo maybe this is another domain object? - */ - $sanitized = [ - 'status' => $input['status'] ?? 0, - 'email' => $input['email'] ?? '', - 'password' => $input['password'] ?? '', - 'namePrefix' => $input['namePrefix'] ?? '', - 'nameFirst' => $input['nameFirst'] ?? '', - 'nameLast' => $input['nameLast'] ?? '', - 'nameMiddle' => $input['nameMiddle'] ?? '', - 'nameSuffix' => $input['nameSuffix'] ?? '', - 'issuer' => $input['issuer'] ?? '', - 'tokenPassword' => $input['tokenPassword'] ?? '', - 'tokenId' => $input['tokenId'] ?? '', - 'preferences' => $input['preferences'] ?? '', - 'createdDate' => $input['createdDate'] ?? null, - 'createdUserId' => $input['createdUserId'] ?? 0, - 'updatedDate' => $input['updatedDate'] ?? null, - 'updatedUserId' => $input['updatedUserId'] ?? 0, - ]; - - if (empty($sanitized['createdDate'])) { - $sanitized['createdDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); - } - - if (empty($sanitized['updatedDate'])) { - $sanitized['updatedDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); - } - - return $sanitized; - } - - /** - * @param TUserSanitizedInsertInput $inputData - * - * @return TValidationErrors|array{} - */ - private function validateInput(array $inputData): array - { - $errors = []; - $required = [ - 'email', - 'password', - 'issuer', - 'tokenPassword', - 'tokenId', - ]; - - foreach ($required as $name) { - $field = $inputData[$name]; - if (true === empty($field)) { - $errors[] = ['Field ' . $name . ' cannot be empty.']; - } - } - - return $errors; + return $this->facade->insert($input); } } diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index 22ad499..cb74dd7 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -13,17 +13,11 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; -use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** - * @phpstan-import-type TUserSanitizedUpdateInput from InputTypes * @phpstan-import-type TUserInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes */ final class UserPutService extends AbstractUserService { @@ -34,149 +28,6 @@ final class UserPutService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputData = $this->sanitizeInput($input); - $errors = $this->validateInput($inputData); - - /** - * Errors exist - return early - */ - if (!empty($errors)) { - return new Payload( - DomainStatus::INVALID, - [ - 'errors' => $errors, - ] - ); - } - - /** - * The password needs to be hashed - */ - $password = $inputData['password']; - $hashed = $this->security->hash($password); - - $inputData['password'] = $hashed; - - /** - * Update the record - */ - /** - * @todo get the user from the database to make sure that it is valid - */ - - try { - $userId = $this - ->repository - ->user() - ->update($inputData) - ; - } catch (PDOException $ex) { - /** - * @todo send generic response and log the error - */ - return $this->getErrorPayload($ex->getMessage()); - } - - if ($userId < 1) { - return $this->getErrorPayload('No id returned'); - } - - /** - * Get the user from the database - */ - $dbUser = $this->repository->user()->findById($userId); - $domainUser = $this->transport->newUser($dbUser); - - /** - * Return the user back - */ - return new Payload( - DomainStatus::UPDATED, - [ - 'data' => $domainUser->toArray(), - ] - ); - } - - /** - * @param string $message - * - * @return Payload - */ - private function getErrorPayload(string $message): Payload - { - return new Payload( - DomainStatus::ERROR, - [ - 'errors' => [ - HttpCodesEnum::AppCannotUpdateDatabaseRecord->text() - . $message, - ], - ] - ); - } - - /** - * @param TUserInput $input - * - * @return TUserSanitizedUpdateInput - */ - private function sanitizeInput(array $input): array - { - /** - * Only the fields we want - * - * @todo add sanitizers here - * @todo maybe this is another domain object? - */ - $sanitized = [ - 'id' => $input['id'] ?? 0, - 'status' => $input['status'] ?? 0, - 'email' => $input['email'] ?? '', - 'password' => $input['password'] ?? '', - 'namePrefix' => $input['namePrefix'] ?? '', - 'nameFirst' => $input['nameFirst'] ?? '', - 'nameLast' => $input['nameLast'] ?? '', - 'nameMiddle' => $input['nameMiddle'] ?? '', - 'nameSuffix' => $input['nameSuffix'] ?? '', - 'issuer' => $input['issuer'] ?? '', - 'tokenPassword' => $input['tokenPassword'] ?? '', - 'tokenId' => $input['tokenId'] ?? '', - 'preferences' => $input['preferences'] ?? '', - 'updatedDate' => $input['updatedDate'] ?? null, - 'updatedUserId' => $input['updatedUserId'] ?? 0, - ]; - - if (empty($sanitized['updatedDate'])) { - $sanitized['updatedDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); - } - - return $sanitized; - } - - /** - * @param TUserSanitizedUpdateInput $inputData - * - * @return TValidationErrors|array{} - */ - private function validateInput(array $inputData): array - { - $errors = []; - $required = [ - 'email', - 'password', - 'issuer', - 'tokenPassword', - 'tokenId', - ]; - - foreach ($required as $name) { - $field = $inputData[$name]; - if (true === empty($field)) { - $errors[] = ['Field ' . $name . ' cannot be empty.']; - } - } - - return $errors; + return $this->facade->update($input); } } diff --git a/src/Responder/JsonResponder.php b/src/Responder/JsonResponder.php index 36ea2ff..066cae1 100644 --- a/src/Responder/JsonResponder.php +++ b/src/Responder/JsonResponder.php @@ -14,9 +14,9 @@ namespace Phalcon\Api\Responder; use Exception as BaseException; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Http\ResponseInterface; use function sha1; diff --git a/src/Responder/ResponderInterface.php b/src/Responder/ResponderInterface.php index 7b5342a..dfaceb2 100644 --- a/src/Responder/ResponderInterface.php +++ b/src/Responder/ResponderInterface.php @@ -13,7 +13,7 @@ namespace Phalcon\Api\Responder; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Http\ResponseInterface; interface ResponderInterface diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 23fbede..381d2ee 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -16,10 +16,10 @@ use DateTimeImmutable; use Faker\Factory; use PDO; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Encryption\Security\JWT\Builder; diff --git a/tests/Fixtures/Domain/Services/ServiceFixture.php b/tests/Fixtures/Domain/Services/ServiceFixture.php index 8e04a05..c47933b 100644 --- a/tests/Fixtures/Domain/Services/ServiceFixture.php +++ b/tests/Fixtures/Domain/Services/ServiceFixture.php @@ -13,19 +13,13 @@ namespace Phalcon\Api\Tests\Fixtures\Domain\Services; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; final readonly class ServiceFixture implements DomainInterface { public function __invoke(array $input): Payload { - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => $input, - ] - ); + return Payload::success($input); } } diff --git a/tests/Unit/Action/ActionHandlerTest.php b/tests/Unit/Action/ActionHandlerTest.php index 7ba4ae9..a572428 100644 --- a/tests/Unit/Action/ActionHandlerTest.php +++ b/tests/Unit/Action/ActionHandlerTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Action; use Phalcon\Api\Action\ActionHandler; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Services\ServiceFixture; @@ -39,8 +39,8 @@ public function testInvoke(): void $responder = $this->container->getShared(Container::RESPONDER_JSON); $getData = [ - 'key' => uniqid('key-'), - 'data' => [ + 'key' => uniqid('key-'), + 'value' => [ uniqid('data-'), ], ]; diff --git a/tests/Unit/Domain/ADR/InputTest.php b/tests/Unit/Domain/ADR/InputTest.php new file mode 100644 index 0000000..c82ecb3 --- /dev/null +++ b/tests/Unit/Domain/ADR/InputTest.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\ADR; + +use Faker\Factory; +use Phalcon\Api\Domain\ADR\Input; +use Phalcon\Http\Request; +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\TestCase; + +#[BackupGlobals(true)] +final class InputTest extends TestCase +{ + public function testInvoke(): void + { + $faker = Factory::create(); + $postData = [ + 'post1' => $faker->word(), + 'post2' => $faker->word(), + ]; + $getData = [ + 'get1' => $faker->word(), + 'get2' => $faker->word(), + ]; + $putData = [ + 'put1' => $faker->word(), + 'put2' => $faker->word(), + ]; + + $mockRequest = $this + ->getMockBuilder(Request::class) + ->onlyMethods( + [ + 'getQuery', + 'getPost', + 'getPut', + ] + ) + ->getMock() + ; + $mockRequest->method('getQuery')->willReturn($getData); + $mockRequest->method('getPost')->willReturn($postData); + $mockRequest->method('getPut')->willReturn($putData); + + $input = new Input(); + + $expected = [ + 'get1' => $getData['get1'], + 'get2' => $getData['get2'], + 'post1' => $postData['post1'], + 'post2' => $postData['post2'], + 'put1' => $putData['put1'], + 'put2' => $putData['put2'], + ]; + $actual = $input->__invoke($mockRequest); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/ADR/PayloadTest.php b/tests/Unit/Domain/ADR/PayloadTest.php new file mode 100644 index 0000000..4ec98c7 --- /dev/null +++ b/tests/Unit/Domain/ADR/PayloadTest.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\ADR; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class PayloadTest extends AbstractUnitTestCase +{ + public function testCreated(): void + { + $data = ['id' => 1]; + $payload = Payload::created($data); + + $expected = Payload::class; + $actual = get_class($payload); + $this->assertSame($expected, $actual); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testDeleted(): void + { + $data = ['deleted' => true]; + $payload = Payload::deleted($data); + + $expected = DomainStatus::DELETED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testUpdated(): void + { + $data = ['updated' => true]; + $payload = Payload::updated($data); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + $data = ['items' => [1, 2, 3]]; + $payload = Payload::success($data); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testError(): void + { + $errors = [['something' => 'went wrong']]; + $payload = Payload::error($errors); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['errors' => $errors]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testInvalid(): void + { + $errors = [['field' => 'invalid']]; + $payload = Payload::invalid($errors); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['errors' => $errors]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testUnauthorized(): void + { + $errors = [['reason' => 'no access']]; + $payload = Payload::unauthorized($errors); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = [ + 'code' => HttpCodesEnum::Unauthorized->value, + 'message' => HttpCodesEnum::Unauthorized->text(), + 'data' => [], + 'errors' => $errors + ]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testNotFound(): void + { + $payload = Payload::notFound(); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = [ + 'code' => HttpCodesEnum::NotFound->value, + 'message' => HttpCodesEnum::NotFound->text(), + 'data' => [], + 'errors' => [['Record(s) not found']] + ]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php deleted file mode 100644 index 90b6120..0000000 --- a/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php +++ /dev/null @@ -1,139 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource; - -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Tests\AbstractUnitTestCase; - -final class TransportRepositoryTest extends AbstractUnitTestCase -{ - public function testNewUser(): void - { - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $user = $transport->newUser([]); - $actual = $user->isEmpty(); - $this->assertTrue($actual); - - $dbUser = $this->getNewUserData(); - $user = $transport->newUser($dbUser); - - $fullName = trim( - $dbUser['usr_name_last'] - . ', ' - . $dbUser['usr_name_first'] - . ' ' - . $dbUser['usr_name_middle'] - ); - - $expected = [ - $dbUser['usr_id'] => [ - 'id' => $dbUser['usr_id'], - 'status' => $dbUser['usr_status_flag'], - 'email' => $dbUser['usr_email'], - 'password' => $dbUser['usr_password'], - 'namePrefix' => $dbUser['usr_name_prefix'], - 'nameFirst' => $dbUser['usr_name_first'], - 'nameMiddle' => $dbUser['usr_name_middle'], - 'nameLast' => $dbUser['usr_name_last'], - 'nameSuffix' => $dbUser['usr_name_suffix'], - 'issuer' => $dbUser['usr_issuer'], - 'tokenPassword' => $dbUser['usr_token_password'], - 'tokenId' => $dbUser['usr_token_id'], - 'preferences' => $dbUser['usr_preferences'], - 'createdDate' => $dbUser['usr_created_date'], - 'createdUserId' => $dbUser['usr_created_usr_id'], - 'updatedDate' => $dbUser['usr_updated_date'], - 'updatedUserId' => $dbUser['usr_updated_usr_id'], - 'fullName' => $fullName, - ], - ]; - $actual = $user->toArray(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_id']; - $actual = $user->getId(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_status_flag']; - $actual = $user->getStatus(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_email']; - $actual = $user->getEmail(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_password']; - $actual = $user->getPassword(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_prefix']; - $actual = $user->getNamePrefix(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_first']; - $actual = $user->getNameFirst(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_middle']; - $actual = $user->getNameMiddle(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_last']; - $actual = $user->getNameLast(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_suffix']; - $actual = $user->getNameSuffix(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_issuer']; - $actual = $user->getIssuer(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_token_password']; - $actual = $user->getTokenPassword(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_token_id']; - $actual = $user->getTokenId(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_preferences']; - $actual = $user->getPreferences(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_created_date']; - $actual = $user->getCreatedDate(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_created_usr_id']; - $actual = $user->getCreatedUserId(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_updated_date']; - $actual = $user->getUpdatedDate(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_updated_usr_id']; - $actual = $user->getUpdatedUserId(); - $this->assertSame($expected, $actual); - - $expected = $fullName; - $actual = $user->getFullName(); - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php b/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php new file mode 100644 index 0000000..30d27b6 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Constants; + +use Phalcon\Api\Domain\Infrastructure\Constants\Cache; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function sha1; +use function uniqid; + +final class CacheTest extends AbstractUnitTestCase +{ + public function testConstants(): void + { + $expected = 86400; + $actual = Cache::CACHE_LIFETIME_DAY; + $this->assertSame($expected, $actual); + + $expected = 3600; + $actual = Cache::CACHE_LIFETIME_HOUR; + $this->assertSame($expected, $actual); + + $expected = 60; + $actual = Cache::CACHE_LIFETIME_MINUTE; + $this->assertSame($expected, $actual); + + $expected = 2592000; + $actual = Cache::CACHE_LIFETIME_MONTH; + $this->assertSame($expected, $actual); + + $expected = 14400; + $actual = Cache::CACHE_TOKEN_EXPIRY; + $this->assertSame($expected, $actual); + + $token = uniqid('tok-'); + $shaToken = sha1($token); + + $newUser = $this->getNewUserData(); + $newUser['usr_id'] = 1; + $domainUser = new User( + $newUser['usr_id'], + $newUser['usr_status_flag'], + $newUser['usr_email'], + $newUser['usr_password'], + $newUser['usr_name_prefix'], + $newUser['usr_name_first'], + $newUser['usr_name_middle'], + $newUser['usr_name_last'], + $newUser['usr_name_suffix'], + $newUser['usr_issuer'], + $newUser['usr_token_password'], + $newUser['usr_token_id'], + $newUser['usr_preferences'], + $newUser['usr_created_date'], + $newUser['usr_created_usr_id'], + $newUser['usr_updated_date'], + $newUser['usr_updated_usr_id'] + ); + + $expected = 'tk-' . $domainUser->id . '-' . $shaToken; + $actual = Cache::getCacheTokenKey($domainUser, $token); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php b/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php new file mode 100644 index 0000000..2909f28 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Constants; + +use DateTimeImmutable; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class DatesTest extends AbstractUnitTestCase +{ + public function testConstants(): void + { + $expected = 'Y-m-d'; + $actual = Dates::DATE_FORMAT; + $this->assertSame($expected, $actual); + + $expected = 'NOW()'; + $actual = Dates::DATE_NOW; + $this->assertSame($expected, $actual); + + $expected = 'Y-m-d H:i:s'; + $actual = Dates::DATE_TIME_FORMAT; + $this->assertSame($expected, $actual); + + $expected = 'Y-m-d\\TH:i:sp'; + $actual = Dates::DATE_TIME_UTC_FORMAT; + $this->assertSame($expected, $actual); + + $expected = 'UTC'; + $actual = Dates::DATE_TIME_ZONE; + $this->assertSame($expected, $actual); + + $now = new DateTimeImmutable(); + $expected = $now->format('Y-m-d'); + + $actual = Dates::toUTC(format: 'Y-m-d'); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Components/ContainerTest.php b/tests/Unit/Domain/Infrastructure/ContainerTest.php similarity index 91% rename from tests/Unit/Domain/Components/ContainerTest.php rename to tests/Unit/Domain/Infrastructure/ContainerTest.php index c40a728..643afe4 100644 --- a/tests/Unit/Domain/Components/ContainerTest.php +++ b/tests/Unit/Domain/Infrastructure/ContainerTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\Filter; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php new file mode 100644 index 0000000..14fa5bc --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\DTO; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class AuthInputTest extends AbstractUnitTestCase +{ + public function testObject(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + $faker = FakerFactory::create(); + + // Build an input with many fields present + $input = [ + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'token' => $faker->password(), + ]; + + $sanitized = $sanitizer->sanitize($input); + $authInput = AuthInput::new($sanitizer, $input); + + $expected = $sanitized['email']; + $actual = $authInput->email; + $this->assertSame($expected, $actual); + + $expected = $sanitized['password']; + $actual = $authInput->password; + $this->assertSame($expected, $actual); + + $expected = $sanitized['token']; + $actual = $authInput->token; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php new file mode 100644 index 0000000..3d0d693 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\Sanitizers; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class AuthSanitizerTest extends AbstractUnitTestCase +{ + public function testEmpty(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new AuthSanitizer($filter); + + $expected = [ + 'email' => null, + 'password' => null, + 'token' => null, + ]; + $actual = $sanitizer->sanitize([]); + $this->assertSame($expected, $actual); + } + + public function testObject(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new AuthSanitizer($filter); + + $userData = [ + 'email' => 'John.Doe (newsletter) +spam@example.COM', + 'password' => 'some ', + 'token' => 'some ', + ]; + + $expected = [ + 'email' => 'John.Doenewsletter+spam@example.COM', + 'password' => 'some ', + 'token' => 'some ', + ]; + $actual = $sanitizer->sanitize($userData); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php new file mode 100644 index 0000000..db79869 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\Validators; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthLoginValidator; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; + +final class AuthLoginValidatorTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $input = []; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthLoginValidator($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + HttpCodesEnum::AppIncorrectCredentials->error(), + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + $faker = FakerFactory::create(); + + $input = [ + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + ]; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthLoginValidator($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php new file mode 100644 index 0000000..333d74d --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\Validators; + +use Exception; +use Faker\Factory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthTokenValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCacheInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; + +final class AuthTokenValidatorTest extends AbstractUnitTestCase +{ + public function testFailureTokenNotPresent(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var TokenManager $tokenManager */ + $tokenManager = $this->container->get(Container::JWT_TOKEN_MANAGER); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $input = []; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthTokenValidator($tokenManager, $repository, $validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + HttpCodesEnum::AppTokenNotPresent->error(), + ]; + + $this->assertSame($expected, $actual); + } + + public function testFailureTokenNotValid(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $mockJWTToken = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject' + ] + ) + ->getMock() + ; + $mockJWTToken + ->method('getObject') + ->willThrowException(new Exception('error')) + ; + + $userData = $this->getNewUserData(); + $userData['usr_id'] = rand(1, 100); + $token = $this->getUserToken($userData); + + $tokenManager = new TokenManager($tokenCache, $env, $mockJWTToken); + + $input = [ + 'token' => $token, + ]; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthTokenValidator($tokenManager, $repository, $validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + HttpCodesEnum::AppTokenNotValid->error(), + ]; + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php new file mode 100644 index 0000000..d210cb9 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\DTO; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function json_encode; + +final class UserInputTest extends AbstractUnitTestCase +{ + public function testToArray(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + $faker = FakerFactory::create(); + + // Build an input with many fields present + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'namePrefix' => $faker->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => $faker->randomElement([null, $faker->suffix()]), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => json_encode(['k' => $faker->word()]), + 'createdDate' => $faker->date('Y-m-d'), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + 'updatedDate' => $faker->date('Y-m-d'), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + ]; + + $sanitized = $sanitizer->sanitize($input); + $userInput = UserInput::new($sanitizer, $input); + + $expected = $sanitized['id']; + $actual = $userInput->id; + $this->assertSame($expected, $actual); + + $expected = $sanitized['status']; + $actual = $userInput->status; + $this->assertSame($expected, $actual); + + $expected = $sanitized['email']; + $actual = $userInput->email; + $this->assertSame($expected, $actual); + + $expected = $sanitized['password']; + $actual = $userInput->password; + $this->assertSame($expected, $actual); + + $expected = $sanitized['namePrefix']; + $actual = $userInput->namePrefix; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameFirst']; + $actual = $userInput->nameFirst; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameMiddle']; + $actual = $userInput->nameMiddle; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameLast']; + $actual = $userInput->nameLast; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameSuffix']; + $actual = $userInput->nameSuffix; + $this->assertSame($expected, $actual); + + $expected = $sanitized['issuer']; + $actual = $userInput->issuer; + $this->assertSame($expected, $actual); + + $expected = $sanitized['tokenPassword']; + $actual = $userInput->tokenPassword; + $this->assertSame($expected, $actual); + + $expected = $sanitized['tokenId']; + $actual = $userInput->tokenId; + $this->assertSame($expected, $actual); + + $expected = $sanitized['preferences']; + $actual = $userInput->preferences; + $this->assertSame($expected, $actual); + + $expected = $sanitized['createdDate']; + $actual = $userInput->createdDate; + $this->assertSame($expected, $actual); + + $expected = $sanitized['createdUserId']; + $actual = $userInput->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $sanitized['updatedDate']; + $actual = $userInput->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $sanitized['updatedUserId']; + $actual = $userInput->updatedUserId; + $this->assertSame($expected, $actual); + + $expected = get_object_vars($userInput); + $actual = $userInput->toArray(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php new file mode 100644 index 0000000..198d47b --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\DTO; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function rand; + +final class UserTest extends AbstractUnitTestCase +{ + public function testObject(): void + { + $userData = $this->getNewUserData(); + $userData['usr_id'] = rand(1, 100); + + $user = new User( + $userData['usr_id'], + $userData['usr_status_flag'], + $userData['usr_email'], + $userData['usr_password'], + $userData['usr_name_prefix'], + $userData['usr_name_first'], + $userData['usr_name_middle'], + $userData['usr_name_last'], + $userData['usr_name_suffix'], + $userData['usr_issuer'], + $userData['usr_token_password'], + $userData['usr_token_id'], + $userData['usr_preferences'], + $userData['usr_created_date'], + $userData['usr_created_usr_id'], + $userData['usr_updated_date'], + $userData['usr_updated_usr_id'], + ); + + $expected = ($userData['usr_name_last'] ?? '') + . ', ' + . ($userData['usr_name_first'] ?? '') + . ' ' + . ($userData['usr_name_middle'] ?? ''); + $actual = $user->fullName(); + $this->assertSame($expected, $actual); + + $expected = [ + 'id' => $userData['usr_id'], + 'status' => $userData['usr_status_flag'], + 'email' => $userData['usr_email'], + 'password' => $userData['usr_password'], + 'namePrefix' => $userData['usr_name_prefix'], + 'nameFirst' => $userData['usr_name_first'], + 'nameMiddle' => $userData['usr_name_middle'], + 'nameLast' => $userData['usr_name_last'], + 'nameSuffix' => $userData['usr_name_suffix'], + 'issuer' => $userData['usr_issuer'], + 'tokenPassword' => $userData['usr_token_password'], + 'tokenId' => $userData['usr_token_id'], + 'preferences' => $userData['usr_preferences'], + 'createdDate' => $userData['usr_created_date'], + 'createdUserId' => $userData['usr_created_usr_id'], + 'updatedDate' => $userData['usr_updated_date'], + 'updatedUserId' => $userData['usr_updated_usr_id'], + ]; + $actual = $user->toArray(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php new file mode 100644 index 0000000..4a03d93 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Mappers; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function json_encode; + +final class UserMapperTest extends AbstractUnitTestCase +{ + public function testDb(): void + { + $faker = FakerFactory::create(); + + $preferences = [ + 'theme' => $faker->randomElement(['dark', 'light']), + 'lang' => $faker->languageCode(), + ]; + $preferencesJson = json_encode($preferences); + + $createdDate = $faker->date(Dates::DATE_TIME_FORMAT); + $updatedDate = $faker->date(Dates::DATE_TIME_FORMAT); + + $user = new UserInput( + $faker->numberBetween(1, 1000), + $faker->numberBetween(0, 9), + $faker->safeEmail(), + $faker->password(), + $faker->optional()->title(), + $faker->firstName(), + $faker->optional()->word(), + $faker->lastName(), + $faker->optional()->suffix(), + $faker->company(), + $faker->optional()->password(), + $faker->uuid(), + $preferencesJson, + $createdDate, + $faker->numberBetween(1, 1000), + $updatedDate, + $faker->numberBetween(1, 1000) + ); + + $mapper = new UserMapper(); + $row = $mapper->db($user); + + $expected = [ + 'usr_id' => $user->id, + 'usr_status_flag' => $user->status, + 'usr_email' => $user->email, + 'usr_password' => $user->password, + 'usr_name_prefix' => $user->namePrefix, + 'usr_name_first' => $user->nameFirst, + 'usr_name_middle' => $user->nameMiddle, + 'usr_name_last' => $user->nameLast, + 'usr_name_suffix' => $user->nameSuffix, + 'usr_issuer' => $user->issuer, + 'usr_token_password' => $user->tokenPassword, + 'usr_token_id' => $user->tokenId, + 'usr_preferences' => $user->preferences, + 'usr_created_date' => $user->createdDate, + 'usr_created_usr_id' => $user->createdUserId, + 'usr_updated_date' => $user->updatedDate, + 'usr_updated_usr_id' => $user->updatedUserId, + ]; + + $actual = $row; + $this->assertSame($expected, $actual); + } + + public function testDomain(): void + { + $faker = FakerFactory::create(); + $mapper = new UserMapper(); + + // Empty row: defaults should be applied + $emptyUser = $mapper->domain([]); + + $expected = 0; + $actual = $emptyUser->id; + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = $emptyUser->status; + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = $emptyUser->email; + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = $emptyUser->password; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->preferences; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->createdDate; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->createdUserId; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->updatedDate; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->updatedUserId; + $this->assertSame($expected, $actual); + + // Row with present created/updated user ids as strings should be cast to int + $row = [ + 'usr_id' => (string)$faker->numberBetween(1, 1000), + 'usr_status_flag' => (string)$faker->numberBetween(0, 9), + 'usr_email' => $faker->safeEmail(), + 'usr_created_usr_id' => (string)$faker->numberBetween(1, 1000), + 'usr_updated_usr_id' => (string)$faker->numberBetween(1, 1000), + ]; + + $user = $mapper->domain($row); + + $expected = (int)$row['usr_id']; + $actual = $user->id; + $this->assertSame($expected, $actual); + + $expected = (int)$row['usr_status_flag']; + $actual = $user->status; + $this->assertSame($expected, $actual); + + $expected = $row['usr_email']; + $actual = $user->email; + $this->assertSame($expected, $actual); + + $expected = (int)$row['usr_created_usr_id']; + $actual = $user->createdUserId; + $this->assertSame($expected, $actual); + + $expected = (int)$row['usr_updated_usr_id']; + $actual = $user->updatedUserId; + $this->assertSame($expected, $actual); + } + + public function testInput(): void + { + $faker = FakerFactory::create(); + $mapper = new UserMapper(); + + $preferences = ['k' => $faker->word()]; + $preferencesJson = json_encode($preferences); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'namePrefix' => $faker->optional()->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->optional()->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => null, + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->optional()->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => $preferencesJson, + 'createdDate' => $faker->date('Y-m-d'), + 'createdUserId' => $faker->numberBetween(1, 1000), + 'updatedDate' => $faker->date('Y-m-d'), + 'updatedUserId' => $faker->numberBetween(1, 1000), + ]; + + $user = $mapper->input($input); + + $expected = $input['id']; + $actual = $user->id; + $this->assertSame($expected, $actual); + + $expected = $input['status']; + $actual = $user->status; + $this->assertSame($expected, $actual); + + $expected = $input['email']; + $actual = $user->email; + $this->assertSame($expected, $actual); + + $expected = $input['password']; + $actual = $user->password; + $this->assertSame($expected, $actual); + + $expected = $input['namePrefix']; + $actual = $user->namePrefix; + $this->assertSame($expected, $actual); + + $expected = $input['nameFirst']; + $actual = $user->nameFirst; + $this->assertSame($expected, $actual); + + $expected = $input['nameMiddle']; + $actual = $user->nameMiddle; + $this->assertSame($expected, $actual); + + $expected = $input['nameLast']; + $actual = $user->nameLast; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $user->nameSuffix; + $this->assertSame($expected, $actual); + + $expected = $input['issuer']; + $actual = $user->issuer; + $this->assertSame($expected, $actual); + + $expected = $input['tokenPassword']; + $actual = $user->tokenPassword; + $this->assertSame($expected, $actual); + + $expected = $input['tokenId']; + $actual = $user->tokenId; + $this->assertSame($expected, $actual); + + $expected = $input['preferences']; + $actual = $user->preferences; + $this->assertSame($expected, $actual); + + $expected = $input['createdDate']; + $actual = $user->createdDate; + $this->assertSame($expected, $actual); + + $expected = $input['createdUserId']; + $actual = $user->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $input['updatedDate']; + $actual = $user->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $input['updatedUserId']; + $actual = $user->updatedUserId; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php similarity index 64% rename from tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php index 1443a5e..07d0070 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php @@ -11,10 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Repositories; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; @@ -22,108 +23,108 @@ final class UserRepositoryTest extends AbstractUnitTestCase { public function testFindByEmail(): void { - /** @var QueryRepository $repository */ - $repository = $this->container->get(Container::REPOSITORY); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); $migration = new UsersMigration($this->getConnection()); - $repositoryUser = $repository->user()->findByEmail(''); + $repositoryUser = $repository->findByEmail(''); $this->assertEmpty($repositoryUser); $migrationUser = $this->getNewUser($migration); $email = $migrationUser['usr_email']; - $repositoryUser = $repository->user()->findByEmail($email); + $repositoryUser = $repository->findByEmail($email); $this->runAssertions($migrationUser, $repositoryUser); } public function testFindById(): void { - /** @var QueryRepository $repository */ - $repository = $this->container->get(Container::REPOSITORY); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); $migration = new UsersMigration($this->getConnection()); - $repositoryUser = $repository->user()->findById(0); + $repositoryUser = $repository->findById(0); $this->assertEmpty($repositoryUser); $migrationUser = $this->getNewUser($migration); $userId = $migrationUser['usr_id']; - $repositoryUser = $repository->user()->findById($userId); + $repositoryUser = $repository->findById($userId); $this->runAssertions($migrationUser, $repositoryUser); } - private function runAssertions(array $dbUser, array $user): void + private function runAssertions(array $dbUser, User $user): void { $expected = $dbUser['usr_id']; - $actual = $user['usr_id']; + $actual = $user->id; $this->assertSame($expected, $actual); $expected = $dbUser['usr_status_flag']; - $actual = $user['usr_status_flag']; + $actual = $user->status; $this->assertSame($expected, $actual); $expected = $dbUser['usr_email']; - $actual = $user['usr_email']; + $actual = $user->email; $this->assertSame($expected, $actual); $expected = $dbUser['usr_password']; - $actual = $user['usr_password']; + $actual = $user->password; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_prefix']; - $actual = $user['usr_name_prefix']; + $actual = $user->namePrefix; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_first']; - $actual = $user['usr_name_first']; + $actual = $user->nameFirst; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_middle']; - $actual = $user['usr_name_middle']; + $actual = $user->nameMiddle; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_last']; - $actual = $user['usr_name_last']; + $actual = $user->nameLast; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_suffix']; - $actual = $user['usr_name_suffix']; + $actual = $user->nameSuffix; $this->assertSame($expected, $actual); $expected = $dbUser['usr_issuer']; - $actual = $user['usr_issuer']; + $actual = $user->issuer; $this->assertSame($expected, $actual); $expected = $dbUser['usr_token_password']; - $actual = $user['usr_token_password']; + $actual = $user->tokenPassword; $this->assertSame($expected, $actual); $expected = $dbUser['usr_token_id']; - $actual = $user['usr_token_id']; + $actual = $user->tokenId; $this->assertSame($expected, $actual); $expected = $dbUser['usr_preferences']; - $actual = $user['usr_preferences']; + $actual = $user->preferences; $this->assertSame($expected, $actual); $expected = $dbUser['usr_created_date']; - $actual = $user['usr_created_date']; + $actual = $user->createdDate; $this->assertSame($expected, $actual); $expected = $dbUser['usr_created_usr_id']; - $actual = $user['usr_created_usr_id']; + $actual = $user->createdUserId; $this->assertSame($expected, $actual); $expected = $dbUser['usr_updated_date']; - $actual = $user['usr_updated_date']; + $actual = $user->updatedDate; $this->assertSame($expected, $actual); $expected = $dbUser['usr_updated_usr_id']; - $actual = $user['usr_updated_usr_id']; + $actual = $user->updatedUserId; $this->assertSame($expected, $actual); } } diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php new file mode 100644 index 0000000..60964a4 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Sanitizers; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class UserSanitizerTest extends AbstractUnitTestCase +{ + public function testEmpty(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new UserSanitizer($filter); + + $expected = [ + 'id' => 0, + 'status' => 0, + 'email' => null, + 'password' => null, + 'namePrefix' => null, + 'nameFirst' => null, + 'nameLast' => null, + 'nameMiddle' => null, + 'nameSuffix' => null, + 'issuer' => null, + 'tokenPassword' => null, + 'tokenId' => null, + 'preferences' => null, + 'createdDate' => null, + 'createdUserId' => 0, + 'updatedDate' => null, + 'updatedUserId' => 0, + ]; + $actual = $sanitizer->sanitize([]); + $this->assertSame($expected, $actual); + } + + public function testObject(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new UserSanitizer($filter); + + $userData = [ + 'id' => '123', + 'status' => '4', + 'email' => 'John.Doe (newsletter) +spam@example.COM', + 'password' => 'some ', + 'namePrefix' => 'some ', + 'nameFirst' => 'some ', + 'nameLast' => 'some ', + 'nameMiddle' => 'some ', + 'nameSuffix' => 'some ', + 'issuer' => 'some ', + 'tokenPassword' => 'some ', + 'tokenId' => 'some ', + 'preferences' => 'some ', + 'createdDate' => 'some ', + 'createdUserId' => '123', + 'updatedDate' => 'some ', + 'updatedUserId' => '123', + ]; + + $expected = [ + 'id' => 123, + 'status' => 4, + 'email' => 'John.Doenewsletter+spam@example.COM', + 'password' => 'some ', + 'namePrefix' => 'some <value>', + 'nameFirst' => 'some <value>', + 'nameLast' => 'some <value>', + 'nameMiddle' => 'some <value>', + 'nameSuffix' => 'some <value>', + 'issuer' => 'some <value>', + 'tokenPassword' => 'some ', + 'tokenId' => 'some ', + 'preferences' => 'some <value>', + 'createdDate' => 'some <value>', + 'createdUserId' => 123, + 'updatedDate' => 'some <value>', + 'updatedUserId' => 123, + ]; + $actual = $sanitizer->sanitize($userData); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php new file mode 100644 index 0000000..155fdd0 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Validators; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; + +final class UserValidatorTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $input = []; + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidator($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + $faker = FakerFactory::create(); + + $input = [ + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + ]; + + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidator($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php new file mode 100644 index 0000000..afab4ff --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Validators; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidatorUpdate; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; + +final class UserValidatorUpdateTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $input = []; + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidatorUpdate($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 100), + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + ]; + + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidatorUpdate($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Validation/AbsIntTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Validation/AbsIntTest.php new file mode 100644 index 0000000..d01141b --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Validation/AbsIntTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Validation; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation; +use PHPUnit\Framework\Attributes\DataProvider; + +final class AbsIntTest extends AbstractUnitTestCase +{ + /** + * @return array + */ + public static function getExamples(): array + { + return [ + [ + 'amount' => 123, + 'expected' => 0, + ], + [ + 'amount' => 123.12, + 'expected' => 0, + ], + [ + 'amount' => '-12,000', + 'expected' => 1, + ], + [ + 'amount' => '-12,0@0', + 'expected' => 1, + ], + [ + 'amount' => '-12,0@@0', + 'expected' => 1, + ], + [ + 'amount' => '123abc', + 'expected' => 1, + ], + [ + 'amount' => '123.12e3', + 'expected' => 1, + ], + ]; + } + + #[DataProvider('getExamples')] + public function testValidator( + mixed $amount, + int $expected + ): void { + $validation = new Validation(); + $validation->add('amount', new AbsInt()); + + $messages = $validation->validate( + [ + 'amount' => $amount, + ] + ); + + $actual = $messages->count(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php new file mode 100644 index 0000000..63a60bb --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Validation; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class ResultTest extends AbstractUnitTestCase +{ + public function testSuccessIsValidAndHasNoErrors(): void + { + $result = Result::success(); + + $actual = $result->isValid(); + $this->assertTrue($actual); + + $expected = []; + $actual = $result->getErrors(); + $this->assertSame($expected, $actual); + } + + public function testErrorIsInvalidAndReturnsErrors(): void + { + $errors = ['field' => ['must not be empty']]; + $result = Result::error($errors); + + $expected = $errors; + $actual = $result->getErrors(); + $this->assertSame($expected, $actual); + } + + public function testGetMetaReturnsDefaultWhenMissing(): void + { + $result = new Result([], ['present' => 123]); + + $expected = 123; + $actual = $result->getMeta('present'); + $this->assertSame($expected, $actual); + + $actual = $result->getMeta('uknown'); + $this->assertNull($actual); + + $expected = 'default'; + $actual = $result->getMeta('unknown', 'default'); + $this->assertSame($expected, $actual); + } + + public function testSetMetaAddsOrUpdatesMeta(): void + { + $result = new Result(); + + $actual = $result->getMeta('key'); + $this->assertNull($actual); + + $result->setMeta('key', 'value'); + + $expected = 'value'; + $actual = $result->getMeta('key'); + $this->assertSame($expected, $actual); + + $result->setMeta('key', 42); + + $expected = 42; + $actual = $result->getMeta('key'); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php similarity index 58% rename from tests/Unit/Domain/Components/Encryption/JWTTokenTest.php rename to tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php index 476ff38..5236ae0 100644 --- a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php @@ -11,19 +11,16 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Encryption; - -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Encryption\Security\JWT\Token\Token; -use function sleep; - final class JWTTokenTest extends AbstractUnitTestCase { private JWTToken $jwtToken; @@ -37,18 +34,24 @@ public function setUp(): void public function testGetForUserReturnsTokenString(): void { - $user = $this->getUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $token = $this->jwtToken->getForUser($user); + $token = $this->jwtToken->getForUser($domainUser); $this->assertIsString($token); $this->assertNotEmpty($token); } public function testGetObjectReturnsPlainToken(): void { - $user = $this->getUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $tokenString = $this->jwtToken->getForUser($user); + $tokenString = $this->jwtToken->getForUser($domainUser); $plain = $this->jwtToken->getObject($tokenString); $this->assertInstanceOf(Token::class, $plain); @@ -73,9 +76,12 @@ public function testGetObjectThrowsOnInvalidTokenStructure(): void public function testGetUserReturnsUserArray(): void { - $user = $this->getUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $tokenString = $this->jwtToken->getForUser($user); + $tokenString = $this->jwtToken->getForUser($domainUser); $plain = $this->jwtToken->getObject($tokenString); $userRepository = $this @@ -91,39 +97,24 @@ public function testGetUserReturnsUserArray(): void $userRepository->expects($this->once()) ->method('findOneBy') - ->willReturn($user) + ->willReturn($domainUser) ; - $mockRepository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $mockRepository->expects($this->once()) - ->method('user') - ->willReturn($userRepository) - ; - - $result = $this->jwtToken->getUser($mockRepository, $plain); - $this->assertEquals($user, $result); + $result = $this->jwtToken->getUser($userRepository, $plain); + $this->assertEquals($domainUser, $result); } public function testValidateSuccess(): void { - $user = $this->getUserData(); - - $tokenString = $this->jwtToken->getForUser($user); - $plain = $this->jwtToken->getObject($tokenString); - $userTransport = new UserTransport($user); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - sleep(1); + $tokenString = $this->jwtToken->getForUser($domainUser); + $plain = $this->jwtToken->getObject($tokenString); - $actual = $this->jwtToken->validate($plain, $userTransport); + $actual = $this->jwtToken->validate($plain, $domainUser); $this->assertSame([], $actual); } diff --git a/tests/Unit/Domain/Components/Encryption/SecurityTest.php b/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php similarity index 80% rename from tests/Unit/Domain/Components/Encryption/SecurityTest.php rename to tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php index be1d7df..d505046 100644 --- a/tests/Unit/Domain/Components/Encryption/SecurityTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Encryption; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; use Phalcon\Api\Tests\AbstractUnitTestCase; final class SecurityTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php b/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php new file mode 100644 index 0000000..411ab54 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; + +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Cache\Cache; + +use function rand; +use function uniqid; + +final class TokenCacheTest extends AbstractUnitTestCase +{ + public function testStoreAndInvalidateForUser(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var Cache $cache */ + $cache = $this->container->get(Container::CACHE); + /** @var TokenCache $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + + /** + * Empty cache + */ + $cache->getAdapter()->clear(); + + /** + * Create user data + */ + $newUser = $this->getNewUserData(); + $newUser['usr_id'] = rand(1, 100); + $domainUser = new User( + $newUser['usr_id'], + $newUser['usr_status_flag'], + $newUser['usr_email'], + $newUser['usr_password'], + $newUser['usr_name_prefix'], + $newUser['usr_name_first'], + $newUser['usr_name_middle'], + $newUser['usr_name_last'], + $newUser['usr_name_suffix'], + $newUser['usr_issuer'], + $newUser['usr_token_password'], + $newUser['usr_token_id'], + $newUser['usr_preferences'], + $newUser['usr_created_date'], + $newUser['usr_created_usr_id'], + $newUser['usr_updated_date'], + $newUser['usr_updated_usr_id'] + ); + + $token1 = uniqid('tok-'); + $token2 = uniqid('tok-'); + + $tokenKey1 = CacheConstants::getCacheTokenKey($domainUser, $token1); + $tokenKey2 = CacheConstants::getCacheTokenKey($domainUser, $token2); + + /** + * Store + */ + $actual = $tokenCache->storeTokenInCache($env, $domainUser, $token1); + $this->assertTrue($actual); + $actual = $tokenCache->storeTokenInCache($env, $domainUser, $token2); + $this->assertTrue($actual); + + /** + * Check the cache + */ + $actual = $cache->has($tokenKey1); + $this->assertTrue($actual); + $actual = $cache->has($tokenKey2); + $this->assertTrue($actual); + + /** + * Invalidate + */ + $actual = $tokenCache->invalidateForUser($env, $domainUser); + $this->assertTrue($actual); + + $actual = $cache->has($tokenKey1); + $this->assertFalse($actual); + $actual = $cache->has($tokenKey2); + $this->assertFalse($actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php new file mode 100644 index 0000000..a35a98c --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; + +use Exception; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCacheInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Encryption\Security\JWT\Token\Token; + +use function rand; + +final class TokenManagerTest extends AbstractUnitTestCase +{ + public function testGetObjectTokenEmpty(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + /** @var JWTToken $jwtToken */ + $jwtToken = $this->container->get(Container::JWT_TOKEN); + + $manager = new TokenManager($tokenCache, $env, $jwtToken); + + $expected = null; + $actual = $manager->getObject(''); + $this->assertSame($expected, $actual); + + $actual = $manager->getObject(null); + $this->assertSame($expected, $actual); + } + + public function testGetObjectWithException(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + $mockJWTToken = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() + ; + $mockJWTToken + ->method('getObject') + ->willThrowException(new Exception('error')) + ; + + $manager = new TokenManager($tokenCache, $env, $mockJWTToken); + + $expected = null; + $actual = $manager->getObject(''); + $this->assertSame($expected, $actual); + + $actual = $manager->getObject(null); + $this->assertSame($expected, $actual); + } + + public function testGetObjectSuccess(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + /** @var JWTToken $jwtToken */ + $jwtToken = $this->container->get(Container::JWT_TOKEN); + $userData = $this->getNewUserData(); + $userData['usr_id'] = rand(1, 100); + + $token = $this->getUserToken($userData); + + $manager = new TokenManager($tokenCache, $env, $jwtToken); + + $expected = Token::class; + $actual = $manager->getObject($token); + $this->assertInstanceOf($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php similarity index 91% rename from tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php rename to tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php index 6c95c0f..613f813 100644 --- a/tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Common; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Enums\Common; -use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; final class FlagsEnumTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php similarity index 97% rename from tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php rename to tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php index 88f0e0b..b02f254 100644 --- a/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Http; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php similarity index 95% rename from tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php rename to tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php index 42592d4..df14ca9 100644 --- a/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Http; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php similarity index 88% rename from tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php rename to tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php index 5d40f00..e4703a7 100644 --- a/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Env\Adapters; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env\Adapters; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; use Phalcon\Api\Tests\AbstractUnitTestCase; final class DotEnvTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Components/Env/EnvFactoryTest.php b/tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php similarity index 77% rename from tests/Unit/Domain/Components/Env/EnvFactoryTest.php rename to tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php index 90aa664..c869af9 100644 --- a/tests/Unit/Domain/Components/Env/EnvFactoryTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Env; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; -use Phalcon\Api\Domain\Components\Env\EnvFactory; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Infrastructure\Env\EnvFactory; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; use Phalcon\Api\Tests\AbstractUnitTestCase; final class EnvFactoryTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Components/Env/EnvManagerTest.php b/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php similarity index 95% rename from tests/Unit/Domain/Components/Env/EnvManagerTest.php rename to tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php index 1e8f32c..3ff8f53 100644 --- a/tests/Unit/Domain/Components/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Env; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php similarity index 90% rename from tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php index 153a256..e8c8f2b 100644 --- a/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\HealthMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php similarity index 90% rename from tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php index 146fac8..e258055 100644 --- a/tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Middleware\NotFoundMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Event; use Phalcon\Mvc\Micro; diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php similarity index 72% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php index 453701a..d4bcf8c 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php @@ -11,13 +11,14 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\DataProvider; @@ -67,26 +68,28 @@ public function testValidateTokenClaimsFailure( array $userData, array $expectedErrors ): void { - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); - $tokenUser = $user; + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); [$micro, $middleware, $jwtToken] = $this->setupTest(); /** * Make the signature non valid */ - $tokenUser = array_replace($tokenUser, $userData); + $tokenUser = array_replace($user, $userData); $token = $this->getUserToken($tokenUser); $tokenObject = $jwtToken->getObject($token); + $domainUser = $userMapper->domain($user); /** - * Store the user in the session + * Store the user in the registry */ - /** @var TransportRepository $transport */ - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionUser($user); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $domainUser); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ @@ -113,22 +116,24 @@ public function testValidateTokenClaimsFailure( public function testValidateTokenClaimsSuccess(): void { - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); - $tokenUser = $user; + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $userMapper->domain($user); [$micro, $middleware, $jwtToken] = $this->setupTest(); - $token = $this->getUserToken($tokenUser); + $token = $this->getUserToken($user); $tokenObject = $jwtToken->getObject($token); /** - * Store the user in the session + * Store the user in the registry */ - /** @var TransportRepository $transport */ - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionUser($user); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $tokenUser); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php similarity index 93% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php index bbc0517..e5d5111 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php similarity index 62% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php index 6c65fdd..c0c04a6 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php @@ -11,15 +11,17 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] @@ -27,20 +29,22 @@ final class ValidateTokenRevokedMiddlewareTest extends AbstractUnitTestCase { public function testValidateTokenRevokedFailureInvalidToken(): void { - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); - $tokenUser = $user; + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $userMapper->domain($user); [$micro, $middleware] = $this->setupTest(); - $token = $this->getUserToken($tokenUser); + $token = $this->getUserToken($user); /** - * Store the user in the session + * Store the user in the registry */ - /** @var UserTransport $userRepository */ - $userRepository = $micro->getSharedService(Container::REPOSITORY_TRANSPORT); - $userRepository->setSessionUser($user); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $tokenUser); // There is no entry in the cache for this token, so this should fail. $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); @@ -68,24 +72,27 @@ public function testValidateTokenRevokedFailureInvalidToken(): void public function testValidateTokenRevokedSuccess(): void { - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); - $tokenUser = $user; + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $userMapper->domain($user); [$micro, $middleware] = $this->setupTest(); - $token = $this->getUserToken($tokenUser); + $token = $this->getUserToken($user); /** - * Store the user in the session + * Store the user in the registry */ - /** @var UserTransport $userRepository */ - $userRepository = $micro->getSharedService(Container::REPOSITORY_TRANSPORT); - $userRepository->setSessionUser($user); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $tokenUser); + /** @var Cache $cache */ $cache = $micro->getSharedService(Container::CACHE); - $sessionUser = $userRepository->getSessionUser(); - $cacheKey = $cache->getCacheTokenKey($sessionUser, $token); + $sessionUser = $registry->get('user'); + $cacheKey = CacheConstants::getCacheTokenKey($sessionUser, $token); $payload = [ 'token' => $token, ]; diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php similarity index 85% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php index 539aa48..bd96962 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php @@ -11,10 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -82,13 +83,16 @@ public function testValidateTokenStructureFailureNoDots(): void public function testValidateTokenStructureSuccess(): void { - $userData = $this->getNewUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); + $domainUser = $userMapper->domain($userData); [$micro, $middleware] = $this->setupTest(); /** @var JWTToken $jwtToken */ $jwtToken = $micro->getSharedService(Container::JWT_TOKEN); - $token = $jwtToken->getForUser($userData); + $token = $jwtToken->getForUser($domainUser); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ 'REQUEST_METHOD' => 'GET', diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php similarity index 73% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php index 827d519..c412f1d 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php @@ -11,13 +11,15 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] @@ -30,8 +32,9 @@ public function testValidateTokenUserFailureRecordNotFound(): void $userData['usr_id'] = 1; $token = $this->getUserToken($userData); $tokenObject = $jwtToken->getObject($token); - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ @@ -58,15 +61,19 @@ public function testValidateTokenUserFailureRecordNotFound(): void public function testValidateTokenUserSuccess(): void { - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $domainUser = $userMapper->domain($user); [$micro, $middleware, $jwtToken] = $this->setupTest(); - $token = $jwtToken->getForUser($user); + $token = $jwtToken->getForUser($domainUser); $tokenObject = $jwtToken->getObject($token); - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ diff --git a/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php similarity index 78% rename from tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php rename to tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php index 041baea..3bbbc81 100644 --- a/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Cache\Cache; final class CacheDataProviderTest extends AbstractUnitTestCase { diff --git a/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php similarity index 94% rename from tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php rename to tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php index a77e9ad..6992abe 100644 --- a/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Providers\ErrorHandlerProvider; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Logger\Logger; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Components/Providers/RouterProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php similarity index 92% rename from tests/Unit/Domain/Components/Providers/RouterProviderTest.php rename to tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php index 0429c1b..d0a993b 100644 --- a/tests/Unit/Domain/Components/Providers/RouterProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Providers\RouterProvider; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Providers\RouterProvider; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; diff --git a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php index 2049fb1..3bc5aae 100644 --- a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php @@ -13,9 +13,10 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; +use Faker\Factory; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; @@ -110,6 +111,34 @@ public function testServiceWithCredentials(): void } public function testServiceWrongCredentials(): void + { + $faker = Factory::create(); + /** @var LoginPostService $service */ + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + + /** + * Issue a wrong password + */ + $payload = [ + 'email' => $faker->email(), + 'password' => $faker->password(), + ]; + + $payload = $service->__invoke($payload); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppIncorrectCredentials->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testServiceWrongCredentialsForUser(): void { /** @var LoginPostService $service */ $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); diff --git a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php index 75f9637..326be2b 100644 --- a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php @@ -14,15 +14,16 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Domain\Services\Auth\LogoutPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; use Phalcon\Encryption\Security\JWT\Token\Item; use Phalcon\Encryption\Security\JWT\Token\Token; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -51,49 +52,56 @@ public function testServiceEmptyToken(): void public function testServiceInvalidToken(): void { - $user = $this->getNewUserData(); - $errors = [ + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $user = $this->getNewUserData(); + $user['usr_id'] = 1; + $domainUser = $userMapper->domain($user); + $errors = [ ['Incorrect token data'], ]; /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - 'validate', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + 'validate', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn($user); + $mockJWT->method('getUser')->willReturn($domainUser); $mockJWT->method('validate')->willReturn($errors); @@ -124,36 +132,39 @@ public function testServiceNotRefreshToken(): void /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(false); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); @@ -185,14 +196,14 @@ public function testServiceNotRefreshToken(): void public function testServiceSuccess(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); /** @var LogoutPostService $service */ $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); /** @var Cache $cache */ $cache = $this->container->getShared(Container::CACHE); /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); $migration = new UsersMigration($this->getConnection()); /** @@ -226,14 +237,14 @@ public function testServiceSuccess(): void $this->assertTrue($actual); $token = $data['jwt']['token']; - $domainUser = $transport->newUser($dbUser); - $tokenKey = $cache->getCacheTokenKey($domainUser, $token); + $domainUser = $userMapper->domain($dbUser); + $tokenKey = CacheConstants::getCacheTokenKey($domainUser, $token); $actual = $cache->has($tokenKey); $this->assertTrue($actual); $refreshToken = $data['jwt']['refreshToken']; - $tokenKey = $cache->getCacheTokenKey($domainUser, $refreshToken); + $tokenKey = CacheConstants::getCacheTokenKey($domainUser, $refreshToken); $actual = $cache->has($tokenKey); $this->assertTrue($actual); @@ -271,43 +282,50 @@ public function testServiceSuccess(): void public function testServiceWrongUser(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $domainUser = $userMapper->domain([]); + /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn([]); + $mockJWT->method('getUser')->willReturn(null); /** diff --git a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php index f274adb..fdf5937 100644 --- a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php @@ -14,9 +14,10 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Domain\Services\Auth\RefreshPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -49,49 +50,56 @@ public function testServiceEmptyToken(): void public function testServiceInvalidToken(): void { - $user = $this->getNewUserData(); - $errors = [ + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $user = $this->getNewUserData(); + $user['usr_id'] = 1; + $domainUser = $userMapper->domain($user); + $errors = [ ['Incorrect token data'], ]; /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - 'validate', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + 'validate', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn($user); + $mockJWT->method('getUser')->willReturn($domainUser); $mockJWT->method('validate')->willReturn($errors); @@ -122,36 +130,39 @@ public function testServiceNotRefreshToken(): void /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(false); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); @@ -231,40 +242,43 @@ public function testServiceWrongUser(): void /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn([]); + $mockJWT->method('getUser')->willReturn(null); /** diff --git a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php index 05807f5..dde46b4 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Services\User\UserDeleteService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; @@ -76,9 +76,7 @@ public function testServiceWithUserId(): void $errors = $actual['errors']; - $expected = [ - 'Record(s) not found', - ]; + $expected = [['Record(s) not found']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -103,9 +101,7 @@ public function testServiceZeroUserId(): void $errors = $actual['errors']; - $expected = [ - 'Record(s) not found', - ]; + $expected = [['Record(s) not found']]; $actual = $errors; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php index 2557457..128da7b 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php @@ -13,13 +13,16 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use DateTimeImmutable; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] @@ -31,19 +34,36 @@ public function testDispatchGet(): void $env = $this->container->getShared(Container::ENV); /** @var Cache $cache */ $cache = $this->container->getShared(Container::CACHE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); $userId = $dbUser['usr_id']; $token = $this->getUserToken($dbUser); - $domainUser = $transport->newUser($dbUser); + $domainUser = $userMapper->domain($dbUser); /** * Store the token in the cache */ - $cache->storeTokenInCache($env, $domainUser, $token); + $cacheKey = CacheConstants::getCacheTokenKey($domainUser, $token); + /** @var int $expiration */ + $expiration = $env->get( + 'TOKEN_EXPIRATION', + CacheConstants::CACHE_TOKEN_EXPIRY, + 'int' + ); + $expirationDate = (new DateTimeImmutable()) + ->modify('+' . $expiration . ' seconds') + ->format(Dates::DATE_TIME_FORMAT) + ; + + $payload = [ + 'token' => $token, + 'expiry' => $expirationDate, + ]; + + $cache->set($cacheKey, $payload, $expiration); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ @@ -75,8 +95,8 @@ public function testDispatchGet(): void $actual = $errors; $this->assertSame($expected, $actual); - $user = $transport->newUser($dbUser); - $expected = $user->toArray(); + $user = $userMapper->domain($dbUser); + $expected = [$userId => $user->toArray()]; $actual = $data; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Services/User/UserServiceGetTest.php b/tests/Unit/Domain/Services/User/UserServiceGetTest.php index d84e820..0bcbd48 100644 --- a/tests/Unit/Domain/Services/User/UserServiceGetTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceGetTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Services\User\UserGetService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; @@ -37,9 +37,11 @@ public function testServiceEmptyUserId(): void $actual = $payload->getResult(); $this->assertArrayHasKey('errors', $actual); - $expected = 'Record(s) not found'; - $actual = $actual['errors'][0]; - $this->assertStringContainsString($expected, $actual); + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); } public function testServiceWithUserId(): void @@ -155,8 +157,10 @@ public function testServiceWrongUserId(): void $actual = $payload->getResult(); $this->assertArrayHasKey('errors', $actual); - $expected = 'Record(s) not found'; - $actual = $actual['errors'][0]; - $this->assertStringContainsString($expected, $actual); + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); } } diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php index 0cbd8f4..d3a92c6 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -16,16 +16,15 @@ use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; use Phalcon\Api\Domain\Services\User\UserPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Filter; use PHPUnit\Framework\Attributes\BackupGlobals; +use function htmlspecialchars; + #[BackupGlobals(true)] final class UserServicePostTest extends AbstractUnitTestCase { @@ -41,45 +40,23 @@ public function testServiceFailureNoIdReturned(): void ) ->getMock() ; - $userRepository - ->method('insert') - ->willReturn(0) - ; + $userRepository->method('insert')->willReturn(0); - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository - ->method('user') - ->willReturn($userRepository) - ; - - /** - * Difference of achieving the same thing - see test above on - * how the class is used without getting it from the DI container - */ - $this->container->setShared(Container::REPOSITORY, $repository); + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $service */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; $payload = $service->__invoke($domainData); @@ -92,7 +69,7 @@ public function testServiceFailureNoIdReturned(): void $errors = $actual['errors']; - $expected = ['Cannot create database record: No id returned']; + $expected = [['Cannot create database record: No id returned']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -114,38 +91,21 @@ public function testServiceFailurePdoError(): void ->willThrowException(new PDOException('abcde')) ; - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository - ->method('user') - ->willReturn($userRepository) - ; + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); - /** @var Filter $filter */ - $filter = $this->container->get(Container::FILTER); - /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserPostService $service */ + $service = $this->container->get(Container::USER_POST_SERVICE); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); - $service = new UserPostService($repository, $transport, $filter, $security); - - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; $payload = $service->__invoke($domainData); @@ -158,7 +118,7 @@ public function testServiceFailurePdoError(): void $errors = $actual['errors']; - $expected = ['Cannot create database record: abcde']; + $expected = [['Cannot create database record: abcde']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -167,8 +127,8 @@ public function testServiceFailureValidation(): void { /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $userData = $this->getNewUserData(); @@ -183,9 +143,8 @@ public function testServiceFailureValidation(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; $payload = $service->__invoke($domainData); @@ -199,11 +158,12 @@ public function testServiceFailureValidation(): void $errors = $actual['errors']; $expected = [ - ['Field email cannot be empty.'], - ['Field password cannot be empty.'], - ['Field issuer cannot be empty.'], - ['Field tokenPassword cannot be empty.'], - ['Field tokenId cannot be empty.'], + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], ]; $actual = $errors; $this->assertSame($expected, $actual); @@ -213,8 +173,8 @@ public function testServiceSuccess(): void { /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $userData = $this->getNewUserData(); $userData['usr_created_usr_id'] = 4; @@ -223,9 +183,8 @@ public function testServiceSuccess(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; $payload = $service->__invoke($domainData); @@ -255,23 +214,23 @@ public function testServiceSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -287,9 +246,8 @@ public function testServiceSuccess(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; - $actual = $data['preferences']; - $this->assertSame($expected, $actual); + $actual = $data['preferences']; + $this->assertNull($actual); $expected = $domainData['createdDate']; $actual = $data['createdDate']; @@ -314,8 +272,8 @@ public function testServiceSuccessEmptyDates(): void $today = $now->format('Y-m-d'); /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $userData = $this->getNewUserData(); unset( @@ -326,9 +284,8 @@ public function testServiceSuccessEmptyDates(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; $payload = $service->__invoke($domainData); @@ -358,23 +315,23 @@ public function testServiceSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -390,9 +347,8 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; - $actual = $data['preferences']; - $this->assertSame($expected, $actual); + $actual = $data['preferences']; + $this->assertNull($actual); $actual = $data['createdDate']; $this->assertStringContainsString($today, $actual); diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index 7fe18af..799f9a3 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -13,17 +13,15 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; +use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; use Phalcon\Api\Domain\Services\User\UserPutService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use Phalcon\Filter\Filter; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] @@ -31,49 +29,43 @@ final class UserServicePutTest extends AbstractUnitTestCase { public function testServiceFailureNoIdReturned(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + $userRepository = $this ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() ->onlyMethods( [ 'update', + 'findById', ] ) ->getMock() ; $userRepository->method('update')->willReturn(0); + $userRepository->method('findById')->willReturn($findByUser); - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository->method('user')->willReturn($userRepository); - - /** @var Filter $filter */ - $filter = $this->container->get(Container::FILTER); - /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); - $service = new UserPutService($repository, $transport, $filter, $security); - - $userData = $this->getNewUserData(); + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); /** - * $userData is a db record. We need a domain object here + * Update user */ - $domainUser = $transport->newUser($userData); - $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; - $payload = $service->__invoke($domainData); + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); + + $payload = $service->__invoke($updateUser); $expected = DomainStatus::ERROR; $actual = $payload->getStatus(); @@ -84,64 +76,107 @@ public function testServiceFailureNoIdReturned(): void $errors = $actual['errors']; - $expected = ['Cannot update database record: No id returned']; + $expected = [['Cannot update database record: No id returned']]; $actual = $errors; $this->assertSame($expected, $actual); } public function testServiceFailurePdoError(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + $userRepository = $this ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() ->onlyMethods( [ 'update', + 'findById', ] ) ->getMock() ; + $userRepository->method('findById')->willReturn($findByUser); $userRepository ->method('update') ->willThrowException(new PDOException('abcde')) ; - $repository = $this - ->getMockBuilder(QueryRepository::class) + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); + + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); + + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + + /** + * $userData is a db record. We need a domain object here + */ + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); + + $payload = $service->__invoke($updateUser); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot update database record: abcde']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureRecordNotFound(): void + { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + + $userRepository = $this + ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() ->onlyMethods( [ - 'user', + 'findById', ] ) ->getMock() ; - $repository - ->method('user') - ->willReturn($userRepository) - ; + $userRepository->method('findById')->willReturn(null); - /** @var Filter $filter */ - $filter = $this->container->get(Container::FILTER); - /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); - $service = new UserPutService($repository, $transport, $filter, $security); - - $userData = $this->getNewUserData(); + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); /** - * $userData is a db record. We need a domain object here + * Update user */ - $domainUser = $transport->newUser($userData); - $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; - $payload = $service->__invoke($domainData); + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); - $expected = DomainStatus::ERROR; + $payload = $service->__invoke($updateUser); + + $expected = DomainStatus::NOT_FOUND; $actual = $payload->getStatus(); $this->assertSame($expected, $actual); @@ -150,7 +185,7 @@ public function testServiceFailurePdoError(): void $errors = $actual['errors']; - $expected = ['Cannot update database record: abcde']; + $expected = [['Record(s) not found']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -159,12 +194,12 @@ public function testServiceFailureValidation(): void { /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $userData = $this->getNewUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); unset( + $userData['usr_id'], $userData['usr_email'], $userData['usr_password'], $userData['usr_issuer'], @@ -175,11 +210,10 @@ public function testServiceFailureValidation(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); - $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); - $payload = $service->__invoke($domainData); + $payload = $service->__invoke($updateUser); $expected = DomainStatus::INVALID; $actual = $payload->getStatus(); @@ -191,11 +225,13 @@ public function testServiceFailureValidation(): void $errors = $actual['errors']; $expected = [ - ['Field email cannot be empty.'], - ['Field password cannot be empty.'], - ['Field issuer cannot be empty.'], - ['Field tokenPassword cannot be empty.'], - ['Field tokenId cannot be empty.'], + ['Field id is not a valid absolute integer and greater than 0'], + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], ]; $actual = $errors; $this->assertSame($expected, $actual); @@ -205,13 +241,18 @@ public function testServiceSuccess(): void { /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $migration = new UsersMigration($this->getConnection()); - $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; - $userData = $this->getNewUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + $userData['usr_id'] = $userId; + /** + * Don't hash the password + */ + $userData['usr_password'] = $this->getStrongPassword(); $userData['usr_created_usr_id'] = 4; $userData['usr_updated_usr_id'] = 5; @@ -219,11 +260,8 @@ public function testServiceSuccess(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; - - $domainData['id'] = $userId; $payload = $service->__invoke($domainData); @@ -251,23 +289,23 @@ public function testServiceSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -283,21 +321,27 @@ public function testServiceSuccess(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; + $expected = ''; $actual = $data['preferences']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = $dbUser['usr_created_date']; $actual = $data['createdDate']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = 0; $actual = $data['createdUserId']; $this->assertSame($expected, $actual); - $expected = $dbUser['usr_updated_date']; + $expected = $domainData['updatedDate']; $actual = $data['updatedDate']; - $this->assertNotSame($expected, $actual); + $this->assertSame($expected, $actual); $expected = 5; $actual = $data['updatedUserId']; @@ -306,15 +350,24 @@ public function testServiceSuccess(): void public function testServiceSuccessEmptyDates(): void { + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; - $userData = $this->getNewUserData(); + + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + $userData['usr_id'] = $userId; + /** + * Don't hash the password + */ + $userData['usr_password'] = $this->getStrongPassword(); + unset( $userData['usr_updated_date'], ); @@ -322,11 +375,8 @@ public function testServiceSuccessEmptyDates(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; - - $domainData['id'] = $userId; $payload = $service->__invoke($domainData); @@ -354,23 +404,23 @@ public function testServiceSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -386,21 +436,27 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; + $expected = ''; $actual = $data['preferences']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = $dbUser['usr_created_date']; $actual = $data['createdDate']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = 0; $actual = $data['createdUserId']; $this->assertSame($expected, $actual); - $expected = $dbUser['usr_updated_date']; - $actual = $data['updatedDate']; - $this->assertNotSame($expected, $actual); + $today = date('Y-m-d '); + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); $expected = $dbUser['usr_updated_usr_id']; $actual = $data['updatedUserId']; diff --git a/tests/Unit/Responder/JsonResponderTest.php b/tests/Unit/Responder/JsonResponderTest.php index a31b6b8..91d82f9 100644 --- a/tests/Unit/Responder/JsonResponderTest.php +++ b/tests/Unit/Responder/JsonResponderTest.php @@ -13,12 +13,11 @@ namespace Phalcon\Api\Tests\Unit\Responder; -use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Http\ResponseInterface; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -38,17 +37,7 @@ public function testFailure(): void $responder = $this->container->get(Container::RESPONDER_JSON); $errorContent = uniqid('error-'); - $payload = new Payload( - DomainStatus::UNAUTHORIZED, - [ - 'code' => HttpCodesEnum::Unauthorized->value, - 'message' => HttpCodesEnum::Unauthorized->text(), - 'data' => [], - 'errors' => [ - [1234 => $errorContent], - ], - ] - ); + $payload = Payload::unauthorized([[1234 => $errorContent]]); ob_start(); $outputResponse = $responder->__invoke($response, $payload); @@ -115,17 +104,7 @@ public function testSuccess(): void $responder = $this->container->getShared(Container::RESPONDER_JSON); $dataContent = uniqid('data-'); - $payload = new Payload( - DomainStatus::SUCCESS, - [ - 'code' => HttpCodesEnum::OK->value, - 'message' => HttpCodesEnum::OK->text(), - 'data' => [ - $dataContent, - ], - 'errors' => [], - ] - ); + $payload = Payload::success([$dataContent]); ob_start(); $outputResponse = $responder->__invoke($response, $payload);