From dc665b633182b3e7f43e4043aa65234b0adf89c6 Mon Sep 17 00:00:00 2001 From: Syndesi Date: Tue, 23 Dec 2025 19:34:24 +0100 Subject: [PATCH 1/2] implement behavior in which the data payload of user registration endpoint is correctly normalized, fixes #282 --- CHANGELOG.md | 2 + .../User/PostRegisterController.php | 38 ++--- .../Endpoint/User/PostRegisterTest.php | 18 +++ .../User/PostRegisterControllerTest.php | 134 ++++++++++++------ 4 files changed, 129 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0dd0fd..45737529 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Fixed +- Implement behavior in which the data payload of user registration endpoint is correctly normalized, fixes #282. ## 0.1.28 - 2025-12-21 ### Changed diff --git a/src/Controller/User/PostRegisterController.php b/src/Controller/User/PostRegisterController.php index 826fd14a..05f33ad7 100644 --- a/src/Controller/User/PostRegisterController.php +++ b/src/Controller/User/PostRegisterController.php @@ -9,9 +9,9 @@ use App\Factory\Exception\Server500LogicExceptionFactory; use App\Response\CreatedResponse; use App\Security\UserPasswordHasher; +use App\Service\CreateElementFromRawDataService; use App\Service\ElementManager; use App\Service\RequestUtilService; -use App\Type\NodeElement; use EmberNexusBundle\Service\EmberNexusConfiguration; use Laudis\Neo4j\Databags\Statement; use Ramsey\Uuid\Rfc4122\UuidV4; @@ -25,6 +25,9 @@ class PostRegisterController extends AbstractController { + /** + * @SuppressWarnings("PHPMD.ExcessiveParameterList") + */ public function __construct( private ElementManager $elementManager, private CypherEntityManager $cypherEntityManager, @@ -32,6 +35,7 @@ public function __construct( private UserPasswordHasher $userPasswordHasher, private EmberNexusConfiguration $emberNexusConfiguration, private RequestUtilService $requestUtilService, + private CreateElementFromRawDataService $createElementFromRawDataService, private Client400ReservedIdentifierExceptionFactory $client400ReservedIdentifierExceptionFactory, private Client403ForbiddenExceptionFactory $client403ForbiddenExceptionFactory, private Server500LogicExceptionFactory $server500LogicExceptionFactory, @@ -50,16 +54,22 @@ public function postRegister(Request $request): Response } $body = \Safe\json_decode($request->getContent(), true); - $data = $this->requestUtilService->getDataFromBody($body); + $rawData = $this->requestUtilService->getDataFromBody($body); $this->requestUtilService->validateTypeFromBody('User', $body); $userId = UuidV4::uuid4(); $password = $this->requestUtilService->getStringFromBody('password', $body); - $uniqueUserIdentifier = $this->requestUtilService->getUniqueUserIdentifierFromBodyAndData($body, $data); + $uniqueUserIdentifier = $this->requestUtilService->getUniqueUserIdentifierFromBodyAndData($body, $rawData); $this->checkForDuplicateUniqueUserIdentifier($uniqueUserIdentifier); - $userNode = $this->createUserNode($userId, $data, $uniqueUserIdentifier, $password); - + $uniqueIdentifier = $this->emberNexusConfiguration->getRegisterUniqueIdentifier(); + $userNode = $this->createElementFromRawDataService->createElementFromRawData( + $userId, + 'User', + rawData: $rawData + ); + $userNode->addProperty($uniqueIdentifier, $uniqueUserIdentifier); + $userNode->addProperty('_passwordHash', $this->userPasswordHasher->hashPassword($password)); $this->elementManager->create($userNode); $this->elementManager->flush(); @@ -90,24 +100,6 @@ private function checkForDuplicateUniqueUserIdentifier(string $uniqueUserIdentif } } - /** - * @param array $data - */ - private function createUserNode(UuidInterface $userId, array $data, string $uniqueUserIdentifier, string $password): NodeElement - { - $uniqueIdentifier = $this->emberNexusConfiguration->getRegisterUniqueIdentifier(); - $userNode = (new NodeElement()) - ->setId($userId) - ->setLabel('User') - ->addProperties([ - ...$data, - $uniqueIdentifier => $uniqueUserIdentifier, - '_passwordHash' => $this->userPasswordHasher->hashPassword($password), - ]); - - return $userNode; - } - private function createCreatedResponse(UuidInterface $userId): CreatedResponse { return new CreatedResponse( diff --git a/tests/FeatureTests/Endpoint/User/PostRegisterTest.php b/tests/FeatureTests/Endpoint/User/PostRegisterTest.php index 9d1374b0..a487ae2f 100644 --- a/tests/FeatureTests/Endpoint/User/PostRegisterTest.php +++ b/tests/FeatureTests/Endpoint/User/PostRegisterTest.php @@ -106,4 +106,22 @@ public function testPostRegisterFailsForDuplicateEmail(): void $this->assertIsProblemResponse($response2, 400); } + + public function testPostRegisterCanTriggerNormalizationExceptions(): void + { + $response = $this->runPostRequest( + '/register', + null, + [ + 'type' => 'User', + 'password' => '1234', + 'uniqueUserIdentifier' => 'user6@register.user.endpoint.localhost.dev', + 'data' => [ + '_passwordHash' => 'The key "_passwordHash" can not be manually defined.', + ], + ] + ); + + $this->assertIsProblemResponse($response, 400); + } } diff --git a/tests/UnitTests/Controller/User/PostRegisterControllerTest.php b/tests/UnitTests/Controller/User/PostRegisterControllerTest.php index bdd39416..d20c2e54 100644 --- a/tests/UnitTests/Controller/User/PostRegisterControllerTest.php +++ b/tests/UnitTests/Controller/User/PostRegisterControllerTest.php @@ -7,11 +7,14 @@ use App\Controller\User\PostRegisterController; use App\Exception\Client400ReservedIdentifierException; use App\Exception\Client403ForbiddenException; +use App\Factory\Exception\Client400BadContentExceptionFactory; +use App\Factory\Exception\Client400MissingPropertyExceptionFactory; use App\Factory\Exception\Client400ReservedIdentifierExceptionFactory; use App\Factory\Exception\Client403ForbiddenExceptionFactory; use App\Factory\Exception\Server500LogicExceptionFactory; use App\Response\CreatedResponse; use App\Security\UserPasswordHasher; +use App\Service\CreateElementFromRawDataService; use App\Service\ElementManager; use App\Service\RequestUtilService; use App\Type\NodeElement; @@ -27,6 +30,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Ramsey\Uuid\Nonstandard\Uuid; +use Ramsey\Uuid\UuidInterface; use ReflectionMethod; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -39,6 +43,9 @@ class PostRegisterControllerTest extends TestCase { use ProphecyTrait; + /** + * @SuppressWarnings("PHPMD.ExcessiveParameterList") + */ private function getPostRegisterController( ?ElementManager $elementManager = null, ?EntityManager $cypherEntityManager = null, @@ -46,6 +53,7 @@ private function getPostRegisterController( ?UserPasswordHasher $userPasswordHasher = null, ?EmberNexusConfiguration $emberNexusConfiguration = null, ?RequestUtilService $requestUtilService = null, + ?CreateElementFromRawDataService $createElementFromRawDataService = null, ?Client400ReservedIdentifierExceptionFactory $client400ReservedIdentifierExceptionFactory = null, ?Client403ForbiddenExceptionFactory $client403ForbiddenExceptionFactory = null, ?Server500LogicExceptionFactory $server500LogicExceptionFactory = null, @@ -57,6 +65,7 @@ private function getPostRegisterController( $userPasswordHasher ?? $this->createMock(UserPasswordHasher::class), $emberNexusConfiguration ?? $this->createMock(EmberNexusConfiguration::class), $requestUtilService ?? $this->createMock(RequestUtilService::class), + $createElementFromRawDataService ?? $this->createMock(CreateElementFromRawDataService::class), $client400ReservedIdentifierExceptionFactory ?? $this->createMock(Client400ReservedIdentifierExceptionFactory::class), $client403ForbiddenExceptionFactory ?? $this->createMock(Client403ForbiddenExceptionFactory::class), $server500LogicExceptionFactory ?? $this->createMock(Server500LogicExceptionFactory::class) @@ -84,13 +93,28 @@ public function testPostRegisterWithEnabledRegister(): void $cypherEntityManager = $this->createMock(EntityManager::class); $cypherEntityManager->method('getClient')->willReturn($cypherClient); + $userNode = new NodeElement(); + $createElementFromRawDataService = $this->createMock(CreateElementFromRawDataService::class); + $createElementFromRawDataService + ->expects($this->once()) + ->method('createElementFromRawData') + ->with( + $this->callback(fn ($arg) => $arg instanceof UuidInterface), + 'User', + null, + null, + [] + ) + ->willReturn($userNode); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); $urlGenerator->method('generate')->willReturn('url'); $postRegisterController = $this->getPostRegisterController( - emberNexusConfiguration: $emberNexusConfiguration, - userPasswordHasher: new UserPasswordHasher(), + cypherEntityManager: $cypherEntityManager, router: $urlGenerator, - cypherEntityManager: $cypherEntityManager + userPasswordHasher: new UserPasswordHasher(), + emberNexusConfiguration: $emberNexusConfiguration, + createElementFromRawDataService: $createElementFromRawDataService ); $request = $this->createMock(Request::class); @@ -102,6 +126,69 @@ public function testPostRegisterWithEnabledRegister(): void $this->assertSame('url', $response->headers->get('Location')); } + public function testPostRegisterWithEnabledRegisterAndData(): void + { + $emberNexusConfiguration = $this->createMock(EmberNexusConfiguration::class); + $emberNexusConfiguration->method('getRegisterUniqueIdentifier')->willReturn('email'); + $emberNexusConfiguration->method('isRegisterEnabled')->willReturn(true); + + $requestUtilService = new RequestUtilService( + $emberNexusConfiguration, + $this->createMock(Client400BadContentExceptionFactory::class), + $this->createMock(Client400MissingPropertyExceptionFactory::class), + ); + + $null = null; + $cypherClient = $this->createMock(ClientInterface::class); + $cypherClient->expects($this->once()) + ->method('runStatement') + ->willReturn(new SummarizedResult( + $null, + [ + new CypherMap([ + 'count' => 0, + ]), + ] + )); + $cypherEntityManager = $this->createMock(EntityManager::class); + $cypherEntityManager->method('getClient')->willReturn($cypherClient); + + $userNode = new NodeElement(); + $createElementFromRawDataService = $this->createMock(CreateElementFromRawDataService::class); + $createElementFromRawDataService + ->expects($this->once()) + ->method('createElementFromRawData') + ->with( + $this->callback(fn ($arg) => $arg instanceof UuidInterface), + 'User', + null, + null, + [ + 'a' => 'b', + ] + ) + ->willReturn($userNode); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator->method('generate')->willReturn('url'); + $postRegisterController = $this->getPostRegisterController( + cypherEntityManager: $cypherEntityManager, + router: $urlGenerator, + userPasswordHasher: new UserPasswordHasher(), + emberNexusConfiguration: $emberNexusConfiguration, + requestUtilService: $requestUtilService, + createElementFromRawDataService: $createElementFromRawDataService + ); + + $request = $this->createMock(Request::class); + $request->method('getContent')->willReturn('{"type": "User", "password": "1234", "uniqueUserIdentifier": "test@example.com", "data": {"a": "b"}}'); + + $response = $postRegisterController->postRegister($request); + $this->assertInstanceOf(CreatedResponse::class, $response); + $this->assertSame(201, $response->getStatusCode()); + $this->assertSame('url', $response->headers->get('Location')); + } + public function testPostRegisterWithDisabledRegister(): void { $emberNexusConfiguration = $this->createMock(EmberNexusConfiguration::class); @@ -155,8 +242,8 @@ public function testCheckForDuplicateUniqueUserIdentifierWithDuplicate(): void $cypherEntityManager = $this->createMock(EntityManager::class); $cypherEntityManager->method('getClient')->willReturn($cypherClient); $postRegisterController = $this->getPostRegisterController( - emberNexusConfiguration: $emberNexusConfiguration, - cypherEntityManager: $cypherEntityManager + cypherEntityManager: $cypherEntityManager, + emberNexusConfiguration: $emberNexusConfiguration ); $method = new ReflectionMethod(PostRegisterController::class, 'checkForDuplicateUniqueUserIdentifier'); @@ -195,8 +282,8 @@ public function testCheckForDuplicateUniqueUserIdentifierWithNoDuplicate(): void $cypherEntityManager = $this->createMock(EntityManager::class); $cypherEntityManager->method('getClient')->willReturn($cypherClient); $postRegisterController = $this->getPostRegisterController( - emberNexusConfiguration: $emberNexusConfiguration, - cypherEntityManager: $cypherEntityManager + cypherEntityManager: $cypherEntityManager, + emberNexusConfiguration: $emberNexusConfiguration ); $method = new ReflectionMethod(PostRegisterController::class, 'checkForDuplicateUniqueUserIdentifier'); @@ -204,39 +291,6 @@ public function testCheckForDuplicateUniqueUserIdentifierWithNoDuplicate(): void $method->invokeArgs($postRegisterController, [$userId]); } - public function testCreateUserNode(): void - { - $emberNexusConfiguration = $this->createMock(EmberNexusConfiguration::class); - $emberNexusConfiguration->method('getRegisterUniqueIdentifier')->willReturn('email'); - $postRegisterController = $this->getPostRegisterController( - emberNexusConfiguration: $emberNexusConfiguration, - userPasswordHasher: new UserPasswordHasher() - ); - $method = new ReflectionMethod(PostRegisterController::class, 'createUserNode'); - - $userId = Uuid::uuid4(); - $data = []; - $uniqueUserIdentifier = 'test@localhost.dev'; - $password = '1234'; - $userNode = $method->invokeArgs($postRegisterController, [$userId, $data, $uniqueUserIdentifier, $password]); - $this->assertInstanceOf(NodeElement::class, $userNode); - $this->assertSame($userId, $userNode->getId()); - $this->assertSame($uniqueUserIdentifier, $userNode->getProperty('email')); - $this->assertTrue($userNode->hasProperty('_passwordHash')); - - $userId = Uuid::uuid4(); - $data = [ - 'email' => 'manual-specified-email-which-should-be-overwritten@localhost.dev', - ]; - $uniqueUserIdentifier = 'test@localhost.dev'; - $password = '1234'; - $userNode = $method->invokeArgs($postRegisterController, [$userId, $data, $uniqueUserIdentifier, $password]); - $this->assertInstanceOf(NodeElement::class, $userNode); - $this->assertSame($userId, $userNode->getId()); - $this->assertSame($uniqueUserIdentifier, $userNode->getProperty('email')); - $this->assertTrue($userNode->hasProperty('_passwordHash')); - } - public function testCreateCreatedResponse(): void { $urlGenerator = $this->createMock(UrlGeneratorInterface::class); From d9fcf9df64ff997e3b3443bb495bd7228ab45596 Mon Sep 17 00:00:00 2001 From: Syndesi Date: Tue, 23 Dec 2025 22:14:15 +0100 Subject: [PATCH 2/2] make changes compatible with pre-0.1.6-specification --- src/Controller/User/PostRegisterController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Controller/User/PostRegisterController.php b/src/Controller/User/PostRegisterController.php index 05f33ad7..aa059330 100644 --- a/src/Controller/User/PostRegisterController.php +++ b/src/Controller/User/PostRegisterController.php @@ -63,6 +63,11 @@ public function postRegister(Request $request): Response $this->checkForDuplicateUniqueUserIdentifier($uniqueUserIdentifier); $uniqueIdentifier = $this->emberNexusConfiguration->getRegisterUniqueIdentifier(); + if (array_key_exists($uniqueIdentifier, $rawData)) { + // remove unique identifier from data payload, was required in releases before 0.1.6 + unset($rawData[$uniqueIdentifier]); + } + $userNode = $this->createElementFromRawDataService->createElementFromRawData( $userId, 'User',