Rewrite test for Icinga\Web\Form

refs #6011
This commit is contained in:
Johannes Meyer 2014-04-24 10:13:47 +02:00
parent f20d459000
commit 2b15d35dec
2 changed files with 147 additions and 232 deletions

View File

@ -29,18 +29,18 @@
namespace Icinga\Web; namespace Icinga\Web;
use \Zend_Controller_Request_Abstract; use Zend_Controller_Request_Abstract;
use \Zend_Form; use Zend_Form;
use \Zend_Config; use Zend_Config;
use \Zend_Form_Element_Submit; use Zend_Form_Element_Submit;
use \Zend_Form_Element_Reset; use Zend_Form_Element_Reset;
use \Zend_View_Interface; use Zend_View_Interface;
use \Icinga\Web\Form\Element\Note; use Icinga\Web\Form\Element\Note;
use \Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use \Icinga\Web\Form\Decorator\HelpText; use Icinga\Web\Form\Decorator\HelpText;
use \Icinga\Web\Form\Decorator\BootstrapForm; use Icinga\Web\Form\Decorator\BootstrapForm;
use \Icinga\Web\Form\InvalidCSRFTokenException; use Icinga\Web\Form\InvalidCSRFTokenException;
use \Icinga\Application\Config as IcingaConfig; use Icinga\Application\Config as IcingaConfig;
/** /**
* Base class for forms providing CSRF protection, confirmation logic and auto submission * Base class for forms providing CSRF protection, confirmation logic and auto submission
@ -52,7 +52,7 @@ class Form extends Zend_Form
* *
* @var Zend_Controller_Request_Abstract * @var Zend_Controller_Request_Abstract
*/ */
private $request; protected $request;
/** /**
* Main configuration * Main configuration
@ -61,14 +61,14 @@ class Form extends Zend_Form
* *
* @var IcingaConfig * @var IcingaConfig
*/ */
private $config; protected $config;
/** /**
* The preference object to use instead of the one from the user (used for testing) * The preference object to use instead of the one from the user (used for testing)
* *
* @var Zend_Config * @var Zend_Config
*/ */
private $preferences; protected $preferences;
/** /**
* Whether this form should NOT add random generated "challenge" tokens that are associated with the user's current * Whether this form should NOT add random generated "challenge" tokens that are associated with the user's current
@ -84,21 +84,21 @@ class Form extends Zend_Form
* *
* @var string * @var string
*/ */
private $tokenElementName = 'CSRFToken'; protected $tokenElementName = 'CSRFToken';
/** /**
* Flag to indicate that form is already build * Flag to indicate that form is already build
* *
* @var bool * @var bool
*/ */
private $created = false; protected $created = false;
/** /**
* Session id used for CSRF token generation * Session id used for CSRF token generation
* *
* @var string * @var string
*/ */
private $sessionId; protected $sessionId;
/** /**
* Label for submit button * Label for submit button
@ -107,7 +107,7 @@ class Form extends Zend_Form
* *
* @var string * @var string
*/ */
private $submitLabel; protected $submitLabel;
/** /**
* Label for cancel button * Label for cancel button
@ -116,7 +116,7 @@ class Form extends Zend_Form
* *
* @var string * @var string
*/ */
private $cancelLabel; protected $cancelLabel;
/** /**
* Last used note-id * Last used note-id
@ -125,21 +125,7 @@ class Form extends Zend_Form
* *
* @var int * @var int
*/ */
private $last_note_id = 0; protected $last_note_id = 0;
/**
* Decorator that replaces the DtDd Zend-Form default
*
* @var Form\Decorator\BootstrapFormDecorator
*/
private $formDecorator;
/**
* Whether to ignore users leaving the form with unsaved changes
*
* @var bool
*/
private $ignoreChangeDiscarding = false;
/** /**
* Getter for the session ID * Getter for the session ID
@ -147,26 +133,14 @@ class Form extends Zend_Form
* If the ID has never been set, the ID from session_id() is returned * If the ID has never been set, the ID from session_id() is returned
* *
* @return string * @return string
*
* @see session_id()
* @see setSessionId()
*/ */
public function getSessionId() public function getSessionId()
{ {
if (!$this->sessionId) { if (!$this->sessionId) {
$this->sessionId = session_id(); $this->sessionId = session_id();
} }
return $this->sessionId;
}
/** return $this->sessionId;
* Set whether to inform a user when he is about to discard changes (false, default) or not
*
* @param boolean $bool False to not inform users when they leave modified forms, otherwise true
*/
public function setIgnoreChangeDiscarding($bool)
{
$this->ignoreChangeDiscarding = (boolean) $bool;
} }
/** /**
@ -210,6 +184,7 @@ class Form extends Zend_Form
*/ */
protected function create() protected function create()
{ {
} }
/** /**
@ -217,6 +192,7 @@ class Form extends Zend_Form
*/ */
protected function preValidation(array $data) protected function preValidation(array $data)
{ {
} }
/** /**
@ -264,6 +240,7 @@ class Form extends Zend_Form
if ($this->config === null) { if ($this->config === null) {
$this->config = new Zend_Config(array(), true); $this->config = new Zend_Config(array(), true);
} }
return $this->config; return $this->config;
} }
@ -287,6 +264,7 @@ class Form extends Zend_Form
if ($this->preferences) { if ($this->preferences) {
return $this->preferences; return $this->preferences;
} }
return $this->getRequest()->getUser()->getPreferences(); return $this->getRequest()->getUser()->getPreferences();
} }
@ -297,7 +275,6 @@ class Form extends Zend_Form
*/ */
public function buildForm() public function buildForm()
{ {
if ($this->created === false) { if ($this->created === false) {
$this->initCsrfToken(); $this->initCsrfToken();
$this->create(); $this->create();
@ -314,11 +291,8 @@ class Form extends Zend_Form
if (!$this->getAction() && $this->getRequest()) { if (!$this->getAction() && $this->getRequest()) {
$this->setAction($this->getRequest()->getRequestUri()); $this->setAction($this->getRequest()->getRequestUri());
} }
$this->addElementDecorators();
$this->created = true; $this->created = true;
if (!$this->ignoreChangeDiscarding) {
//$this->setAttrib('data-icinga-component', 'app/form');
}
} }
} }
@ -335,16 +309,17 @@ class Form extends Zend_Form
/** /**
* Add cancel button to form * Add cancel button to form
*/ */
private function addCancelButton() protected function addCancelButton()
{ {
$cancelLabel = new Zend_Form_Element_Reset( $this->addElement(
new Zend_Form_Element_Reset(
array( array(
'name' => 'btn_reset', 'name' => 'btn_reset',
'label' => $this->cancelLabel, 'label' => $this->cancelLabel,
'class' => 'btn pull-right' 'class' => 'btn pull-right'
) )
)
); );
$this->addElement($cancelLabel);
} }
/** /**
@ -360,15 +335,16 @@ class Form extends Zend_Form
/** /**
* Add submit button to form * Add submit button to form
*/ */
private function addSubmitButton() protected function addSubmitButton()
{ {
$submitButton = new Zend_Form_Element_Submit( $this->addElement(
new Zend_Form_Element_Submit(
array( array(
'name' => 'btn_submit', 'name' => 'btn_submit',
'label' => $this->submitLabel, 'label' => $this->submitLabel
)
) )
); );
$this->addElement($submitButton);
} }
/** /**
@ -403,13 +379,12 @@ class Form extends Zend_Form
* *
* @throws ProgrammingError When an element is found which does not yet exist * @throws ProgrammingError When an element is found which does not yet exist
*/ */
final public function enableAutoSubmit($triggerElements) public function enableAutoSubmit($triggerElements)
{ {
foreach ($triggerElements as $elementName) { foreach ($triggerElements as $elementName) {
$element = $this->getElement($elementName); $element = $this->getElement($elementName);
if ($element !== null) { if ($element !== null) {
$element->setAttrib('onchange', '$(this.form).submit();'); $element->setAttrib('onchange', '$(this.form).submit();');
$element->setAttrib('data-icinga-form-autosubmit', true);
} else { } else {
throw new ProgrammingError( throw new ProgrammingError(
'You need to add the element "' . $elementName . '" to' . 'You need to add the element "' . $elementName . '" to' .
@ -426,8 +401,6 @@ class Form extends Zend_Form
* in the request is valid and gets repopulated in case its invalid. * in the request is valid and gets repopulated in case its invalid.
* *
* @return bool True when the form is submitted and valid, otherwise false * @return bool True when the form is submitted and valid, otherwise false
* @see Form::isValid()
* @see Form::isSubmitted()
*/ */
public function isSubmittedAndValid() public function isSubmittedAndValid()
{ {
@ -466,6 +439,7 @@ class Form extends Zend_Form
$checkData = $this->getRequest()->getParams(); $checkData = $this->getRequest()->getParams();
$submitted = isset($checkData['btn_submit']); $submitted = isset($checkData['btn_submit']);
} }
return $submitted; return $submitted;
} }
@ -474,13 +448,13 @@ class Form extends Zend_Form
* *
* This method should be used for testing purposes only * This method should be used for testing purposes only
* *
* @param bool $disabled Set true in order to disable CSRF tokens in this form (default: true), otherwise false * @param bool $disabled Set true in order to disable CSRF tokens in
* * this form (default: true), otherwise false
* @see tokenDisabled
*/ */
final public function setTokenDisabled($disabled = true) public function setTokenDisabled($disabled = true)
{ {
$this->tokenDisabled = (boolean) $disabled; $this->tokenDisabled = (boolean) $disabled;
if ($disabled === true) { if ($disabled === true) {
$this->removeElement($this->tokenElementName); $this->removeElement($this->tokenElementName);
} }
@ -489,20 +463,18 @@ class Form extends Zend_Form
/** /**
* Add CSRF counter measure field to form * Add CSRF counter measure field to form
*/ */
final public function initCsrfToken() public function initCsrfToken()
{ {
if ($this->tokenDisabled || $this->getElement($this->tokenElementName)) { if (!$this->tokenDisabled && $this->getElement($this->tokenElementName) === null) {
return;
}
$this->addElement( $this->addElement(
'hidden', 'hidden',
$this->tokenElementName, $this->tokenElementName,
array( array(
'value' => $this->generateCsrfTokenAsString(), 'value' => $this->generateCsrfTokenAsString()
'decorators' => array('ViewHelper')
) )
); );
} }
}
/** /**
* Test the submitted data for a correct CSRF token * Test the submitted data for a correct CSRF token
@ -511,18 +483,16 @@ class Form extends Zend_Form
* *
* @throws InvalidCSRFTokenException When CSRF Validation fails * @throws InvalidCSRFTokenException When CSRF Validation fails
*/ */
final public function assertValidCsrfToken(array $checkData) public function assertValidCsrfToken(array $checkData)
{ {
if ($this->tokenDisabled) { if (!$this->tokenDisabled) {
return;
}
if (!isset($checkData[$this->tokenElementName]) if (!isset($checkData[$this->tokenElementName])
|| !$this->hasValidCsrfToken($checkData[$this->tokenElementName]) || !$this->hasValidCsrfToken($checkData[$this->tokenElementName])
) { ) {
throw new InvalidCSRFTokenException(); throw new InvalidCSRFTokenException();
} }
} }
}
/** /**
* Check whether the form's CSRF token-field has a valid value * Check whether the form's CSRF token-field has a valid value
@ -531,12 +501,9 @@ class Form extends Zend_Form
* *
* @return bool * @return bool
*/ */
private function hasValidCsrfToken($elementValue) protected function hasValidCsrfToken($elementValue)
{ {
if ($this->getElement($this->tokenElementName) === null) { if ($this->getElement($this->tokenElementName) === null || strpos($elementValue, '|') === false) {
return false;
}
if (strpos($elementValue, '|') === false) {
return false; return false;
} }
@ -549,26 +516,12 @@ class Form extends Zend_Form
return $token === hash('sha256', $this->getSessionId() . $seed); return $token === hash('sha256', $this->getSessionId() . $seed);
} }
/**
* Add element decorators which apply to all elements
*
* Adds `HelpText` decorator
*
* @see HelpText
*/
private function addElementDecorators()
{
foreach ($this->getElements() as $element) {
$element->addDecorator(new HelpText());
}
}
/** /**
* Generate a new (seed, token) pair * Generate a new (seed, token) pair
* *
* @return array * @return array
*/ */
final public function generateCsrfToken() public function generateCsrfToken()
{ {
$seed = mt_rand(); $seed = mt_rand();
$hash = hash('sha256', $this->getSessionId() . $seed); $hash = hash('sha256', $this->getSessionId() . $seed);
@ -581,7 +534,7 @@ class Form extends Zend_Form
* *
* @return string * @return string
*/ */
final public function generateCsrfTokenAsString() public function generateCsrfTokenAsString()
{ {
list ($seed, $token) = $this->generateCsrfToken($this->getSessionId()); list ($seed, $token) = $this->generateCsrfToken($this->getSessionId());
return sprintf('%s|%s', $seed, $token); return sprintf('%s|%s', $seed, $token);
@ -593,31 +546,29 @@ class Form extends Zend_Form
* Additionally, all DtDd tags will be removed and the Bootstrap compatible * Additionally, all DtDd tags will be removed and the Bootstrap compatible
* BootstrapForm decorator will be added to the elements * BootstrapForm decorator will be added to the elements
* *
*
* @param string|Zend_Form_Element $element String element type, or an object of type Zend_Form_Element * @param string|Zend_Form_Element $element String element type, or an object of type Zend_Form_Element
* @param string $name The name of the element to add if $element is a string * @param string $name The name of the element to add if $element is a string
* @param array $options The settings for the element if $element is a string * @param array $options The settings for the element if $element is a string
* *
* @return Form * @return self
* @see Zend_Form::addElement() * @see Zend_Form::addElement()
*/ */
public function addElement($element, $name = null, $options = null) public function addElement($element, $name = null, $options = null)
{ {
parent::addElement($element, $name, $options); parent::addElement($element, $name, $options);
$el = $name ? $this->getElement($name) : $element; $el = $name !== null ? $this->getElement($name) : $element;
// Do not add structural elements to invisible elements
// which produces ugly views
if (strpos(strtolower(get_class($el)), 'hidden') !== false) {
$el->setDecorators(array('ViewHelper'));
return $this;
}
if ($el) { if ($el) {
if (strpos(strtolower(get_class($el)), 'hidden') !== false) {
// Do not add structural elements to invisible elements which produces ugly views
$el->setDecorators(array('ViewHelper'));
} else {
$el->removeDecorator('HtmlTag'); $el->removeDecorator('HtmlTag');
$el->removeDecorator('Label'); $el->removeDecorator('Label');
$el->removeDecorator('DtDdWrapper'); $el->removeDecorator('DtDdWrapper');
$el->addDecorator(new BootstrapForm()); $el->addDecorator(new BootstrapForm());
$el->addDecorator(new HelpText());
}
} }
return $this; return $this;
@ -626,7 +577,9 @@ class Form extends Zend_Form
/** /**
* Load the default decorators * Load the default decorators
* *
* @return Zend_Form * Overwrites Zend_Form::loadDefaultDecorators to avoid having the HtmlTag-Decorator added
*
* @return self
*/ */
public function loadDefaultDecorators() public function loadDefaultDecorators()
{ {
@ -637,8 +590,10 @@ class Form extends Zend_Form
$decorators = $this->getDecorators(); $decorators = $this->getDecorators();
if (empty($decorators)) { if (empty($decorators)) {
$this->addDecorator('FormElements') $this->addDecorator('FormElements')
//->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form'))
->addDecorator('Form'); ->addDecorator('Form');
} }
return $this; return $this;
} }
} }

View File

@ -7,95 +7,55 @@ namespace Tests\Icinga\Web;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
/**
* Dummy extension class as Icinga\Web\Form is an abstract one
*/
class TestForm extends Form
{
public function create()
{
}
}
/**
* Tests for the Icinga\Web\Form class (Base class for all other forms)
*/
class FormTest extends BaseTestCase class FormTest extends BaseTestCase
{ {
/** public function testWhetherAddElementDoesNotAddSpecificDecorators()
* Tests whether the cancel label will be added to the form
*/
function testCancelLabel()
{ {
$form = new TestForm(); $form = new Form();
$form->setCancelLabel('Cancel'); $form->addElement('text', 'someText');
$form->buildForm(); $element = $form->getElement('someText');
$this->assertCount(2, $form->getElements(), 'Asserting that the cancel label is present');
$this->assertFalse(
$element->getDecorator('HtmlTag'),
'Form::addElement does not remove the HtmlTag-Decorator'
);
$this->assertFalse(
$element->getDecorator('Label'),
'Form::addElement does not remove the Label-Decorator'
);
$this->assertFalse(
$element->getDecorator('DtDdWrapper'),
'Form::addElement does not remove the DtDdWrapper-Decorator'
);
} }
/** public function testWhetherAddElementDoesNotAddAnyOptionalDecoratorsToHiddenElements()
* Tests whether the submit button will be added to the form
*/
function testSubmitButton()
{ {
$form = new TestForm(); $form = new Form();
$form->setSubmitLabel('Submit'); $form->addElement('hidden', 'somethingHidden');
$form->buildForm(); $element = $form->getElement('somethingHidden');
$this->assertCount(2, $form->getElements(), 'Asserting that the submit button is present');
$this->assertCount(
1,
$element->getDecorators(),
'Form::addElement adds more decorators than necessary to hidden elements'
);
$this->assertInstanceOf(
'\Zend_Form_Decorator_ViewHelper',
$element->getDecorator('ViewHelper'),
'Form::addElement does not add the ViewHelper-Decorator to hidden elements'
);
} }
/** public function testWhetherLoadDefaultDecoratorsDoesNotAddTheHtmlTagDecorator()
* Tests whether automatic form submission will be enabled for a single field
*/
function testEnableAutoSubmitSingle()
{ {
$form = new TestForm(); $form = new Form();
$form->addElement('checkbox', 'example1', array()); $form->loadDefaultDecorators();
$form->enableAutoSubmit(array('example1'));
$this->assertArrayHasKey('data-icinga-form-autosubmit', $form->getElement('example1')->getAttribs(),
'Asserting that auto-submit got enabled for one element');
}
/** $this->assertArrayNotHasKey(
* Tests whether automatic form submission will be enabled for multiple fields 'HtmlTag',
*/ $form->getDecorators(),
function testEnableAutoSubmitMultiple() 'Form::loadDefaultDecorators adds the HtmlTag-Decorator'
{ );
$form = new TestForm();
$form->addElement('checkbox', 'example1', array());
$form->addElement('checkbox', 'example2', array());
$form->enableAutoSubmit(array('example1', 'example2'));
$this->assertArrayHasKey('data-icinga-form-autosubmit', $form->getElement('example1')->getAttribs(),
'Asserting that auto-submit got enabled for multiple elements');
$this->assertArrayHasKey('data-icinga-form-autosubmit', $form->getElement('example2')->getAttribs(),
'Asserting that auto-submit got enabled for multiple elements');
}
/**
* Tests whether automatic form submission can only be enabled for existing elements
*
* @expectedException Icinga\Exception\ProgrammingError
*/
function testEnableAutoSubmitExisting()
{
$form = new TestForm();
$form->enableAutoSubmit(array('not_existing'));
}
/**
* Tests whether a form will be detected as properly submitted
*/
function testFormSubmission()
{
$form = new TestForm();
$form->setTokenDisabled();
$form->setSubmitLabel('foo');
$request = $this->getRequest();
$form->setRequest($request->setMethod('GET'));
$this->assertFalse($form->isSubmittedAndValid(),
'Asserting that it is not possible to submit a form not using POST');
$request->setMethod('POST')->setPost(array('btn_submit' => 'foo'));
$this->assertTrue($form->isSubmittedAndValid(),
'Asserting that it is possible to detect a form as submitted');
} }
} }