From 21a1174bada7991e91b9efa56273ed3b2c097b51 Mon Sep 17 00:00:00 2001 From: Luis Galeas Date: Wed, 25 Sep 2024 19:57:08 +0100 Subject: [PATCH] User Details Endpoint --- .../code/run_in_local_test.sh | 2 - .../User/Controller/UserController.php | 26 +++++- .../UserProjectionReactionController.php | 16 +++- .../UserDetails.UserDetails.mongodb.xml | 14 +++ ...ls.ValueObject.UnverifiedEmail.mongodb.xml | 6 ++ ...ails.ValueObject.VerifiedEmail.mongodb.xml | 6 ++ ...ifiedEmailButRequestedNewEmail.mongodb.xml | 7 ++ .../Projection/UserDetails/GetUserDetails.php | 81 ++++++++++++++++ .../Projection/UserDetails/UserDetails.php | 44 +++++++++ .../UserDetails/UserDetailsProjector.php | 88 ++++++++++++++++++ .../ValueObject/UnverifiedEmail.php | 25 +++++ .../UserDetails/ValueObject/VerifiedEmail.php | 25 +++++ .../VerifiedEmailButRequestedNewEmail.php | 32 +++++++ .../User/Query/GetUserDetailsQuery.php | 10 ++ .../GetUserDetailsQueryHandler.php | 29 ++++++ .../Request/V1_Identity_User_Details.json | 13 +++ .../Response/V1_Identity_User_Details.json | 72 +++++++++++++++ .../UserDetails/GetUserDetailsTest.php | 92 +++++++++++++++++++ .../UserDetails/UserDetailsProjectorTest.php | 63 +++++++++++++ .../data_destination_identity_user.tf | 11 +++ 20 files changed, 658 insertions(+), 4 deletions(-) create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.UserDetails.mongodb.xml create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.UnverifiedEmail.mongodb.xml create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmail.mongodb.xml create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmailButRequestedNewEmail.mongodb.xml create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetails.php create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetails.php create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjector.php create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/UnverifiedEmail.php create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmail.php create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmailButRequestedNewEmail.php create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Query/GetUserDetailsQuery.php create mode 100644 application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/QueryHandler/GetUserDetailsQueryHandler.php create mode 100644 application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Request/V1_Identity_User_Details.json create mode 100644 application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Response/V1_Identity_User_Details.json create mode 100644 application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetailsTest.php create mode 100644 application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjectorTest.php diff --git a/application/backend-monorepo-multiservice/code/run_in_local_test.sh b/application/backend-monorepo-multiservice/code/run_in_local_test.sh index 06d2ac4b..a64d892a 100755 --- a/application/backend-monorepo-multiservice/code/run_in_local_test.sh +++ b/application/backend-monorepo-multiservice/code/run_in_local_test.sh @@ -1,6 +1,4 @@ #!/bin/bash set -e -php bin/console galeas:dbs:updates - sleep 50000000 diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Controller/UserController.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Controller/UserController.php index fd013f3e..03a9f6f0 100644 --- a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Controller/UserController.php +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Controller/UserController.php @@ -10,7 +10,9 @@ use Galeas\Api\BoundedContext\Identity\User\CommandHandler\RequestPrimaryEmailChange\RequestPrimaryEmailChangeHandler; use Galeas\Api\BoundedContext\Identity\User\CommandHandler\SignUp\SignUpHandler; use Galeas\Api\BoundedContext\Identity\User\CommandHandler\VerifyPrimaryEmail\VerifyPrimaryEmailHandler; +use Galeas\Api\BoundedContext\Identity\User\Query\GetUserDetailsQuery; use Galeas\Api\BoundedContext\Identity\User\Query\ListSentVerificationEmailQuery; +use Galeas\Api\BoundedContext\Identity\User\QueryHandler\GetUserDetailsQueryHandler; use Galeas\Api\BoundedContext\Identity\User\QueryHandler\ListSentVerificationEmailQueryHandler; use Galeas\Api\CommonController\BaseController; use Symfony\Component\HttpFoundation\Request; @@ -24,17 +26,20 @@ class UserController extends BaseController private VerifyPrimaryEmailHandler $verifyPrimaryEmailHandler; private RequestPrimaryEmailChangeHandler $requestPrimaryEmailChangeHandler; private ListSentVerificationEmailQueryHandler $listSentVerificationEmailQueryHandler; + private GetUserDetailsQueryHandler $getUserDetailsQueryHandler; public function __construct( SignUpHandler $signUpHandler, VerifyPrimaryEmailHandler $verifyPrimaryEmailHandler, RequestPrimaryEmailChangeHandler $requestPrimaryEmailChangeHandler, - ListSentVerificationEmailQueryHandler $listSentVerificationEmailQueryHandler + ListSentVerificationEmailQueryHandler $listSentVerificationEmailQueryHandler, + GetUserDetailsQueryHandler $getUserDetailsQueryHandler ) { $this->signUpHandler = $signUpHandler; $this->verifyPrimaryEmailHandler = $verifyPrimaryEmailHandler; $this->requestPrimaryEmailChangeHandler = $requestPrimaryEmailChangeHandler; $this->listSentVerificationEmailQueryHandler = $listSentVerificationEmailQueryHandler; + $this->getUserDetailsQueryHandler = $getUserDetailsQueryHandler; } /** @@ -112,4 +117,23 @@ public function listSentVerificationEmails(Request $request): Response Response::HTTP_OK ); } + + /** + * @RequestSchema(name="V1_Identity_User_Details") + * + * @ResponseSchema(name="V1_Identity_User_Details") + */ + #[Route('/identity/user/details', name: 'V1_Identity_User_Details', methods: ['POST'])] + public function getUserDetails(Request $request): Response + { + return $this->jsonPostRequestJsonResponse( + $request, + 'Request/V1_Identity_User_Details.json', + 'Response/V1_Identity_User_Details.json', + GetUserDetailsQuery::class, + $this->getUserDetailsQueryHandler, + null, + Response::HTTP_OK + ); + } } diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/ControllerForProjectionReaction/UserProjectionReactionController.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/ControllerForProjectionReaction/UserProjectionReactionController.php index 9c9a2628..a10aad9a 100644 --- a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/ControllerForProjectionReaction/UserProjectionReactionController.php +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/ControllerForProjectionReaction/UserProjectionReactionController.php @@ -8,6 +8,7 @@ use Galeas\Api\BoundedContext\Identity\User\Projection\SentVerificationEmail\SentVerificationEmailProjector; use Galeas\Api\BoundedContext\Identity\User\Projection\TakenEmail\TakenEmailProjector; use Galeas\Api\BoundedContext\Identity\User\Projection\TakenUsername\TakenUsernameProjector; +use Galeas\Api\BoundedContext\Identity\User\Projection\UserDetails\UserDetailsProjector; use Galeas\Api\BoundedContext\Identity\User\Reaction\SendPrimaryEmailVerification\SendPrimaryEmailVerificationReactor; use Galeas\Api\CommonController\ProjectionReactionController; use Symfony\Component\HttpFoundation\Request; @@ -22,19 +23,22 @@ class UserProjectionReactionController extends ProjectionReactionController private TakenUsernameProjector $takenUsernameProjector; private SentVerificationEmailProjector $sentVerificationEmailProjector; private SendPrimaryEmailVerificationReactor $sendPrimaryEmailVerificationReactor; + private UserDetailsProjector $userDetailsProjector; public function __construct( PrimaryEmailVerificationCodeProjector $primaryEmailVerificationCodeProjector, TakenEmailProjector $takenEmailProjector, TakenUsernameProjector $takenUsernameProjector, SentVerificationEmailProjector $sentVerificationEmailProjector, - SendPrimaryEmailVerificationReactor $sendPrimaryEmailVerificationReactor + SendPrimaryEmailVerificationReactor $sendPrimaryEmailVerificationReactor, + UserDetailsProjector $userDetailsProjector ) { $this->primaryEmailVerificationCodeProjector = $primaryEmailVerificationCodeProjector; $this->takenEmailProjector = $takenEmailProjector; $this->takenUsernameProjector = $takenUsernameProjector; $this->sentVerificationEmailProjector = $sentVerificationEmailProjector; $this->sendPrimaryEmailVerificationReactor = $sendPrimaryEmailVerificationReactor; + $this->userDetailsProjector = $userDetailsProjector; } #[Route('/projection/primary_email_verification_code', name: 'projection_primary_email_verification_code', methods: ['POST'])] @@ -77,6 +81,16 @@ public function sentVerificationEmail(Request $request): Response ); } + #[Route('/projection/user_details', name: 'projection_user_details', methods: ['POST'])] + public function userDetails(Request $request): Response + { + return $this->jsonPostRequestJsonResponse( + $request, + $this->userDetailsProjector, + 200 + ); + } + #[Route('/reaction/send_primary_email_verification', name: 'reaction_send_primary_email_verification', methods: ['POST'])] public function sendPrimaryEmailVerificationReactor(Request $request): Response { diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.UserDetails.mongodb.xml b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.UserDetails.mongodb.xml new file mode 100644 index 00000000..aa85eac1 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.UserDetails.mongodb.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.UnverifiedEmail.mongodb.xml b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.UnverifiedEmail.mongodb.xml new file mode 100644 index 00000000..911d6372 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.UnverifiedEmail.mongodb.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmail.mongodb.xml b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmail.mongodb.xml new file mode 100644 index 00000000..4a977657 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmail.mongodb.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmailButRequestedNewEmail.mongodb.xml b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmailButRequestedNewEmail.mongodb.xml new file mode 100644 index 00000000..b1fb37f5 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/MongoMapping/UserDetails.ValueObject.VerifiedEmailButRequestedNewEmail.mongodb.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetails.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetails.php new file mode 100644 index 00000000..bc4e883b --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetails.php @@ -0,0 +1,81 @@ +projectionDocumentManager = $projectionDocumentManager; + } + + /** + * @return array{userId: string, primaryEmailStatus: array{unverifiedEmail: array{email: string}}|array{verifiedEmail: array{email: string}}|array{verifiedEmailButRequestedNewEmail: array{requestedEmail: string, verifiedEmail: string}}} + * + * @throws ProjectionCannotRead + */ + public function getUserDetails(string $userId): array + { + try { + $userDetails = $this->projectionDocumentManager + ->createQueryBuilder(UserDetails::class) + ->field('id')->equals($userId) + ->getQuery() + ->getSingleResult() + ; + + if ($userDetails instanceof UserDetails) { + $status = $userDetails->getPrimaryEmailStatus(); + + switch ($status) { + case $status instanceof UnverifiedEmail: + $primaryEmailStatus = [ + 'unverifiedEmail' => [ + 'email' => $status->getEmail(), + ], + ]; + + break; + + case $status instanceof VerifiedEmail: + $primaryEmailStatus = [ + 'verifiedEmail' => [ + 'email' => $status->getEmail(), + ], + ]; + + break; + + case $status instanceof VerifiedEmailButRequestedNewEmail: + $primaryEmailStatus = [ + 'verifiedButRequestedNewEmail' => [ + 'requestedEmail' => $status->getRequestedEmail(), + 'verifiedEmail' => $status->getVerifiedEmail(), + ], + ]; + + break; + } + + return [ + 'userId' => $userDetails->getUserId(), + 'primaryEmailStatus' => $primaryEmailStatus, + ]; + } + + throw new \Exception('Expected UserDetails instance, got nothing.'); + } catch (\Throwable $exception) { + throw new ProjectionCannotRead($exception); + } + } +} diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetails.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetails.php new file mode 100644 index 00000000..1c3e32d9 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetails.php @@ -0,0 +1,44 @@ +id; + } + + public function getPrimaryEmailStatus(): UnverifiedEmail|VerifiedEmail|VerifiedEmailButRequestedNewEmail + { + return $this->primaryEmailStatus; + } + + public function changePrimaryEmailStatus(UnverifiedEmail|VerifiedEmail|VerifiedEmailButRequestedNewEmail $primaryEmailStatus): self + { + $this->primaryEmailStatus = $primaryEmailStatus; + + return $this; + } + + public static function fromProperties(string $userId, UnverifiedEmail|VerifiedEmail|VerifiedEmailButRequestedNewEmail $primaryEmailStatus): self + { + $userDetails = new self(); + $userDetails->id = $userId; + $userDetails->primaryEmailStatus = $primaryEmailStatus; + + return $userDetails; + } +} diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjector.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjector.php new file mode 100644 index 00000000..ad50bb8e --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjector.php @@ -0,0 +1,88 @@ +projectionDocumentManager = $projectionDocumentManager; + } + + public function project(Event $event): void + { + try { + if ( + $event instanceof SignedUp + || $event instanceof PrimaryEmailVerified + || $event instanceof PrimaryEmailChangeRequested + ) { + $userId = $event->aggregateId()->id(); + } else { + return; + } + + $userDetails = $this->projectionDocumentManager + ->createQueryBuilder(UserDetails::class) + ->field('id')->equals($userId) + ->getQuery() + ->getSingleResult() + ; + + if ($userDetails instanceof UserDetails && $event instanceof SignedUp) { + $userDetails = UserDetails::fromProperties($userId, UnverifiedEmail::fromProperties( + $event->primaryEmail() + )); + } elseif ($userDetails instanceof UserDetails) { + $currentStatus = $userDetails->getPrimaryEmailStatus(); + $newStatus = $this->getPrimaryEmailStatusFromEvent($event, $currentStatus); + $userDetails->changePrimaryEmailStatus($newStatus); + } else { + throw new \InvalidArgumentException('Unsupported operation'); + } + + $this->projectionDocumentManager->persist($userDetails); + $this->projectionDocumentManager->flush(); + } catch (\Throwable $throwable) { + throw new ProjectionCannotProcess($throwable); + } + } + + /** + * @throws \InvalidArgumentException + */ + private function getPrimaryEmailStatusFromEvent( + Event $event, + null|UnverifiedEmail|VerifiedEmail|VerifiedEmailButRequestedNewEmail $currentStatus = null + ): UnverifiedEmail|VerifiedEmail|VerifiedEmailButRequestedNewEmail { + if ($event instanceof PrimaryEmailVerified && $currentStatus instanceof UnverifiedEmail) { + return VerifiedEmail::fromProperties($currentStatus->getEmail()); + } + if ($event instanceof PrimaryEmailVerified && $currentStatus instanceof VerifiedEmailButRequestedNewEmail) { + return VerifiedEmail::fromProperties($currentStatus->getRequestedEmail()); + } + if ($event instanceof PrimaryEmailChangeRequested && $currentStatus instanceof UnverifiedEmail) { + return UnverifiedEmail::fromProperties($event->newEmailRequested()); + } + if ($event instanceof PrimaryEmailChangeRequested && $currentStatus instanceof VerifiedEmailButRequestedNewEmail) { + return VerifiedEmailButRequestedNewEmail::fromProperties($currentStatus->getVerifiedEmail(), $event->newEmailRequested()); + } + + throw new \InvalidArgumentException('Unsupported operation'); + } +} diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/UnverifiedEmail.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/UnverifiedEmail.php new file mode 100644 index 00000000..ae6c0e17 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/UnverifiedEmail.php @@ -0,0 +1,25 @@ +email; + } + + public static function fromProperties(string $email): self + { + $instance = new self(); + $instance->email = $email; + + return $instance; + } +} diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmail.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmail.php new file mode 100644 index 00000000..6767bbe3 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmail.php @@ -0,0 +1,25 @@ +email; + } + + public static function fromProperties(string $email): self + { + $instance = new self(); + $instance->email = $email; + + return $instance; + } +} diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmailButRequestedNewEmail.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmailButRequestedNewEmail.php new file mode 100644 index 00000000..4719be0f --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Projection/UserDetails/ValueObject/VerifiedEmailButRequestedNewEmail.php @@ -0,0 +1,32 @@ +verifiedEmail; + } + + public function getRequestedEmail(): string + { + return $this->requestedEmail; + } + + public static function fromProperties(string $verifiedEmail, string $requestedEmail): self + { + $instance = new self(); + $instance->verifiedEmail = $verifiedEmail; + $instance->requestedEmail = $requestedEmail; + + return $instance; + } +} diff --git a/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Query/GetUserDetailsQuery.php b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Query/GetUserDetailsQuery.php new file mode 100644 index 00000000..cc86ea8d --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/BoundedContext/Identity/User/Query/GetUserDetailsQuery.php @@ -0,0 +1,10 @@ +getUserDetails = $getUserDetails; + } + + /** + * @return array{userId: string, primaryEmailStatus: array{unverifiedEmail: array{email: string}}|array{verifiedEmail: array{email: string}}|array{verifiedEmailButRequestedNewEmail: array{requestedEmail: string, verifiedEmail: string}}} + * + * @throws ProjectionCannotRead + */ + public function handle(GetUserDetailsQuery $getUserDetailsQuery): array + { + return $this->getUserDetails->getUserDetails($getUserDetailsQuery->authenticatedUserId); + } +} diff --git a/application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Request/V1_Identity_User_Details.json b/application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Request/V1_Identity_User_Details.json new file mode 100644 index 00000000..cd23bcc5 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Request/V1_Identity_User_Details.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "withSessionToken": { + "$ref": "Request/Shared/StringSessionToken.json" + } + }, + "required": [ + "withSessionToken" + ] +} \ No newline at end of file diff --git a/application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Response/V1_Identity_User_Details.json b/application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Response/V1_Identity_User_Details.json new file mode 100644 index 00000000..3c682308 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/src/JsonSchema/Schemas/Response/V1_Identity_User_Details.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "userId": { + "type": "string" + }, + "primaryEmailStatus": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "unverifiedEmail": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string" + } + }, + "required": ["email"] + } + }, + "required": ["unverifiedEmail"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "verifiedEmail": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string" + } + }, + "required": ["email"] + } + }, + "required": ["verifiedEmail"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "verifiedButRequestedNewEmail": { + "type": "object", + "additionalProperties": false, + "properties": { + "verifiedEmail": { + "type": "string" + }, + "requestedEmail": { + "type": "string" + } + }, + "required": ["verifiedEmail", "requestedEmail"] + } + }, + "required": ["verifiedButRequestedNewEmail"] + } + ] + } + }, + "required": [ + "userId", + "primaryEmailStatus" + ] +} \ No newline at end of file diff --git a/application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetailsTest.php b/application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetailsTest.php new file mode 100644 index 00000000..58839bac --- /dev/null +++ b/application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/GetUserDetailsTest.php @@ -0,0 +1,92 @@ +getContainer() + ->get(GetUserDetails::class) + ; + + $this->getProjectionDocumentManager()->persist(UserDetails::fromProperties( + '1123', + UnverifiedEmail::fromProperties('unverified@b.com') + )); + $this->getProjectionDocumentManager()->flush(); + Assert::assertEquals( + $getDetailsService->getUserDetails('1123'), + [ + 'userId' => '1123', + 'primaryEmailStatus' => [ + 'unverifiedEmail' => [ + 'email' => 'unverified@b.com', + ], + ], + ] + ); + + $this->getProjectionDocumentManager()->persist(UserDetails::fromProperties( + '1124', + VerifiedEmail::fromProperties('verified@b.com') + )); + $this->getProjectionDocumentManager()->flush(); + Assert::assertEquals( + $getDetailsService->getUserDetails('1124'), + [ + 'userId' => '1124', + 'primaryEmailStatus' => [ + 'verifiedEmail' => [ + 'email' => 'verified@b.com', + ], + ], + ] + ); + + $this->getProjectionDocumentManager()->persist(UserDetails::fromProperties( + '1125', + VerifiedEmailButRequestedNewEmail::fromProperties( + 'verified_but_requested_new@c.com', + 'new_requested@c.com', + ) + )); + $this->getProjectionDocumentManager()->flush(); + Assert::assertEquals( + $getDetailsService->getUserDetails('1125'), + [ + 'userId' => '1125', + 'primaryEmailStatus' => [ + 'verifiedButRequestedNewEmail' => [ + 'verifiedEmail' => 'verified_but_requested_new@c.com', + 'requestedEmail' => 'new_requested@c.com', + ], + ], + ] + ); + } + + public function testGetNoDetails(): void + { + $this->expectException(ProjectionCannotRead::class); + $this->expectExceptionMessageMatches('/Expected UserDetails instance/'); + + /** @var GetUserDetails $getDetailsService */ + $getDetailsService = $this->getContainer() + ->get(GetUserDetails::class) + ; + $getDetailsService->getUserDetails('1123'); + } +} diff --git a/application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjectorTest.php b/application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjectorTest.php new file mode 100644 index 00000000..897f1d06 --- /dev/null +++ b/application/backend-monorepo-multiservice/code/tests/UnitAndIntegration/BoundedContext/Identity/User/Projection/UserDetails/UserDetailsProjectorTest.php @@ -0,0 +1,63 @@ +getContainer() + ->get(UserDetailsProjector::class) + ; + + $signedUp = SampleEvents::signedUp(); + $userId = $signedUp->aggregateId()->id(); + $UserDetailsProjectorService->project($signedUp); + + Assert::assertEquals( + [ + UserDetails::fromProperties( + $userId, + UnverifiedEmail::fromProperties($signedUp->primaryEmail()) + ), + ], + $this->findUserDetails($userId) + ); + } + + /** + * @throws \Exception + */ + private function findUserDetails(string $userId): ?UserDetails + { + $queryBuilder = $this->getProjectionDocumentManager() + ->createQueryBuilder(UserDetails::class) + ; + + $queryBuilder->field('id')->equals($userId); + + $userDetails = $queryBuilder + ->getQuery() + ->getSingleResult() + ; + + if ($userDetails instanceof UserDetails) { + return $userDetails; + } + + if (null === $userDetails) { + return null; + } + + throw new \Exception('Unexpected type'); + } +} diff --git a/infrastructure/ambar/production/data_destination_identity_user.tf b/infrastructure/ambar/production/data_destination_identity_user.tf index b070af61..3a9cddee 100644 --- a/infrastructure/ambar/production/data_destination_identity_user.tf +++ b/infrastructure/ambar/production/data_destination_identity_user.tf @@ -38,6 +38,17 @@ resource "ambar_data_destination" "Identity_User_SentVerificationEmail" { password = "password" } +resource "ambar_data_destination" "Identity_User_UserDetails" { + filter_ids = [ + ambar_filter.identity_all.resource_id, + ] + description = "Identity_User_UserDetails" + destination_endpoint = "${var.data_destination_identity.endpoint_prefix}/api/v1/identity/user/projection/user_details" + username = "username" + password = "password" +} + + resource "ambar_data_destination" "Identity_User_SendPrimaryEmailVerification" { filter_ids = [ ambar_filter.identity_all.resource_id,