Files
chill-bundles/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/PersonJsonDenormalizerTest.php

303 lines
11 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonDenormalizer;
use libphonenumber\PhoneNumber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/**
* @internal
*
* @covers \Chill\PersonBundle\Serializer\Normalizer\PersonJsonDenormalizer
*/
final class PersonJsonDenormalizerTest extends TestCase
{
private function createIdentifierManager(): PersonIdentifierManagerInterface
{
return new class () implements PersonIdentifierManagerInterface {
public function getWorkers(): array
{
return [];
}
public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
{
if (is_int($personIdentifierDefinition)) {
$definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy');
// Force the id for testing purposes
$r = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
$r->setAccessible(true);
$r->setValue($definition, $personIdentifierDefinition);
} else {
$definition = $personIdentifierDefinition;
}
$engine = new class () implements PersonIdentifierEngineInterface {
public static function getName(): string
{
return 'dummy';
}
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
{
// trivial canonicalization for tests
return isset($value['content']) ? (string) $value['content'] : null;
}
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
{
return '';
}
public function isEmpty(PersonIdentifier $identifier): bool
{
$value = $identifier->getValue();
$content = isset($value['content']) ? trim((string) $value['content']) : '';
return '' === $content;
}
};
return new PersonIdentifierWorker($engine, $definition);
}
};
}
public function testSupportsDenormalizationReturnsTrueForValidData(): void
{
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
$data = [
'type' => 'person',
// important: new Person (creation) must not contain an id
];
self::assertTrue($denormalizer->supportsDenormalization($data, Person::class));
}
public function testSupportsDenormalizationReturnsFalseForInvalidData(): void
{
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
// not an array
self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class));
// missing type
self::assertFalse($denormalizer->supportsDenormalization([], Person::class));
// wrong type value
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person'], Person::class));
// id present means it's not a create payload for this denormalizer
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 123], Person::class));
// wrong target class
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], \stdClass::class));
}
public function testDenormalizeMapsPayloadToPersonProperties(): void
{
$json = <<<'JSON'
{
"type": "person",
"firstName": "Jérome",
"lastName": "diallo",
"altNames": [
{
"key": "jeune_fille",
"value": "FJ"
}
],
"birthdate": null,
"deathdate": null,
"phonenumber": "",
"mobilenumber": "",
"email": "",
"gender": {
"id": 5,
"type": "chill_main_gender"
},
"center": {
"id": 1,
"type": "center"
},
"civility": null,
"identifiers": [
{
"type": "person_identifier",
"value": {
"content": "789456"
},
"definition_id": 5
}
]
}
JSON;
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$inner = new class () implements DenormalizerInterface {
public ?Gender $gender = null;
public ?Center $center = null;
public function denormalize($data, $type, $format = null, array $context = [])
{
if (PhoneNumber::class === $type) {
return '' === $data ? null : new PhoneNumber();
}
if (\DateTime::class === $type || \DateTimeImmutable::class === $type) {
return null === $data ? null : new \DateTimeImmutable((string) $data);
}
if (Gender::class === $type) {
return $this->gender ??= new Gender();
}
if (Center::class === $type) {
return $this->center ??= new Center();
}
if (Civility::class === $type) {
return null; // input is null in our payload
}
return null;
}
public function supportsDenormalization($data, $type, $format = null)
{
return true;
}
};
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
$denormalizer->setDenormalizer($inner);
$person = $denormalizer->denormalize($data, Person::class);
self::assertInstanceOf(Person::class, $person);
self::assertSame('Jérome', $person->getFirstName());
self::assertSame('diallo', $person->getLastName());
// phone numbers: empty strings map to null via the inner denormalizer stub
self::assertNull($person->getPhonenumber());
self::assertNull($person->getMobilenumber());
// email passes through as is
self::assertSame('', $person->getEmail());
// nested objects are provided by our inner denormalizer and must be set back on the Person
self::assertSame($inner->gender, $person->getGender());
self::assertSame($inner->center, $person->getCenter());
// dates are null in the provided payload
self::assertNull($person->getBirthdate());
self::assertNull($person->getDeathdate());
// civility is null as provided
self::assertNull($person->getCivility());
// altNames: make sure the alt name with key jeune_fille has label FJ
$found = false;
foreach ($person->getAltNames() as $altName) {
if ('jeune_fille' === $altName->getKey()) {
$found = true;
self::assertSame('FJ', $altName->getLabel());
}
}
self::assertTrue($found, 'Expected altNames to contain key "jeune_fille"');
$found = false;
foreach ($person->getIdentifiers() as $identifier) {
if (5 === $identifier->getDefinition()->getId()) {
$found = true;
self::assertSame(['content' => '789456'], $identifier->getValue());
}
}
self::assertTrue($found, 'Expected identifiers with definition id 5');
}
public function testDenormalizeRemovesEmptyIdentifier(): void
{
$data = [
'type' => 'person',
'firstName' => 'Alice',
'lastName' => 'Smith',
'identifiers' => [
[
'type' => 'person_identifier',
'value' => ['content' => ''],
'definition_id' => 7,
],
],
];
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
$person = $denormalizer->denormalize($data, Person::class);
// The identifier with empty content must be considered empty and removed
self::assertSame(0, $person->getIdentifiers()->count(), 'Expected no identifiers to remain on the person');
}
public function testDenormalizeRemovesPreviouslyExistingIdentifierWhenIncomingValueIsEmpty(): void
{
// Prepare an existing Person with a pre-existing identifier (definition id = 9)
$definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy');
$ref = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
$ref->setValue($definition, 9);
$existingIdentifier = new PersonIdentifier($definition);
$existingIdentifier->setValue(['content' => 'ABC']);
$person = new Person();
$person->addIdentifier($existingIdentifier);
// Also set the identifier's own id = 9 so that the denormalizer logic matches it
// (the current denormalizer matches by PersonIdentifier->getId() === definition_id)
$refId = new \ReflectionProperty(PersonIdentifier::class, 'id');
$refId->setValue($existingIdentifier, 9);
// Incoming payload sets the same definition id with an empty value
$data = [
'type' => 'person',
'identifiers' => [
[
'type' => 'person_identifier',
'value' => ['content' => ''],
'definition_id' => 9,
],
],
];
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
// Use AbstractNormalizer::OBJECT_TO_POPULATE to update the existing person
$result = $denormalizer->denormalize($data, Person::class, null, [
AbstractNormalizer::OBJECT_TO_POPULATE => $person,
]);
self::assertSame($person, $result, 'Denormalizer should update and return the provided Person instance');
self::assertSame(0, $person->getIdentifiers()->count(), 'The previously existing identifier should be removed when incoming value is empty');
}
}