ConstraintValidatorTestCase.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Validator\Test;
  11. use PHPUnit\Framework\Assert;
  12. use PHPUnit\Framework\Constraint\IsIdentical;
  13. use PHPUnit\Framework\Constraint\IsInstanceOf;
  14. use PHPUnit\Framework\Constraint\IsNull;
  15. use PHPUnit\Framework\Constraint\LogicalOr;
  16. use PHPUnit\Framework\ExpectationFailedException;
  17. use PHPUnit\Framework\TestCase;
  18. use Symfony\Component\Validator\Constraint;
  19. use Symfony\Component\Validator\Constraints\NotNull;
  20. use Symfony\Component\Validator\Constraints\Valid;
  21. use Symfony\Component\Validator\ConstraintValidatorInterface;
  22. use Symfony\Component\Validator\ConstraintViolation;
  23. use Symfony\Component\Validator\ConstraintViolationInterface;
  24. use Symfony\Component\Validator\ConstraintViolationList;
  25. use Symfony\Component\Validator\Context\ExecutionContext;
  26. use Symfony\Component\Validator\Context\ExecutionContextInterface;
  27. use Symfony\Component\Validator\Mapping\ClassMetadata;
  28. use Symfony\Component\Validator\Mapping\PropertyMetadata;
  29. use Symfony\Component\Validator\Validator\ContextualValidatorInterface;
  30. use Symfony\Component\Validator\Validator\ValidatorInterface;
  31. use Symfony\Contracts\Translation\TranslatorInterface;
  32. /**
  33. * A test case to ease testing Constraint Validators.
  34. *
  35. * @author Bernhard Schussek <bschussek@gmail.com>
  36. */
  37. abstract class ConstraintValidatorTestCase extends TestCase
  38. {
  39. /**
  40. * @var ExecutionContextInterface
  41. */
  42. protected $context;
  43. /**
  44. * @var ConstraintValidatorInterface
  45. */
  46. protected $validator;
  47. protected $group;
  48. protected $metadata;
  49. protected $object;
  50. protected $value;
  51. protected $root;
  52. protected $propertyPath;
  53. protected $constraint;
  54. protected $defaultTimezone;
  55. private $expectedViolations;
  56. private $call;
  57. protected function setUp(): void
  58. {
  59. $this->group = 'MyGroup';
  60. $this->metadata = null;
  61. $this->object = null;
  62. $this->value = 'InvalidValue';
  63. $this->root = 'root';
  64. $this->propertyPath = 'property.path';
  65. // Initialize the context with some constraint so that we can
  66. // successfully build a violation.
  67. $this->constraint = new NotNull();
  68. $this->context = $this->createContext();
  69. $this->validator = $this->createValidator();
  70. $this->validator->initialize($this->context);
  71. $this->expectedViolations = [];
  72. $this->call = 0;
  73. \Locale::setDefault('en');
  74. $this->setDefaultTimezone('UTC');
  75. }
  76. protected function tearDown(): void
  77. {
  78. $this->restoreDefaultTimezone();
  79. }
  80. protected function setDefaultTimezone($defaultTimezone)
  81. {
  82. // Make sure this method can not be called twice before calling
  83. // also restoreDefaultTimezone()
  84. if (null === $this->defaultTimezone) {
  85. $this->defaultTimezone = date_default_timezone_get();
  86. date_default_timezone_set($defaultTimezone);
  87. }
  88. }
  89. protected function restoreDefaultTimezone()
  90. {
  91. if (null !== $this->defaultTimezone) {
  92. date_default_timezone_set($this->defaultTimezone);
  93. $this->defaultTimezone = null;
  94. }
  95. }
  96. protected function createContext()
  97. {
  98. $translator = $this->createMock(TranslatorInterface::class);
  99. $translator->expects($this->any())->method('trans')->willReturnArgument(0);
  100. $validator = $this->createMock(ValidatorInterface::class);
  101. $validator->expects($this->any())
  102. ->method('validate')
  103. ->willReturnCallback(function () {
  104. return $this->expectedViolations[$this->call++] ?? new ConstraintViolationList();
  105. });
  106. $context = new ExecutionContext($validator, $this->root, $translator);
  107. $context->setGroup($this->group);
  108. $context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath);
  109. $context->setConstraint($this->constraint);
  110. $contextualValidator = $this->getMockBuilder(AssertingContextualValidator::class)
  111. ->setConstructorArgs([$context])
  112. ->setMethods([
  113. 'atPath',
  114. 'validate',
  115. 'validateProperty',
  116. 'validatePropertyValue',
  117. 'getViolations',
  118. ])
  119. ->getMock();
  120. $contextualValidator->expects($this->any())
  121. ->method('atPath')
  122. ->willReturnCallback(function ($path) use ($contextualValidator) {
  123. return $contextualValidator->doAtPath($path);
  124. });
  125. $contextualValidator->expects($this->any())
  126. ->method('validate')
  127. ->willReturnCallback(function ($value, $constraints = null, $groups = null) use ($contextualValidator) {
  128. return $contextualValidator->doValidate($value, $constraints, $groups);
  129. });
  130. $contextualValidator->expects($this->any())
  131. ->method('validateProperty')
  132. ->willReturnCallback(function ($object, $propertyName, $groups = null) use ($contextualValidator) {
  133. return $contextualValidator->validateProperty($object, $propertyName, $groups);
  134. });
  135. $contextualValidator->expects($this->any())
  136. ->method('validatePropertyValue')
  137. ->willReturnCallback(function ($objectOrClass, $propertyName, $value, $groups = null) use ($contextualValidator) {
  138. return $contextualValidator->doValidatePropertyValue($objectOrClass, $propertyName, $value, $groups);
  139. });
  140. $contextualValidator->expects($this->any())
  141. ->method('getViolations')
  142. ->willReturnCallback(function () use ($contextualValidator) {
  143. return $contextualValidator->doGetViolations();
  144. });
  145. $validator->expects($this->any())
  146. ->method('inContext')
  147. ->with($context)
  148. ->willReturn($contextualValidator);
  149. return $context;
  150. }
  151. protected function setGroup($group)
  152. {
  153. $this->group = $group;
  154. $this->context->setGroup($group);
  155. }
  156. protected function setObject($object)
  157. {
  158. $this->object = $object;
  159. $this->metadata = \is_object($object)
  160. ? new ClassMetadata(\get_class($object))
  161. : null;
  162. $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath);
  163. }
  164. protected function setProperty($object, $property)
  165. {
  166. $this->object = $object;
  167. $this->metadata = \is_object($object)
  168. ? new PropertyMetadata(\get_class($object), $property)
  169. : null;
  170. $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath);
  171. }
  172. protected function setValue($value)
  173. {
  174. $this->value = $value;
  175. $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath);
  176. }
  177. protected function setRoot($root)
  178. {
  179. $this->root = $root;
  180. $this->context = $this->createContext();
  181. $this->validator->initialize($this->context);
  182. }
  183. protected function setPropertyPath($propertyPath)
  184. {
  185. $this->propertyPath = $propertyPath;
  186. $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath);
  187. }
  188. protected function expectNoValidate()
  189. {
  190. $validator = $this->context->getValidator()->inContext($this->context);
  191. $validator->expectNoValidate();
  192. }
  193. protected function expectValidateAt($i, $propertyPath, $value, $group)
  194. {
  195. $validator = $this->context->getValidator()->inContext($this->context);
  196. $validator->expectValidation($i, $propertyPath, $value, $group, function ($passedConstraints) {
  197. $expectedConstraints = new LogicalOr();
  198. $expectedConstraints->setConstraints([new IsNull(), new IsIdentical([]), new IsInstanceOf(Valid::class)]);
  199. Assert::assertThat($passedConstraints, $expectedConstraints);
  200. });
  201. }
  202. protected function expectValidateValue(int $i, $value, array $constraints = [], $group = null)
  203. {
  204. $contextualValidator = $this->context->getValidator()->inContext($this->context);
  205. $contextualValidator->expectValidation($i, '', $value, $group, function ($passedConstraints) use ($constraints) {
  206. if (\is_array($constraints) && !\is_array($passedConstraints)) {
  207. $passedConstraints = [$passedConstraints];
  208. }
  209. Assert::assertEquals($constraints, $passedConstraints);
  210. });
  211. }
  212. protected function expectFailingValueValidation(int $i, $value, array $constraints, $group, ConstraintViolationInterface $violation)
  213. {
  214. $contextualValidator = $this->context->getValidator()->inContext($this->context);
  215. $contextualValidator->expectValidation($i, '', $value, $group, function ($passedConstraints) use ($constraints) {
  216. if (\is_array($constraints) && !\is_array($passedConstraints)) {
  217. $passedConstraints = [$passedConstraints];
  218. }
  219. Assert::assertEquals($constraints, $passedConstraints);
  220. }, $violation);
  221. }
  222. protected function expectValidateValueAt($i, $propertyPath, $value, $constraints, $group = null)
  223. {
  224. $contextualValidator = $this->context->getValidator()->inContext($this->context);
  225. $contextualValidator->expectValidation($i, $propertyPath, $value, $group, function ($passedConstraints) use ($constraints) {
  226. Assert::assertEquals($constraints, $passedConstraints);
  227. });
  228. }
  229. protected function expectViolationsAt($i, $value, Constraint $constraint)
  230. {
  231. $context = $this->createContext();
  232. $validatorClassname = $constraint->validatedBy();
  233. $validator = new $validatorClassname();
  234. $validator->initialize($context);
  235. $validator->validate($value, $constraint);
  236. $this->expectedViolations[] = $context->getViolations();
  237. return $context->getViolations();
  238. }
  239. protected function assertNoViolation()
  240. {
  241. $this->assertSame(0, $violationsCount = \count($this->context->getViolations()), sprintf('0 violation expected. Got %u.', $violationsCount));
  242. }
  243. /**
  244. * @param $message
  245. *
  246. * @return ConstraintViolationAssertion
  247. */
  248. protected function buildViolation($message)
  249. {
  250. return new ConstraintViolationAssertion($this->context, $message, $this->constraint);
  251. }
  252. abstract protected function createValidator();
  253. }
  254. /**
  255. * @internal
  256. */
  257. class ConstraintViolationAssertion
  258. {
  259. /**
  260. * @var ExecutionContextInterface
  261. */
  262. private $context;
  263. /**
  264. * @var ConstraintViolationAssertion[]
  265. */
  266. private $assertions;
  267. private $message;
  268. private $parameters = [];
  269. private $invalidValue = 'InvalidValue';
  270. private $propertyPath = 'property.path';
  271. private $plural;
  272. private $code;
  273. private $constraint;
  274. private $cause;
  275. public function __construct(ExecutionContextInterface $context, string $message, Constraint $constraint = null, array $assertions = [])
  276. {
  277. $this->context = $context;
  278. $this->message = $message;
  279. $this->constraint = $constraint;
  280. $this->assertions = $assertions;
  281. }
  282. public function atPath(string $path)
  283. {
  284. $this->propertyPath = $path;
  285. return $this;
  286. }
  287. public function setParameter(string $key, $value)
  288. {
  289. $this->parameters[$key] = $value;
  290. return $this;
  291. }
  292. public function setParameters(array $parameters)
  293. {
  294. $this->parameters = $parameters;
  295. return $this;
  296. }
  297. public function setTranslationDomain($translationDomain)
  298. {
  299. // no-op for BC
  300. return $this;
  301. }
  302. public function setInvalidValue($invalidValue)
  303. {
  304. $this->invalidValue = $invalidValue;
  305. return $this;
  306. }
  307. public function setPlural(int $number)
  308. {
  309. $this->plural = $number;
  310. return $this;
  311. }
  312. public function setCode(string $code)
  313. {
  314. $this->code = $code;
  315. return $this;
  316. }
  317. public function setCause($cause)
  318. {
  319. $this->cause = $cause;
  320. return $this;
  321. }
  322. public function buildNextViolation(string $message): self
  323. {
  324. $assertions = $this->assertions;
  325. $assertions[] = $this;
  326. return new self($this->context, $message, $this->constraint, $assertions);
  327. }
  328. public function assertRaised()
  329. {
  330. $expected = [];
  331. foreach ($this->assertions as $assertion) {
  332. $expected[] = $assertion->getViolation();
  333. }
  334. $expected[] = $this->getViolation();
  335. $violations = iterator_to_array($this->context->getViolations());
  336. Assert::assertSame($expectedCount = \count($expected), $violationsCount = \count($violations), sprintf('%u violation(s) expected. Got %u.', $expectedCount, $violationsCount));
  337. reset($violations);
  338. foreach ($expected as $violation) {
  339. Assert::assertEquals($violation, current($violations));
  340. next($violations);
  341. }
  342. }
  343. private function getViolation(): ConstraintViolation
  344. {
  345. return new ConstraintViolation(
  346. $this->message,
  347. $this->message,
  348. $this->parameters,
  349. $this->context->getRoot(),
  350. $this->propertyPath,
  351. $this->invalidValue,
  352. $this->plural,
  353. $this->code,
  354. $this->constraint,
  355. $this->cause
  356. );
  357. }
  358. }
  359. /**
  360. * @internal
  361. */
  362. class AssertingContextualValidator implements ContextualValidatorInterface
  363. {
  364. private $context;
  365. private $expectNoValidate = false;
  366. private $atPathCalls = -1;
  367. private $expectedAtPath = [];
  368. private $validateCalls = -1;
  369. private $expectedValidate = [];
  370. public function __construct(ExecutionContextInterface $context)
  371. {
  372. $this->context = $context;
  373. }
  374. public function atPath($path)
  375. {
  376. }
  377. public function doAtPath($path)
  378. {
  379. Assert::assertFalse($this->expectNoValidate, 'No validation calls have been expected.');
  380. if (!isset($this->expectedAtPath[++$this->atPathCalls])) {
  381. throw new ExpectationFailedException(sprintf('Validation for property path "%s" was not expected.', $path));
  382. }
  383. Assert::assertSame($this->expectedAtPath[$this->atPathCalls], $path);
  384. return $this;
  385. }
  386. public function validate($value, $constraints = null, $groups = null)
  387. {
  388. }
  389. public function doValidate($value, $constraints = null, $groups = null)
  390. {
  391. Assert::assertFalse($this->expectNoValidate, 'No validation calls have been expected.');
  392. if (!isset($this->expectedValidate[++$this->validateCalls])) {
  393. return $this;
  394. }
  395. [$expectedValue, $expectedGroup, $expectedConstraints, $violation] = $this->expectedValidate[$this->validateCalls];
  396. Assert::assertSame($expectedValue, $value);
  397. $expectedConstraints($constraints);
  398. Assert::assertSame($expectedGroup, $groups);
  399. if (null !== $violation) {
  400. $this->context->addViolation($violation->getMessage(), $violation->getParameters());
  401. }
  402. return $this;
  403. }
  404. public function validateProperty($object, $propertyName, $groups = null)
  405. {
  406. }
  407. public function doValidateProperty($object, $propertyName, $groups = null)
  408. {
  409. return $this;
  410. }
  411. public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
  412. {
  413. }
  414. public function doValidatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
  415. {
  416. return $this;
  417. }
  418. public function getViolations()
  419. {
  420. }
  421. public function doGetViolations()
  422. {
  423. return $this->context->getViolations();
  424. }
  425. public function expectNoValidate()
  426. {
  427. $this->expectNoValidate = true;
  428. }
  429. public function expectValidation($call, $propertyPath, $value, $group, $constraints, ConstraintViolationInterface $violation = null)
  430. {
  431. $this->expectedAtPath[$call] = $propertyPath;
  432. $this->expectedValidate[$call] = [$value, $group, $constraints, $violation];
  433. }
  434. }