diff --git a/api/src/Etemplate.php b/api/src/Etemplate.php index 50c4805ba8..c242a13c37 100644 --- a/api/src/Etemplate.php +++ b/api/src/Etemplate.php @@ -344,7 +344,7 @@ class Etemplate extends Etemplate\Widget\Template { error_log(__METHOD__."(,".array2string($_content).') validation_errors='.array2string(self::$validation_errors)); self::$response->generic('et2_validation_error', self::$validation_errors); - exit; + return; } // tell request call to remove request, if it is not modified eg. by call to exec in callback diff --git a/api/src/Etemplate/Widget.php b/api/src/Etemplate/Widget.php index c19d6eca16..0ab3b25454 100644 --- a/api/src/Etemplate/Widget.php +++ b/api/src/Etemplate/Widget.php @@ -889,7 +889,7 @@ class Widget public static function set_validation_error($name,$error,$cname=null) { // not yet used: if (is_null($cname)) $cname = self::$name_vars; - error_log(__METHOD__."('$name','$error','$cname') ".function_backtrace()); + //error_log(__METHOD__."('$name','$error','$cname') ".function_backtrace()); if ($cname) $name = self::form_name($cname,$name); diff --git a/api/src/Etemplate/Widget/Textbox.php b/api/src/Etemplate/Widget/Textbox.php index bf5c135071..e182d5d039 100644 --- a/api/src/Etemplate/Widget/Textbox.php +++ b/api/src/Etemplate/Widget/Textbox.php @@ -120,7 +120,7 @@ class Textbox extends Etemplate\Widget { if (!isset($this->attrs['validator'])) { - switch($this->type) + switch($this->attrs['type']) { case 'int': case 'integer': @@ -158,7 +158,7 @@ class Textbox extends Etemplate\Widget } if ($this->attrs['validator'] && !preg_match($this->attrs['validator'],$value)) { - switch($this->type) + switch($this->attrs['type']) { case 'integer': self::set_validation_error($form_name,lang("'%1' is not a valid integer !!!",$value),''); @@ -171,21 +171,21 @@ class Textbox extends Etemplate\Widget break; } } - elseif ($this->type == 'integer' || $this->type == 'float') // cast int and float and check range + elseif ($this->attrs['type'] == 'integer' || $this->attrs['type'] == 'float') // cast int and float and check range { if ((string)$value !== '' || $this->attrs['needed']) // empty values are Ok if needed is not set { - $value = $this->type == 'integer' ? (int) $value : (float) str_replace(',','.',$value); // allow for german (and maybe other) format + $value = $this->attrs['type'] == 'integer' ? (int) $value : (float) str_replace(',','.',$value); // allow for german (and maybe other) format if (!empty($this->attrs['min']) && $value < $this->attrs['min']) { self::set_validation_error($form_name,lang("Value has to be at least '%1' !!!",$this->attrs['min']),''); - $value = $this->type == 'integer' ? (int) $this->attrs['min'] : (float) $this->attrs['min']; + $value = $this->attrs['type'] == 'integer' ? (int) $this->attrs['min'] : (float) $this->attrs['min']; } if (!empty($this->attrs['max']) && $value > $this->attrs['max']) { self::set_validation_error($form_name,lang("Value has to be at maximum '%1' !!!",$this->attrs['max']),''); - $value = $this->type == 'integer' ? (int) $this->attrs['max'] : (float) $this->attrs['max']; + $value = $this->attrs['type'] == 'integer' ? (int) $this->attrs['max'] : (float) $this->attrs['max']; } } } diff --git a/api/src/Etemplate/Widget/test/FloatTest.php b/api/src/Etemplate/Widget/test/FloatTest.php new file mode 100644 index 0000000000..0095e36c87 --- /dev/null +++ b/api/src/Etemplate/Widget/test/FloatTest.php @@ -0,0 +1,197 @@ +read(static::TEST_TEMPLATE, 'test'); + + // Content - doesn't really matter, we're changing it + $content = array( + 'widget' => 'Hello' + ); + + $this->validateRoundTrip($etemplate, $content, array('widget' => $value), + $error ? array() : array('widget' => $expected), + $error ? array('widget' => $error) : array() + ); + } + + /** + * Data provider for float tests + */ + public function floatProvider() + { + return array( + // User value, Expected Error + array('', '', false), + array(1, 1, false), + array(0, 0, false), + array(-1, -1, false), + array(1.5, 1.5, false), + array('1,5', 1.5, false), // Comma as separator is handled + array('one', '', true) + ); + } + + /** + * Test for float minimum attribute + * + * @param String|numeric $value + * @param float $min Minimum allowed value + * @param boolean $error + * + * @dataProvider minProvider + */ + public function testMin($value, $min, $error) + { + // Instanciate the template + $etemplate = new Etemplate(); + $etemplate->read(static::TEST_TEMPLATE, 'test'); + + // Content - doesn't really matter, we're changing it + $content = array( + 'widget' => 'Hello', + 'widget_readonly' => 'World' + ); + $result = $this->mockedExec($etemplate, $content, array(), array(), array()); + + // Only lowercase + $etemplate->getElementById('widget')->attrs['min'] = $min; + + // Check for the load + $data = array(); + foreach($result as $command) + { + if($command['type'] == 'et2_load') + { + $data = $command['data']; + break; + } + } + + // 'Edit' the data client side + $data['data']['content'] = array('widget' => $value); + + // Let it validate + Etemplate::ajax_process_content($data['data']['etemplate_exec_id'], $data['data']['content'], false); + + $content = static::$mocked_exec_result; + static::$mocked_exec_result = array(); + + return $this->validateTest($content, + $error ? array() : array('widget' => $value), + $error ? array('widget' => $error) : array() + ); + } + + public function minProvider() + { + return Array( + // User value, Min, Error + array('', 0, FALSE), + array(1.0, 0, FALSE), + array(0.0, 0, FALSE), + array(-1.0, 0, TRUE), + array(1.5, 0, FALSE), + array(1, 10, FALSE), + array(10, 10, FALSE), + array(1.5, 1.5, FALSE), + array(2, 1.5, TRUE), + ); + } + + /** + * Test for float maximum attribute + * + * @param String|numeric $value + * @param float $max Maximum allowed value + * @param boolean $error + * + * @dataProvider maxProvider + */ + public function testMax($value, $max, $error) + { + // Instanciate the template + $etemplate = new Etemplate(); + $etemplate->read(static::TEST_TEMPLATE, 'test'); + + // Content - doesn't really matter, we're changing it + $content = array( + 'widget' => 'Hello', + 'widget_readonly' => 'World' + ); + $result = $this->mockedExec($etemplate, $content, array(), array(), array()); + + // Only lowercase + $etemplate->getElementById('widget')->attrs['max'] = $max; + + // Check for the load + $data = array(); + foreach($result as $command) + { + if($command['type'] == 'et2_load') + { + $data = $command['data']; + break; + } + } + + // 'Edit' the data client side + $data['data']['content'] = array('widget' => $value); + + // Let it validate + Etemplate::ajax_process_content($data['data']['etemplate_exec_id'], $data['data']['content'], false); + + $content = static::$mocked_exec_result; + static::$mocked_exec_result = array(); + + return $this->validateTest($content, + $error ? array() : array('widget' => $value), + $error ? array('widget' => $error) : array() + ); + } + + public function maxProvider() + { + return Array( + // User value, Max, Error + array('', 0, FALSE), + array(1.0, 0, TRUE), + array(0, 0, FALSE), + array(-1.0, 0, FALSE), + array(1.5, 2, FALSE), + array(1, 10, FALSE), + array(10, 10, FALSE), + array(2.5, 2, TRUE), + array(1.5, 2.5, FALSE), + array(3, 2.5, FALSE), + ); + } +} diff --git a/api/src/Etemplate/Widget/test/IntegerTest.php b/api/src/Etemplate/Widget/test/IntegerTest.php new file mode 100644 index 0000000000..83d2ee6400 --- /dev/null +++ b/api/src/Etemplate/Widget/test/IntegerTest.php @@ -0,0 +1,192 @@ +read(static::TEST_TEMPLATE, 'test'); + + // Content - doesn't really matter, we're changing it + $content = array( + 'widget' => 'Hello', + 'widget_readonly' => 'World' + ); + + $this->validateRoundTrip($etemplate, $content, array('widget' => $value), + $error ? array() : array('widget' => $value), + $error ? array('widget' => $error) : array() + ); + } + + /** + * Data provider for integer tests + */ + public function integerProvider() + { + return array( + // User value, Error + array('', false), + array(1, false), + array(0, false), + array(-1, false), + array(1.5, true), + array('one', true) + ); + } + + /** + * Test for integer minimum attribute + * + * @param String|numeric $value + * @param int $min Minimum allowed value + * @param boolean $error + * + * @dataProvider minProvider + */ + public function testMin($value, $min, $error) + { + // Instanciate the template + $etemplate = new Etemplate(); + $etemplate->read(static::TEST_TEMPLATE, 'test'); + + // Content - doesn't really matter, we're changing it + $content = array( + 'widget' => 'Hello', + 'widget_readonly' => 'World' + ); + $result = $this->mockedExec($etemplate, $content, array(), array(), array()); + + // Only lowercase + $etemplate->getElementById('widget')->attrs['min'] = $min; + + // Check for the load + $data = array(); + foreach($result as $command) + { + if($command['type'] == 'et2_load') + { + $data = $command['data']; + break; + } + } + + // 'Edit' the data client side + $data['data']['content'] = array('widget' => $value); + + // Let it validate + Etemplate::ajax_process_content($data['data']['etemplate_exec_id'], $data['data']['content'], false); + + $content = static::$mocked_exec_result; + static::$mocked_exec_result = array(); + + return $this->validateTest($content, + $error ? array() : array('widget' => $value), + $error ? array('widget' => $error) : array() + ); + } + + public function minProvider() + { + return Array( + // User value, Min, Error + array('', 0, FALSE), + array(1, 0, FALSE), + array(0, 0, FALSE), + array(-1, 0, TRUE), + array(1.5, 0, TRUE), // Errors because it's not an int + array(1, 10, TRUE), + array(10, 10, FALSE), + ); + } + + /** + * Test for integer maximum attribute + * + * @param String|numeric $value + * @param int $max Maximum allowed value + * @param boolean $error + * + * @dataProvider maxProvider + */ + public function testMax($value, $max, $error) + { + // Instanciate the template + $etemplate = new Etemplate(); + $etemplate->read(static::TEST_TEMPLATE, 'test'); + + // Content - doesn't really matter, we're changing it + $content = array( + 'widget' => 'Hello', + 'widget_readonly' => 'World' + ); + $result = $this->mockedExec($etemplate, $content, array(), array(), array()); + + // Only lowercase + $etemplate->getElementById('widget')->attrs['max'] = $max; + + // Check for the load + $data = array(); + foreach($result as $command) + { + if($command['type'] == 'et2_load') + { + $data = $command['data']; + break; + } + } + + // 'Edit' the data client side + $data['data']['content'] = array('widget' => $value); + + // Let it validate + Etemplate::ajax_process_content($data['data']['etemplate_exec_id'], $data['data']['content'], false); + + $content = static::$mocked_exec_result; + static::$mocked_exec_result = array(); + + return $this->validateTest($content, + $error ? array() : array('widget' => $value), + $error ? array('widget' => $error) : array() + ); + } + + public function maxProvider() + { + return Array( + // User value, Max, Error + array('', 0, FALSE), + array(1, 0, TRUE), + array(0, 0, FALSE), + array(-1, 0, FALSE), + array(1.5, 2, TRUE), // Errors because it's not an int + array(1, 10, FALSE), + array(10, 10, FALSE), + ); + } +} diff --git a/api/src/Etemplate/Widget/test/TextboxTest.php b/api/src/Etemplate/Widget/test/TextboxTest.php index eea83987a7..08d538a19f 100644 --- a/api/src/Etemplate/Widget/test/TextboxTest.php +++ b/api/src/Etemplate/Widget/test/TextboxTest.php @@ -17,11 +17,6 @@ require_once realpath(__DIR__.'/../../test/WidgetBaseTest.php'); use EGroupware\Api\Etemplate; -/** - * Description of TextboxTest - * - * @author nathan - */ class TextboxTest extends \EGroupware\Api\Etemplate\WidgetBaseTest { @@ -29,6 +24,7 @@ class TextboxTest extends \EGroupware\Api\Etemplate\WidgetBaseTest /** * Test the widget's basic functionallity - we put data in, it comes back + * unchanged. */ public function testBasic() { @@ -117,4 +113,60 @@ class TextboxTest extends \EGroupware\Api\Etemplate\WidgetBaseTest $this->assertEquals(array(), $content); } + + /** + * Test regex validation + * + * @dataProvider regexProvider + */ + public function testRegex($value, $error) + { + // Instanciate the template + $etemplate = new Etemplate(); + $etemplate->read(static::TEST_TEMPLATE, 'test'); + + // Content - doesn't really matter, we're changing it + $content = array( + 'widget' => 'Hello', + 'widget_readonly' => 'World' + ); + $result = $this->mockedExec($etemplate, $content, array(), array(), array()); + + // Only lowercase + $etemplate->getElementById('widget')->attrs['validator'] = '/[a-z]*$/'; + + // Check for the load + $data = array(); + foreach($result as $command) + { + if($command['type'] == 'et2_load') + { + $data = $command['data']; + break; + } + } + + // 'Edit' the data client side + $data['data']['content'] = array('widget' => $value); + + // Let it validate + Etemplate::ajax_process_content($data['data']['etemplate_exec_id'], $data['data']['content'], false); + + $content = static::$mocked_exec_result; + static::$mocked_exec_result = array(); + + return $this->validateTest($content, array('widget' => $value), $error ? array('widget' => $error) : array()); + } + + public function regexProvider() + { + return array( + // Value Errors + array('', FALSE), + array('Hello', TRUE), + array('hello', FALSE), + array(1234, TRUE), + array('hi1234',TRUE) + ); + } } diff --git a/api/src/Etemplate/test/WidgetBaseTest.php b/api/src/Etemplate/test/WidgetBaseTest.php index 5ff2e558ae..a6730a1e5e 100644 --- a/api/src/Etemplate/test/WidgetBaseTest.php +++ b/api/src/Etemplate/test/WidgetBaseTest.php @@ -145,4 +145,66 @@ abstract class WidgetBaseTest extends \EGroupware\Api\LoggedInTest { }); return $response; } + + /** + * Exec the template with the provided content, change the values according to + * $set_values, then validate against $expected_values + * + * @param \EGroupware\Api\Etemplate $etemplate + * @param array $content + * @param array $set_values + * @param array $expected_values + * @param array $validation_errors + */ + protected function validateRoundTrip(\EGroupware\Api\Etemplate $etemplate, Array $content, Array $set_values, Array $expected_values = null, Array $validation_errors = array()) + { + if(is_null($expected_values)) + { + $expected_values = $set_values; + } + $result = $this->mockedExec($etemplate, $content, array(), array(), array()); + + // Check for the load + $data = array(); + foreach($result as $command) + { + if($command['type'] == 'et2_load') + { + $data = $command['data']; + break; + } + } + + // 'Edit' the data client side + $data['data']['content'] = $set_values; + + // Let it validate + Etemplate::ajax_process_content($data['data']['etemplate_exec_id'], $data['data']['content'], false); + + $content = static::$mocked_exec_result; + static::$mocked_exec_result = array(); + + return $this->validateTest($content, $expected_values, $validation_errors); + } + + protected function validateTest($content, $expected_values, $validation_errors) + { + // Make validation errors accessible + $ref = new \ReflectionProperty('\\EGroupware\\Api\\Etemplate\\Widget', 'validation_errors'); + $ref->setAccessible(true); + $errors = $ref->getValue(); + + // Test values + foreach($expected_values as $widget_id => $value) + { + $this->assertEquals($value, $content[$widget_id], 'Widget "' . $widget_id . '" did not get expected value'); + } + + // Check validation errors + foreach($validation_errors as $widget_id => $errored) + { + $this->assertTrue(array_key_exists($widget_id, $validation_errors), "Widget $widget_id caused a validation error"); + } + $ref->setValue(array()); + } } diff --git a/api/templates/test/float_test.xet b/api/templates/test/float_test.xet new file mode 100644 index 0000000000..f28b0936bd --- /dev/null +++ b/api/templates/test/float_test.xet @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/api/templates/test/integer_test.xet b/api/templates/test/integer_test.xet new file mode 100644 index 0000000000..feaa37e4a2 --- /dev/null +++ b/api/templates/test/integer_test.xet @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file