FormValidatorTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <?php
  2. namespace Drupal\Tests\Core\Form;
  3. use Drupal\Core\Form\FormState;
  4. use Drupal\Tests\UnitTestCase;
  5. use Symfony\Component\HttpFoundation\Request;
  6. use Symfony\Component\HttpFoundation\RequestStack;
  7. /**
  8. * @coversDefaultClass \Drupal\Core\Form\FormValidator
  9. * @group Form
  10. */
  11. class FormValidatorTest extends UnitTestCase {
  12. /**
  13. * A logger instance.
  14. *
  15. * @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject
  16. */
  17. protected $logger;
  18. /**
  19. * The CSRF token generator to validate the form token.
  20. *
  21. * @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit\Framework\MockObject\MockObject
  22. */
  23. protected $csrfToken;
  24. /**
  25. * The form error handler.
  26. *
  27. * @var \Drupal\Core\Form\FormErrorHandlerInterface|\PHPUnit\Framework\MockObject\MockObject
  28. */
  29. protected $formErrorHandler;
  30. /**
  31. * {@inheritdoc}
  32. */
  33. protected function setUp() {
  34. parent::setUp();
  35. $this->logger = $this->createMock('Psr\Log\LoggerInterface');
  36. $this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
  37. ->disableOriginalConstructor()
  38. ->getMock();
  39. $this->formErrorHandler = $this->createMock('Drupal\Core\Form\FormErrorHandlerInterface');
  40. }
  41. /**
  42. * Tests the 'validation_complete' $form_state flag.
  43. *
  44. * @covers ::validateForm
  45. * @covers ::finalizeValidation
  46. */
  47. public function testValidationComplete() {
  48. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  49. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  50. ->setMethods(NULL)
  51. ->getMock();
  52. $form = [];
  53. $form_state = new FormState();
  54. $this->assertFalse($form_state->isValidationComplete());
  55. $form_validator->validateForm('test_form_id', $form, $form_state);
  56. $this->assertTrue($form_state->isValidationComplete());
  57. }
  58. /**
  59. * Tests the 'must_validate' $form_state flag.
  60. *
  61. * @covers ::validateForm
  62. */
  63. public function testPreventDuplicateValidation() {
  64. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  65. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  66. ->setMethods(['doValidateForm'])
  67. ->getMock();
  68. $form_validator->expects($this->never())
  69. ->method('doValidateForm');
  70. $form = [];
  71. $form_state = (new FormState())
  72. ->setValidationComplete();
  73. $form_validator->validateForm('test_form_id', $form, $form_state);
  74. $this->assertArrayNotHasKey('#errors', $form);
  75. }
  76. /**
  77. * Tests the 'must_validate' $form_state flag.
  78. *
  79. * @covers ::validateForm
  80. */
  81. public function testMustValidate() {
  82. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  83. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  84. ->setMethods(['doValidateForm'])
  85. ->getMock();
  86. $form_validator->expects($this->once())
  87. ->method('doValidateForm');
  88. $this->formErrorHandler->expects($this->once())
  89. ->method('handleFormErrors');
  90. $form = [];
  91. $form_state = (new FormState())
  92. ->setValidationComplete()
  93. ->setValidationEnforced();
  94. $form_validator->validateForm('test_form_id', $form, $form_state);
  95. }
  96. /**
  97. * @covers ::validateForm
  98. */
  99. public function testValidateInvalidFormToken() {
  100. $request_stack = new RequestStack();
  101. $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/test/example?foo=bar']);
  102. $request_stack->push($request);
  103. $this->csrfToken->expects($this->once())
  104. ->method('validate')
  105. ->will($this->returnValue(FALSE));
  106. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  107. ->setConstructorArgs([$request_stack, $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  108. ->setMethods(['doValidateForm'])
  109. ->getMock();
  110. $form_validator->expects($this->never())
  111. ->method('doValidateForm');
  112. $form['#token'] = 'test_form_id';
  113. $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
  114. ->setMethods(['setErrorByName'])
  115. ->getMock();
  116. $form_state->expects($this->once())
  117. ->method('setErrorByName')
  118. ->with('form_token', 'The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.');
  119. $form_state->setValue('form_token', 'some_random_token');
  120. $form_validator->validateForm('test_form_id', $form, $form_state);
  121. $this->assertTrue($form_state->isValidationComplete());
  122. }
  123. /**
  124. * @covers ::validateForm
  125. */
  126. public function testValidateValidFormToken() {
  127. $request_stack = new RequestStack();
  128. $this->csrfToken->expects($this->once())
  129. ->method('validate')
  130. ->will($this->returnValue(TRUE));
  131. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  132. ->setConstructorArgs([$request_stack, $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  133. ->setMethods(['doValidateForm'])
  134. ->getMock();
  135. $form_validator->expects($this->once())
  136. ->method('doValidateForm');
  137. $form['#token'] = 'test_form_id';
  138. $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
  139. ->setMethods(['setErrorByName'])
  140. ->getMock();
  141. $form_state->expects($this->never())
  142. ->method('setErrorByName');
  143. $form_state->setValue('form_token', 'some_random_token');
  144. $form_validator->validateForm('test_form_id', $form, $form_state);
  145. $this->assertTrue($form_state->isValidationComplete());
  146. }
  147. /**
  148. * @covers ::handleErrorsWithLimitedValidation
  149. *
  150. * @dataProvider providerTestHandleErrorsWithLimitedValidation
  151. */
  152. public function testHandleErrorsWithLimitedValidation($sections, $triggering_element, $values, $expected) {
  153. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  154. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  155. ->setMethods(NULL)
  156. ->getMock();
  157. $triggering_element['#limit_validation_errors'] = $sections;
  158. $form = [];
  159. $form_state = (new FormState())
  160. ->setValues($values)
  161. ->setTriggeringElement($triggering_element);
  162. $form_validator->validateForm('test_form_id', $form, $form_state);
  163. $this->assertSame($expected, $form_state->getValues());
  164. }
  165. public function providerTestHandleErrorsWithLimitedValidation() {
  166. return [
  167. // Test with a non-existent section.
  168. [
  169. [['test1'], ['test3']],
  170. [],
  171. [
  172. 'test1' => 'foo',
  173. 'test2' => 'bar',
  174. ],
  175. [
  176. 'test1' => 'foo',
  177. ],
  178. ],
  179. // Test with buttons in a non-validated section.
  180. [
  181. [['test1']],
  182. [
  183. '#is_button' => TRUE,
  184. '#value' => 'baz',
  185. '#name' => 'op',
  186. '#parents' => ['submit'],
  187. ],
  188. [
  189. 'test1' => 'foo',
  190. 'test2' => 'bar',
  191. 'op' => 'baz',
  192. 'submit' => 'baz',
  193. ],
  194. [
  195. 'test1' => 'foo',
  196. 'submit' => 'baz',
  197. 'op' => 'baz',
  198. ],
  199. ],
  200. // Test with a matching button #value and $form_state value.
  201. [
  202. [['submit']],
  203. [
  204. '#is_button' => TRUE,
  205. '#value' => 'baz',
  206. '#name' => 'op',
  207. '#parents' => ['submit'],
  208. ],
  209. [
  210. 'test1' => 'foo',
  211. 'test2' => 'bar',
  212. 'op' => 'baz',
  213. 'submit' => 'baz',
  214. ],
  215. [
  216. 'submit' => 'baz',
  217. 'op' => 'baz',
  218. ],
  219. ],
  220. // Test with a mismatched button #value and $form_state value.
  221. [
  222. [['submit']],
  223. [
  224. '#is_button' => TRUE,
  225. '#value' => 'bar',
  226. '#name' => 'op',
  227. '#parents' => ['submit'],
  228. ],
  229. [
  230. 'test1' => 'foo',
  231. 'test2' => 'bar',
  232. 'op' => 'baz',
  233. 'submit' => 'baz',
  234. ],
  235. [
  236. 'submit' => 'baz',
  237. ],
  238. ],
  239. ];
  240. }
  241. /**
  242. * @covers ::executeValidateHandlers
  243. */
  244. public function testExecuteValidateHandlers() {
  245. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  246. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  247. ->setMethods(NULL)
  248. ->getMock();
  249. $mock = $this->getMockBuilder('stdClass')
  250. ->setMethods(['validate_handler', 'hash_validate'])
  251. ->getMock();
  252. $mock->expects($this->once())
  253. ->method('validate_handler')
  254. ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'));
  255. $mock->expects($this->once())
  256. ->method('hash_validate')
  257. ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'));
  258. $form = [];
  259. $form_state = new FormState();
  260. $form_validator->executeValidateHandlers($form, $form_state);
  261. $form['#validate'][] = [$mock, 'hash_validate'];
  262. $form_validator->executeValidateHandlers($form, $form_state);
  263. // $form_state validate handlers will supersede $form handlers.
  264. $validate_handlers[] = [$mock, 'validate_handler'];
  265. $form_state->setValidateHandlers($validate_handlers);
  266. $form_validator->executeValidateHandlers($form, $form_state);
  267. }
  268. /**
  269. * @covers ::doValidateForm
  270. *
  271. * @dataProvider providerTestRequiredErrorMessage
  272. */
  273. public function testRequiredErrorMessage($element, $expected_message) {
  274. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  275. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  276. ->setMethods(['executeValidateHandlers'])
  277. ->getMock();
  278. $form_validator->expects($this->once())
  279. ->method('executeValidateHandlers');
  280. $form = [];
  281. $form['test'] = $element + [
  282. '#type' => 'textfield',
  283. '#value' => '',
  284. '#needs_validation' => TRUE,
  285. '#required' => TRUE,
  286. '#parents' => ['test'],
  287. ];
  288. $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
  289. ->setMethods(['setError'])
  290. ->getMock();
  291. $form_state->expects($this->once())
  292. ->method('setError')
  293. ->with($this->isType('array'), $expected_message);
  294. $form_validator->validateForm('test_form_id', $form, $form_state);
  295. }
  296. public function providerTestRequiredErrorMessage() {
  297. return [
  298. [
  299. // Use the default message with a title.
  300. ['#title' => 'Test'],
  301. 'Test field is required.',
  302. ],
  303. // Use a custom message.
  304. [
  305. ['#required_error' => 'FAIL'],
  306. 'FAIL',
  307. ],
  308. // No title or custom message.
  309. [
  310. [],
  311. '',
  312. ],
  313. ];
  314. }
  315. /**
  316. * @covers ::doValidateForm
  317. */
  318. public function testElementValidate() {
  319. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  320. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  321. ->setMethods(['executeValidateHandlers'])
  322. ->getMock();
  323. $form_validator->expects($this->once())
  324. ->method('executeValidateHandlers');
  325. $mock = $this->getMockBuilder('stdClass')
  326. ->setMethods(['element_validate'])
  327. ->getMock();
  328. $mock->expects($this->once())
  329. ->method('element_validate')
  330. ->with($this->isType('array'), $this->isInstanceOf('Drupal\Core\Form\FormStateInterface'), NULL);
  331. $form = [];
  332. $form['test'] = [
  333. '#type' => 'textfield',
  334. '#title' => 'Test',
  335. '#parents' => ['test'],
  336. '#element_validate' => [[$mock, 'element_validate']],
  337. ];
  338. $form_state = new FormState();
  339. $form_validator->validateForm('test_form_id', $form, $form_state);
  340. }
  341. /**
  342. * @covers ::performRequiredValidation
  343. *
  344. * @dataProvider providerTestPerformRequiredValidation
  345. */
  346. public function testPerformRequiredValidation($element, $expected_message, $call_watchdog) {
  347. $form_validator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
  348. ->setConstructorArgs([new RequestStack(), $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $this->formErrorHandler])
  349. ->setMethods(['setError'])
  350. ->getMock();
  351. if ($call_watchdog) {
  352. $this->logger->expects($this->once())
  353. ->method('error')
  354. ->with($this->isType('string'), $this->isType('array'));
  355. }
  356. $form = [];
  357. $form['test'] = $element + [
  358. '#title' => 'Test',
  359. '#needs_validation' => TRUE,
  360. '#required' => FALSE,
  361. '#parents' => ['test'],
  362. ];
  363. $form_state = $this->getMockBuilder('Drupal\Core\Form\FormState')
  364. ->setMethods(['setError'])
  365. ->getMock();
  366. $form_state->expects($this->once())
  367. ->method('setError')
  368. ->with($this->isType('array'), $expected_message);
  369. $form_validator->validateForm('test_form_id', $form, $form_state);
  370. }
  371. public function providerTestPerformRequiredValidation() {
  372. return [
  373. [
  374. [
  375. '#type' => 'select',
  376. '#options' => [
  377. 'foo' => 'Foo',
  378. 'bar' => 'Bar',
  379. ],
  380. '#required' => TRUE,
  381. '#value' => 'baz',
  382. '#empty_value' => 'baz',
  383. '#multiple' => FALSE,
  384. ],
  385. 'Test field is required.',
  386. FALSE,
  387. ],
  388. [
  389. [
  390. '#type' => 'select',
  391. '#options' => [
  392. 'foo' => 'Foo',
  393. 'bar' => 'Bar',
  394. ],
  395. '#value' => 'baz',
  396. '#multiple' => FALSE,
  397. ],
  398. 'An illegal choice has been detected. Please contact the site administrator.',
  399. TRUE,
  400. ],
  401. [
  402. [
  403. '#type' => 'checkboxes',
  404. '#options' => [
  405. 'foo' => 'Foo',
  406. 'bar' => 'Bar',
  407. ],
  408. '#value' => ['baz'],
  409. '#multiple' => TRUE,
  410. ],
  411. 'An illegal choice has been detected. Please contact the site administrator.',
  412. TRUE,
  413. ],
  414. [
  415. [
  416. '#type' => 'select',
  417. '#options' => [
  418. 'foo' => 'Foo',
  419. 'bar' => 'Bar',
  420. ],
  421. '#value' => ['baz'],
  422. '#multiple' => TRUE,
  423. ],
  424. 'An illegal choice has been detected. Please contact the site administrator.',
  425. TRUE,
  426. ],
  427. [
  428. [
  429. '#type' => 'textfield',
  430. '#maxlength' => 7,
  431. '#value' => $this->randomMachineName(8),
  432. ],
  433. 'Test cannot be longer than <em class="placeholder">7</em> characters but is currently <em class="placeholder">8</em> characters long.',
  434. FALSE,
  435. ],
  436. ];
  437. }
  438. }