Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 19 additions & 22 deletions src/Controller/User/PostRegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,13 +25,17 @@

class PostRegisterController extends AbstractController
{
/**
* @SuppressWarnings("PHPMD.ExcessiveParameterList")
*/
public function __construct(
private ElementManager $elementManager,
private CypherEntityManager $cypherEntityManager,
private UrlGeneratorInterface $router,
private UserPasswordHasher $userPasswordHasher,
private EmberNexusConfiguration $emberNexusConfiguration,
private RequestUtilService $requestUtilService,
private CreateElementFromRawDataService $createElementFromRawDataService,
private Client400ReservedIdentifierExceptionFactory $client400ReservedIdentifierExceptionFactory,
private Client403ForbiddenExceptionFactory $client403ForbiddenExceptionFactory,
private Server500LogicExceptionFactory $server500LogicExceptionFactory,
Expand All @@ -50,16 +54,27 @@ 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();
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',
rawData: $rawData
);
$userNode->addProperty($uniqueIdentifier, $uniqueUserIdentifier);
$userNode->addProperty('_passwordHash', $this->userPasswordHasher->hashPassword($password));
$this->elementManager->create($userNode);
$this->elementManager->flush();

Expand Down Expand Up @@ -90,24 +105,6 @@ private function checkForDuplicateUniqueUserIdentifier(string $uniqueUserIdentif
}
}

/**
* @param array<string, mixed> $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(
Expand Down
18 changes: 18 additions & 0 deletions tests/FeatureTests/Endpoint/User/PostRegisterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
134 changes: 94 additions & 40 deletions tests/UnitTests/Controller/User/PostRegisterControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -39,13 +43,17 @@ class PostRegisterControllerTest extends TestCase
{
use ProphecyTrait;

/**
* @SuppressWarnings("PHPMD.ExcessiveParameterList")
*/
private function getPostRegisterController(
?ElementManager $elementManager = null,
?EntityManager $cypherEntityManager = null,
?UrlGeneratorInterface $router = null,
?UserPasswordHasher $userPasswordHasher = null,
?EmberNexusConfiguration $emberNexusConfiguration = null,
?RequestUtilService $requestUtilService = null,
?CreateElementFromRawDataService $createElementFromRawDataService = null,
?Client400ReservedIdentifierExceptionFactory $client400ReservedIdentifierExceptionFactory = null,
?Client403ForbiddenExceptionFactory $client403ForbiddenExceptionFactory = null,
?Server500LogicExceptionFactory $server500LogicExceptionFactory = null,
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -195,48 +282,15 @@ 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');

$userId = Uuid::uuid4();
$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);
Expand Down