Skip to content

Commit

Permalink
BAP-22903: Add a validator to check object uniqueness in a collection…
Browse files Browse the repository at this point in the history
… (#40226)
  • Loading branch information
vsoroka authored Jan 21, 2025
1 parent 181924a commit d95df9b
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Oro\Bundle\FormBundle\Tests\Functional\Validator\Constraints;

use Doctrine\Persistence\ObjectManager;
use Oro\Bundle\FormBundle\Validator\Constraints\UniqueEntity;
use Oro\Bundle\FormBundle\Validator\Constraints\UniqueEntityValidator;
use Oro\Bundle\TestFrameworkBundle\Entity\Product;
Expand All @@ -20,74 +19,38 @@
*/
class UniqueEntityValidatorTest extends WebTestCase
{
/** @var UniqueEntityValidator */
private mixed $validator;

/** @var ObjectManager */
private ObjectManager $em;
private UniqueEntityValidator $validator;

#[\Override]
protected function setUp(): void
{
$this->initClient([], self::generateBasicAuthHeader());

$doctrine = self::getContainer()->get('doctrine');
$this->em = $doctrine->getManager();

$this->validator = self::getContainer()->get('oro_form.test.validator_constraints.unique_entity');
}

private function createContext(Constraint $constraint): ExecutionContext
{
$translator = $this->createMock(TranslatorInterface::class);
$validator = $this->createMock(ValidatorInterface::class);
$contextualValidator = $this->createMock(ContextualValidatorInterface::class);

$translator->expects(self::any())
$translator = $this->createMock(TranslatorInterface::class);
$translator->expects(self::once())
->method('trans')
->willReturnArgument(0);

$context = new ExecutionContext(
$validator,
'root',
$translator
);

$context = new ExecutionContext($validator, 'root', $translator);
$context->setGroup('MyGroup');
$context->setNode('InvalidValue', null, null, 'property.path');
$context->setConstraint($constraint);

$validator->expects(self::any())
->method('inContext')
->with($context)
->willReturn($contextualValidator);
->willReturn($this->createMock(ContextualValidatorInterface::class));

return $context;
}

private function createViolation(
string $message,
string $root,
string $propertyPath,
string $invalidValue,
string $code,
UniqueEntity $constraint,
array $parameters = []
): ConstraintViolation {
return new ConstraintViolation(
$message,
$message,
$parameters,
$root,
$propertyPath,
$invalidValue,
null,
$code,
$constraint,
null
);
}

private function validate(Constraint $constraint, ExecutionContextInterface $context): void
{
$entity1 = new Product();
Expand All @@ -99,108 +62,91 @@ private function validate(Constraint $constraint, ExecutionContextInterface $con
$this->validator->initialize($context);
$this->validator->validate($entity1, $constraint);

self::assertSame(
0,
$violationsCount = count($context->getViolations()),
sprintf('0 violation expected. Got %u.', $violationsCount)
);
$violationsCount = count($context->getViolations());
self::assertSame(0, $violationsCount, sprintf('0 violation expected. Got %u.', $violationsCount));

$this->em->persist($entity1);
$this->em->flush();
$em = self::getContainer()->get('doctrine')->getManager();
$em->persist($entity1);
$em->flush();

$this->validator->initialize($context);
$this->validator->validate($entity1, $constraint);

self::assertSame(
0,
$violationsCount = count($context->getViolations()),
sprintf('0 violation expected. Got %u.', $violationsCount)
);
$violationsCount = count($context->getViolations());
self::assertSame(0, $violationsCount, sprintf('0 violation expected. Got %u.', $violationsCount));

$this->validator->validate($entity2, $constraint);
}

public function testValidateViolationsAtEntityLevel(): void
{
$constraint = new UniqueEntity(
[
'message' => 'myMessage',
'fields' => ['name'],
'em' => 'default',
'buildViolationAtEntityLevel' => true,
]
);

$constraint = new UniqueEntity([
'message' => 'myMessage',
'fields' => ['name'],
'em' => 'default',
'buildViolationAtEntityLevel' => true
]);
$context = $this->createContext($constraint);

$this->validate($constraint, $context);

$expectedViolation = $this->createViolation(
'myMessage',
'root',
'property.path',
'InvalidValue',
UniqueEntity::NOT_UNIQUE_ERROR,
$constraint,
[
'{{ unique_key }}' => '"name"',
'{{ unique_fields }}' => '"oro.testframework.product.name.label"',
]
);

self::assertCount(
1,
$context->getViolations(),
sprintf(
'1 violation expected. Got %u.',
count($context->getViolations())
)
sprintf('1 violation expected. Got %u.', count($context->getViolations()))
);
self::assertEquals(
new ConstraintViolation(
'myMessage',
'myMessage',
[
'{{ unique_key }}' => '"name"',
'{{ unique_fields }}' => '"oro.testframework.product.name.label"'
],
'root',
'property.path',
'InvalidValue',
null,
UniqueEntity::NOT_UNIQUE_ERROR,
$constraint
),
$context->getViolations()->get(0)
);

$violation = $context->getViolations()->get(0);

self::assertEquals($expectedViolation, $violation);
}

public function testValidateViolationsAtPropertyPathLevel(): void
{
$constraint = new UniqueEntity(
[
'message' => 'myMessage',
'fields' => ['name'],
'em' => 'default',
'buildViolationAtEntityLevel' => false,
]
);

$constraint = new UniqueEntity([
'message' => 'myMessage',
'fields' => ['name'],
'em' => 'default',
'buildViolationAtEntityLevel' => false,
]);
$context = $this->createContext($constraint);

$this->validate($constraint, $context);

$expectedViolation = $this->createViolation(
'myMessage',
'root',
'property.path.name',
'Foo',
UniqueEntity::NOT_UNIQUE_ERROR,
$constraint,
[
'{{ unique_key }}' => '"name"',
'{{ unique_fields }}' => '"oro.testframework.product.name.label"',
]
);

self::assertCount(
1,
$context->getViolations(),
sprintf(
'1 violation expected. Got %u.',
count($context->getViolations())
)
sprintf('1 violation expected. Got %u.', count($context->getViolations()))
);
self::assertEquals(
new ConstraintViolation(
'myMessage',
'myMessage',
[
'{{ unique_key }}' => '"name"',
'{{ unique_fields }}' => '"oro.testframework.product.name.label"',
],
'root',
'property.path.name',
'Foo',
null,
UniqueEntity::NOT_UNIQUE_ERROR,
$constraint
),
$context->getViolations()->get(0)
);

$violation = $context->getViolations()->get(0);

self::assertEquals($expectedViolation, $violation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace Oro\Bundle\FormBundle\Tests\Unit\Validator\Constraints;

use Doctrine\Common\Collections\AbstractLazyCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Oro\Bundle\FormBundle\Validator\Constraints\Unique;
use Oro\Bundle\FormBundle\Validator\Constraints\UniqueValidator;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Constraints\Unique as SymfonyUniqueConstraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
Expand All @@ -31,7 +33,7 @@ protected function createValidator(): UniqueValidator
public function testValidateWithInvalidConstraint(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate([], $this->createMock(SymfonyUniqueConstraint::class));
$this->validator->validate([], $this->createMock(Constraint::class));
}

public function testValidateWithNullValue(): void
Expand Down Expand Up @@ -67,8 +69,7 @@ public function testValidateWithDuplicateValues(): void
$this->validator->validate([1, 2, 2], $constraint);

$this->buildViolation('Duplicate value found.')
->setParameter('{{ value }}', 'array')
->setCode(SymfonyUniqueConstraint::IS_NOT_UNIQUE)
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}

Expand All @@ -84,35 +85,66 @@ public function testValidateWithFieldsAndReadableProperties(): void
$this->propertyAccessor->expects(self::atLeastOnce())
->method('getValue')
->willReturnMap([
[$object1, 'field1', 'value1'],
[$object1, 'field2', 'value2'],
[$object2, 'field1', 'value1'],
[$object2, 'field2', 'value3'],
[$object3, 'field1', 'value1'],
[$object3, 'field2', 'value2']
[$object1, 'field1', $object1->field1],
[$object1, 'field2', $object1->field2],
[$object2, 'field1', $object2->field1],
[$object2, 'field2', $object2->field2],
[$object3, 'field1', $object3->field1],
[$object3, 'field2', $object3->field2]
]);

$constraint = new Unique(['fields' => ['field1', 'field2']]);
$this->validator->validate([$object1, $object2, $object3], $constraint);

$this->buildViolation($constraint->message)
->setParameter('{{ value }}', 'array')
->setCode(SymfonyUniqueConstraint::IS_NOT_UNIQUE)
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}

public function testValidateWithNormalizer(): void
{
$constraint = new Unique([
'fields' => [],
'normalizer' => static fn ($value) => strtolower($value),
'normalizer' => function (string $value) {
return strtolower($value);
},
'message' => 'Duplicate value found.',
]);
$this->validator->validate(['Value', 'VALUE'], $constraint);

$this->buildViolation('Duplicate value found.')
->setParameter('{{ value }}', 'array')
->setCode(SymfonyUniqueConstraint::IS_NOT_UNIQUE)
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}

public function testValidateWithNormalizerWhenValidatedValueIsCollectionOfObjects(): void
{
$object1 = (object)['field1' => 'Value1', 'field2' => 'value2'];
$object2 = (object)['field1' => 'VALUE1', 'field2' => 'value3'];

$this->propertyAccessor->expects(self::atLeastOnce())
->method('isReadable')
->willReturn(true);
$this->propertyAccessor->expects(self::atLeastOnce())
->method('getValue')
->willReturnMap([
[$object1, 'field1', $object1->field1],
[$object1, 'field2', $object1->field2],
[$object2, 'field1', $object2->field1],
[$object2, 'field2', $object2->field2]
]);

$constraint = new Unique([
'fields' => ['field1'],
'normalizer' => function (array $value) {
return ['field1' => strtolower($value['field1'])];
},
'message' => 'Duplicate value found.',
]);
$this->validator->validate(new ArrayCollection([$object1, $object2]), $constraint);

$this->buildViolation('Duplicate value found.')
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}

Expand All @@ -129,4 +161,15 @@ public function testReduceElementKeysWithUnreadableProperty(): void

$this->assertNoViolation();
}

public function testShouldKeepLazyCollectionUninitialized()
{
$collection = $this->getMockForAbstractClass(AbstractLazyCollection::class);

$constraint = new Unique(['fields' => ['field1']]);
$this->validator->validate($collection, $constraint);

$this->assertNoViolation();
self::assertFalse($collection->isInitialized());
}
}
Loading

0 comments on commit d95df9b

Please sign in to comment.