From bcce2625a8cb0b630d945c6849014049869e10ce Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 27 Nov 2018 12:01:36 +0100 Subject: Implemented AuthController for login * Moved /login functionality to AuthController * Refactored password handling logic to use the Authenticator --- tests/Unit/Http/UrlGeneratorServiceProviderTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'tests/Unit/Http') diff --git a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php index 61bf3e7c..6d18f160 100644 --- a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php +++ b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php @@ -19,7 +19,7 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest $urlGenerator = $this->getMockBuilder(UrlGenerator::class) ->getMock(); - $app = $this->getApp(); + $app = $this->getApp(['make', 'instance', 'bind']); $this->setExpects($app, 'make', [UrlGenerator::class], $urlGenerator); $app->expects($this->exactly(2)) @@ -29,6 +29,9 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest ['http.urlGenerator', $urlGenerator], [UrlGeneratorInterface::class, $urlGenerator] ); + $app->expects($this->once()) + ->method('bind') + ->with(UrlGeneratorInterface::class, UrlGenerator::class); $serviceProvider = new UrlGeneratorServiceProvider($app); $serviceProvider->register(); -- cgit v1.2.3-70-g09d2 From 7414f9b23dbcc66e5f0efda3d0cbfd79372ec780 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 9 Jul 2019 21:43:18 +0200 Subject: Implemented Validation for controllers --- config/app.php | 1 + src/Controllers/BaseController.php | 4 + src/Http/Exceptions/ValidationException.php | 37 +++ src/Http/Validation/Validates.php | 154 +++++++++++ src/Http/Validation/ValidatesRequest.php | 37 +++ src/Http/Validation/ValidationServiceProvider.php | 28 ++ src/Http/Validation/Validator.php | 76 +++++ src/Middleware/ErrorHandler.php | 30 ++ tests/Unit/Controllers/BaseControllerTest.php | 2 + .../Http/Exceptions/ValidationExceptionTest.php | 25 ++ .../Stub/ValidatesRequestImplementation.php | 27 ++ .../Unit/Http/Validation/ValidatesRequestTest.php | 46 +++ tests/Unit/Http/Validation/ValidatesTest.php | 308 +++++++++++++++++++++ .../Validation/ValidationServiceProviderTest.php | 34 +++ tests/Unit/Http/Validation/ValidatorTest.php | 50 ++++ tests/Unit/Middleware/ErrorHandlerTest.php | 70 ++++- 16 files changed, 927 insertions(+), 2 deletions(-) create mode 100644 src/Http/Exceptions/ValidationException.php create mode 100644 src/Http/Validation/Validates.php create mode 100644 src/Http/Validation/ValidatesRequest.php create mode 100644 src/Http/Validation/ValidationServiceProvider.php create mode 100644 src/Http/Validation/Validator.php create mode 100644 tests/Unit/Http/Exceptions/ValidationExceptionTest.php create mode 100644 tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php create mode 100644 tests/Unit/Http/Validation/ValidatesRequestTest.php create mode 100644 tests/Unit/Http/Validation/ValidatesTest.php create mode 100644 tests/Unit/Http/Validation/ValidationServiceProviderTest.php create mode 100644 tests/Unit/Http/Validation/ValidatorTest.php (limited to 'tests/Unit/Http') diff --git a/config/app.php b/config/app.php index 5fda67dd..c4503086 100644 --- a/config/app.php +++ b/config/app.php @@ -25,6 +25,7 @@ return [ \Engelsystem\Middleware\RouteDispatcherServiceProvider::class, \Engelsystem\Middleware\RequestHandlerServiceProvider::class, \Engelsystem\Middleware\SessionHandlerServiceProvider::class, + \Engelsystem\Http\Validation\ValidationServiceProvider::class, // Additional services \Engelsystem\Mail\MailerServiceProvider::class, diff --git a/src/Controllers/BaseController.php b/src/Controllers/BaseController.php index cbc00931..655ed759 100644 --- a/src/Controllers/BaseController.php +++ b/src/Controllers/BaseController.php @@ -2,8 +2,12 @@ namespace Engelsystem\Controllers; +use Engelsystem\Http\Validation\ValidatesRequest; + abstract class BaseController { + use ValidatesRequest; + /** @var string[]|string[][] A list of Permissions required to access the controller or certain pages */ protected $permissions = []; diff --git a/src/Http/Exceptions/ValidationException.php b/src/Http/Exceptions/ValidationException.php new file mode 100644 index 00000000..e48fb0c3 --- /dev/null +++ b/src/Http/Exceptions/ValidationException.php @@ -0,0 +1,37 @@ +validator = $validator; + parent::__construct($message, $code, $previous); + } + + /** + * @return Validator + */ + public function getValidator(): Validator + { + return $this->validator; + } +} diff --git a/src/Http/Validation/Validates.php b/src/Http/Validation/Validates.php new file mode 100644 index 00000000..2e3a1a73 --- /dev/null +++ b/src/Http/Validation/Validates.php @@ -0,0 +1,154 @@ +validateParameterCount(2, $parameters, __FUNCTION__); + $size = $this->getSize($value); + + return $size >= $parameters[0] && $size <= $parameters[1]; + } + + /** + * @param mixed $value + * @return bool + */ + public function bool($value): bool + { + return in_array($value, ['1', 1, true, '0', 0, false], true); + } + + /** + * @param mixed $value + * @param array $parameters ['1,2,3,56,7'] + * @return bool + */ + public function in($value, $parameters): bool + { + $this->validateParameterCount(1, $parameters, __FUNCTION__); + + return in_array($value, explode(',', $parameters[0])); + } + + /** + * @param mixed $value + * @return bool + */ + public function int($value): bool + { + return filter_var($value, FILTER_VALIDATE_INT) !== false; + } + + /** + * @param string $value + * @param array $parameters ['max'] + * @return bool + */ + public function max($value, $parameters): bool + { + $this->validateParameterCount(1, $parameters, __FUNCTION__); + $size = $this->getSize($value); + + return $size <= $parameters[0]; + } + + /** + * @param string $value + * @param array $parameters ['min'] + * @return bool + */ + public function min($value, $parameters) + { + $this->validateParameterCount(1, $parameters, __FUNCTION__); + $size = $this->getSize($value); + + return $size >= $parameters[0]; + } + + /** + * @param mixed $value + * @param array $parameters ['1,2,3,56,7'] + * @return bool + */ + public function notIn($value, $parameters): bool + { + $this->validateParameterCount(1, $parameters, __FUNCTION__); + + return !$this->in($value, $parameters); + } + + /** + * @param mixed $value + * @return bool + */ + public function numeric($value): bool + { + return is_numeric($value); + } + + /** + * @param mixed $value + * @return bool + */ + public function required($value): bool + { + if ( + is_null($value) + || (is_string($value) && trim($value) === '') + ) { + return false; + } + + return true; + } + + /** + * @param mixed $value + * @return int|float + */ + protected function getSize($value) + { + if (is_numeric($value)) { + return $value; + } + + return mb_strlen($value); + } + + /** + * @param int $count + * @param array $parameters + * @param string $rule + * + * @throws InvalidArgumentException + */ + protected function validateParameterCount(int $count, array $parameters, string $rule) + { + if (count($parameters) < $count) { + throw new InvalidArgumentException(sprintf( + 'The rule "%s" requires at least %d parameters', + $rule, + $count + )); + } + } +} diff --git a/src/Http/Validation/ValidatesRequest.php b/src/Http/Validation/ValidatesRequest.php new file mode 100644 index 00000000..33ff76af --- /dev/null +++ b/src/Http/Validation/ValidatesRequest.php @@ -0,0 +1,37 @@ +validator->validate( + (array)$request->getParsedBody(), + $rules + )) { + throw new ValidationException($this->validator); + } + + return $this->validator->getData(); + } + + /** + * @param Validator $validator + */ + public function setValidator(Validator $validator) + { + $this->validator = $validator; + } +} diff --git a/src/Http/Validation/ValidationServiceProvider.php b/src/Http/Validation/ValidationServiceProvider.php new file mode 100644 index 00000000..2f1c6359 --- /dev/null +++ b/src/Http/Validation/ValidationServiceProvider.php @@ -0,0 +1,28 @@ +app->make(Validates::class); + $this->app->instance(Validates::class, $validates); + + $validator = $this->app->make(Validator::class); + $this->app->instance(Validator::class, $validator); + $this->app->instance('validator', $validator); + + $this->app->afterResolving(function ($object, Application $app) { + if (!$object instanceof BaseController) { + return; + } + + $object->setValidator($app->get(Validator::class)); + }); + } +} diff --git a/src/Http/Validation/Validator.php b/src/Http/Validation/Validator.php new file mode 100644 index 00000000..a9235a5f --- /dev/null +++ b/src/Http/Validation/Validator.php @@ -0,0 +1,76 @@ +validate = $validate; + } + + /** + * @param array $data + * @param array $rules + * @return bool + */ + public function validate($data, $rules) + { + $this->errors = []; + $this->data = []; + + foreach ($rules as $key => $values) { + foreach (explode('|', $values) as $parameters) { + $parameters = explode(':', $parameters); + $rule = array_shift($parameters); + $rule = Str::camel($rule); + + if (!method_exists($this->validate, $rule)) { + throw new InvalidArgumentException('Unknown validation rule: ' . $rule); + } + + $value = isset($data[$key]) ? $data[$key] : null; + if (!$this->validate->{$rule}($value, $parameters, $data)) { + $this->errors[$key][] = implode('.', ['validation', $key, $rule]); + + continue; + } + + $this->data[$key] = $value; + } + } + + return empty($this->errors); + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/src/Middleware/ErrorHandler.php b/src/Middleware/ErrorHandler.php index 29b1fac1..c89edb1a 100644 --- a/src/Middleware/ErrorHandler.php +++ b/src/Middleware/ErrorHandler.php @@ -3,6 +3,8 @@ namespace Engelsystem\Middleware; use Engelsystem\Http\Exceptions\HttpException; +use Engelsystem\Http\Exceptions\ValidationException; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -43,6 +45,21 @@ class ErrorHandler implements MiddlewareInterface $response = $handler->handle($request); } catch (HttpException $e) { $response = $this->createResponse($e->getMessage(), $e->getStatusCode(), $e->getHeaders()); + } catch (ValidationException $e) { + $response = $this->createResponse('', 302, ['Location' => $this->getPreviousUrl($request)]); + + if ($request instanceof Request) { + $session = $request->getSession(); + $session->set( + 'errors', + array_merge_recursive( + $session->get('errors', []), + ['validation' => $e->getValidator()->getErrors()] + ) + ); + + $session->set('form-data', $request->request->all()); + } } $statusCode = $response->getStatusCode(); @@ -106,4 +123,17 @@ class ErrorHandler implements MiddlewareInterface { return response($content, $status, $headers); } + + /** + * @param ServerRequestInterface $request + * @return string + */ + protected function getPreviousUrl(ServerRequestInterface $request) + { + if ($header = $request->getHeader('referer')) { + return array_pop($header); + } + + return '/'; + } } diff --git a/tests/Unit/Controllers/BaseControllerTest.php b/tests/Unit/Controllers/BaseControllerTest.php index 738b538f..2adc9dc7 100644 --- a/tests/Unit/Controllers/BaseControllerTest.php +++ b/tests/Unit/Controllers/BaseControllerTest.php @@ -21,5 +21,7 @@ class BaseControllerTest extends TestCase 'dolor', ], ], $controller->getPermissions()); + + $this->assertTrue(method_exists($controller, 'setValidator')); } } diff --git a/tests/Unit/Http/Exceptions/ValidationExceptionTest.php b/tests/Unit/Http/Exceptions/ValidationExceptionTest.php new file mode 100644 index 00000000..c5a38b5a --- /dev/null +++ b/tests/Unit/Http/Exceptions/ValidationExceptionTest.php @@ -0,0 +1,25 @@ +createMock(Validator::class); + + $exception = new ValidationException($validator); + + $this->assertEquals($validator, $exception->getValidator()); + } +} diff --git a/tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php b/tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php new file mode 100644 index 00000000..772b1dc9 --- /dev/null +++ b/tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php @@ -0,0 +1,27 @@ +validate($request, $rules); + } + + /** + * @return bool + */ + public function hasValidator() + { + return !is_null($this->validator); + } +} diff --git a/tests/Unit/Http/Validation/ValidatesRequestTest.php b/tests/Unit/Http/Validation/ValidatesRequestTest.php new file mode 100644 index 00000000..8011bd03 --- /dev/null +++ b/tests/Unit/Http/Validation/ValidatesRequestTest.php @@ -0,0 +1,46 @@ +createMock(Validator::class); + $validator->expects($this->exactly(2)) + ->method('validate') + ->withConsecutive( + [['foo' => 'bar'], ['foo' => 'required']], + [[], ['foo' => 'required']] + ) + ->willReturnOnConsecutiveCalls( + true, + false + ); + $validator->expects($this->once()) + ->method('getData') + ->willReturn(['foo' => 'bar']); + + $implementation = new ValidatesRequestImplementation(); + $implementation->setValidator($validator); + + $return = $implementation->validateData(new Request([], ['foo' => 'bar']), ['foo' => 'required']); + + $this->assertEquals(['foo' => 'bar'], $return); + + $this->expectException(ValidationException::class); + $implementation->validateData(new Request([], []), ['foo' => 'required']); + } +} diff --git a/tests/Unit/Http/Validation/ValidatesTest.php b/tests/Unit/Http/Validation/ValidatesTest.php new file mode 100644 index 00000000..5cf0447a --- /dev/null +++ b/tests/Unit/Http/Validation/ValidatesTest.php @@ -0,0 +1,308 @@ +assertTrue($val->accepted($value) === $result); + } + + /** + * @return array + */ + public function provideBetween() + { + return [ + ['42', [10, 100]], + [42.5, [42, 43]], + [42, [42, 1000]], + [1337, [0, 99], false], + [-17, [32, 45], false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::between + * @param mixed $value + * @param array $parameters + * @param bool $result + * @dataProvider provideBetween + */ + public function testBetween($value, array $parameters, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->between($value, $parameters) === $result); + } + + /** + * @return array + */ + public function provideBool() + { + return [ + ['1'], + [1], + [true], + ['0'], + [0], + [false], + ['true', false], + ['false', false], + ['yes', false], + ['no', false], + ['bool', false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::bool + * @param mixed $value + * @param bool $result + * @dataProvider provideBool + */ + public function testBool($value, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->bool($value) === $result); + } + + /** + * @return array + */ + public function provideIn() + { + return [ + ['lorem', ['lorem,ipsum,dolor']], + [99, ['66,77,88,99,111']], + [4, ['1,3,5,7'], false], + ['toggle', ['on,off'], false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::in + * @param mixed $value + * @param array $parameters + * @param bool $result + * @dataProvider provideIn + */ + public function testIn($value, array $parameters, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->in($value, $parameters) === $result); + } + + /** + * @return array + */ + public function provideInt() + { + return [ + ['1337'], + [42], + ['0'], + [false, false], + ['12asd1', false], + ['one', false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::int + * @param mixed $value + * @param bool $result + * @dataProvider provideInt + */ + public function testInt($value, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->int($value) === $result); + } + + /** + * @return array + */ + public function provideMax() + { + return [ + ['99', [100]], + [-42, [1024]], + [99, [99]], + [100, [10], false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::max + * @param mixed $value + * @param array $parameters + * @param bool $result + * @dataProvider provideMax + */ + public function testMax($value, array $parameters, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->max($value, $parameters) === $result); + } + + /** + * @return array + */ + public function provideMin() + { + return [ + [32, [0]], + [7, [7]], + ['99', [10]], + [3, [42], false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::min + * @param mixed $value + * @param array $parameters + * @param bool $result + * @dataProvider provideMin + */ + public function testMin($value, array $parameters, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->min($value, $parameters) === $result); + } + + /** + * @return array + */ + public function provideNotIn() + { + return [ + [77, ['50,60,70']], + ['test', ['coding,deployment']], + ['PHP', ['Java,PHP,bash'], false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::notIn + * @param mixed $value + * @param array $parameters + * @param bool $result + * @dataProvider provideNotIn + */ + public function testNotIn($value, array $parameters, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->notIn($value, $parameters) === $result); + } + + /** + * @return array + */ + public function provideNumeric() + { + return [ + [77], + ['42'], + ['1337e0'], + ['123f00', false], + [null, false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::numeric + * @param mixed $value + * @param bool $result + * @dataProvider provideNumeric + */ + public function testNumeric($value, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->numeric($value) === $result); + } + + /** + * @return array + */ + public function provideRequired() + { + return [ + ['Lorem ipsum'], + ['1234'], + [1234], + ['0'], + [0], + ['', false], + [' ', false], + [null, false], + ]; + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::required + * @param mixed $value + * @param bool $result + * @dataProvider provideRequired + */ + public function testRequired($value, bool $result = true) + { + $val = new Validates; + $this->assertTrue($val->required($value) === $result); + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::getSize + */ + public function testGetSize() + { + $val = new Validates; + $this->assertTrue($val->max(42, [999])); + $this->assertTrue($val->max('99', [100])); + $this->assertFalse($val->max('101', [100])); + $this->assertTrue($val->max('lorem', [5])); + $this->assertFalse($val->max('Lorem Ipsum', [5])); + } + + /** + * @covers \Engelsystem\Http\Validation\Validates::validateParameterCount + */ + public function testValidateParameterCount() + { + $val = new Validates; + $this->assertTrue($val->between(42, [1, 100])); + + $this->expectException(InvalidArgumentException::class); + $val->between(42, [1]); + } +} diff --git a/tests/Unit/Http/Validation/ValidationServiceProviderTest.php b/tests/Unit/Http/Validation/ValidationServiceProviderTest.php new file mode 100644 index 00000000..969f4351 --- /dev/null +++ b/tests/Unit/Http/Validation/ValidationServiceProviderTest.php @@ -0,0 +1,34 @@ +register(); + + $this->assertTrue($app->has(Validator::class)); + $this->assertTrue($app->has('validator')); + + /** @var ValidatesRequestImplementation $validatesRequest */ + $validatesRequest = $app->make(ValidatesRequestImplementation::class); + $this->assertTrue($validatesRequest->hasValidator()); + + // Test afterResolving early return + $app->make(stdClass::class); + } +} diff --git a/tests/Unit/Http/Validation/ValidatorTest.php b/tests/Unit/Http/Validation/ValidatorTest.php new file mode 100644 index 00000000..799265ec --- /dev/null +++ b/tests/Unit/Http/Validation/ValidatorTest.php @@ -0,0 +1,50 @@ +assertTrue($val->validate( + ['foo' => 'bar', 'lorem' => 'on'], + ['foo' => 'required|not_in:lorem,ipsum,dolor', 'lorem' => 'accepted'] + )); + $this->assertEquals(['foo' => 'bar', 'lorem' => 'on'], $val->getData()); + + $this->assertFalse($val->validate( + [], + ['lorem' => 'required|min:3'] + )); + $this->assertEquals( + ['lorem' => ['validation.lorem.required', 'validation.lorem.min']], + $val->getErrors() + ); + } + + /** + * @covers \Engelsystem\Http\Validation\Validator::validate + */ + public function testValidateNotImplemented() + { + $val = new Validator(new Validates); + $this->expectException(InvalidArgumentException::class); + + $val->validate( + ['lorem' => 'bar'], + ['foo' => 'never_implemented'] + ); + } +} diff --git a/tests/Unit/Middleware/ErrorHandlerTest.php b/tests/Unit/Middleware/ErrorHandlerTest.php index 6c37b651..ea9cb216 100644 --- a/tests/Unit/Middleware/ErrorHandlerTest.php +++ b/tests/Unit/Middleware/ErrorHandlerTest.php @@ -2,14 +2,23 @@ namespace Engelsystem\Test\Unit\Middleware; +use Engelsystem\Application; use Engelsystem\Http\Exceptions\HttpException; +use Engelsystem\Http\Exceptions\ValidationException; +use Engelsystem\Http\Psr7ServiceProvider; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; +use Engelsystem\Http\ResponseServiceProvider; +use Engelsystem\Http\Validation\Validator; use Engelsystem\Middleware\ErrorHandler; use Engelsystem\Test\Unit\Middleware\Stub\ReturnResponseMiddlewareHandler; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Twig_LoaderInterface as TwigLoader; class ErrorHandlerTest extends TestCase @@ -104,7 +113,7 @@ class ErrorHandlerTest extends TestCase /** * @covers \Engelsystem\Middleware\ErrorHandler::process */ - public function testProcessException() + public function testProcessHttpException() { /** @var ServerRequestInterface|MockObject $request */ $request = $this->createMock(ServerRequestInterface::class); @@ -144,6 +153,63 @@ class ErrorHandlerTest extends TestCase $this->assertEquals($psrResponse, $return); } + /** + * @covers \Engelsystem\Middleware\ErrorHandler::process + * @covers \Engelsystem\Middleware\ErrorHandler::getPreviousUrl + */ + public function testProcessValidationException() + { + /** @var TwigLoader|MockObject $twigLoader */ + $twigLoader = $this->createMock(TwigLoader::class); + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + $validator = $this->createMock(Validator::class); + + $handler->expects($this->exactly(2)) + ->method('handle') + ->willReturnCallback(function () use ($validator) { + throw new ValidationException($validator); + }); + + $validator->expects($this->exactly(2)) + ->method('getErrors') + ->willReturn(['foo' => ['validation.foo.numeric']]); + + $session = new Session(new MockArraySessionStorage()); + $session->set('errors', ['validation' => ['foo' => ['validation.foo.required']]]); + $request = Request::create('/foo/bar', 'POST', ['foo' => 'bar']); + $request->setSession($session); + + /** @var Application $app */ + $app = app(); + (new ResponseServiceProvider($app))->register(); + (new Psr7ServiceProvider($app))->register(); + + $errorHandler = new ErrorHandler($twigLoader); + + $return = $errorHandler->process($request, $handler); + + $this->assertEquals(302, $return->getStatusCode()); + $this->assertEquals('/', $return->getHeaderLine('location')); + $this->assertEquals([ + 'errors' => [ + 'validation' => [ + 'foo' => [ + 'validation.foo.required', + 'validation.foo.numeric', + ], + ], + ], + 'form-data' => [ + 'foo' => 'bar', + ], + ], $session->all()); + + $request = $request->withAddedHeader('referer', '/foo/batz'); + $return = $errorHandler->process($request, $handler); + + $this->assertEquals('/foo/batz', $return->getHeaderLine('location')); + } + /** * @covers \Engelsystem\Middleware\ErrorHandler::process */ @@ -153,7 +219,7 @@ class ErrorHandlerTest extends TestCase $request = $this->createMock(ServerRequestInterface::class); /** @var TwigLoader|MockObject $twigLoader */ $twigLoader = $this->createMock(TwigLoader::class); - $response = new Response('

Hi!

', 500); + $response = new Response('

Hi!

', 500); $returnResponseHandler = new ReturnResponseMiddlewareHandler($response); /** @var ErrorHandler|MockObject $errorHandler */ -- cgit v1.2.3-70-g09d2 From 6743106d9a8c760580690aab704f908766731801 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Wed, 10 Jul 2019 13:34:15 +0200 Subject: Replaced validation with `respect/validation` --- composer.json | 1 + src/Http/Validation/Rules/In.php | 21 ++ src/Http/Validation/Rules/NotIn.php | 15 ++ src/Http/Validation/Validates.php | 154 ----------- src/Http/Validation/ValidationServiceProvider.php | 3 - src/Http/Validation/Validator.php | 61 +++-- tests/Unit/Controllers/AuthControllerTest.php | 3 +- tests/Unit/Http/Validation/Rules/InTest.php | 19 ++ tests/Unit/Http/Validation/Rules/NotInTest.php | 20 ++ tests/Unit/Http/Validation/ValidatesTest.php | 308 ---------------------- tests/Unit/Http/Validation/ValidatorTest.php | 42 ++- 11 files changed, 155 insertions(+), 492 deletions(-) create mode 100644 src/Http/Validation/Rules/In.php create mode 100644 src/Http/Validation/Rules/NotIn.php delete mode 100644 src/Http/Validation/Validates.php create mode 100644 tests/Unit/Http/Validation/Rules/InTest.php create mode 100644 tests/Unit/Http/Validation/Rules/NotInTest.php delete mode 100644 tests/Unit/Http/Validation/ValidatesTest.php (limited to 'tests/Unit/Http') diff --git a/composer.json b/composer.json index b2b70789..a1f2101b 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "psr/container": "^1.0", "psr/http-server-middleware": "^1.0", "psr/log": "^1.1", + "respect/validation": "^1.1", "swiftmailer/swiftmailer": "^6.2", "symfony/http-foundation": "^4.3", "symfony/psr-http-message-bridge": "^1.2", diff --git a/src/Http/Validation/Rules/In.php b/src/Http/Validation/Rules/In.php new file mode 100644 index 00000000..d585cc3d --- /dev/null +++ b/src/Http/Validation/Rules/In.php @@ -0,0 +1,21 @@ +validateParameterCount(2, $parameters, __FUNCTION__); - $size = $this->getSize($value); - - return $size >= $parameters[0] && $size <= $parameters[1]; - } - - /** - * @param mixed $value - * @return bool - */ - public function bool($value): bool - { - return in_array($value, ['1', 1, true, '0', 0, false], true); - } - - /** - * @param mixed $value - * @param array $parameters ['1,2,3,56,7'] - * @return bool - */ - public function in($value, $parameters): bool - { - $this->validateParameterCount(1, $parameters, __FUNCTION__); - - return in_array($value, explode(',', $parameters[0])); - } - - /** - * @param mixed $value - * @return bool - */ - public function int($value): bool - { - return filter_var($value, FILTER_VALIDATE_INT) !== false; - } - - /** - * @param string $value - * @param array $parameters ['max'] - * @return bool - */ - public function max($value, $parameters): bool - { - $this->validateParameterCount(1, $parameters, __FUNCTION__); - $size = $this->getSize($value); - - return $size <= $parameters[0]; - } - - /** - * @param string $value - * @param array $parameters ['min'] - * @return bool - */ - public function min($value, $parameters) - { - $this->validateParameterCount(1, $parameters, __FUNCTION__); - $size = $this->getSize($value); - - return $size >= $parameters[0]; - } - - /** - * @param mixed $value - * @param array $parameters ['1,2,3,56,7'] - * @return bool - */ - public function notIn($value, $parameters): bool - { - $this->validateParameterCount(1, $parameters, __FUNCTION__); - - return !$this->in($value, $parameters); - } - - /** - * @param mixed $value - * @return bool - */ - public function numeric($value): bool - { - return is_numeric($value); - } - - /** - * @param mixed $value - * @return bool - */ - public function required($value): bool - { - if ( - is_null($value) - || (is_string($value) && trim($value) === '') - ) { - return false; - } - - return true; - } - - /** - * @param mixed $value - * @return int|float - */ - protected function getSize($value) - { - if (is_numeric($value)) { - return $value; - } - - return mb_strlen($value); - } - - /** - * @param int $count - * @param array $parameters - * @param string $rule - * - * @throws InvalidArgumentException - */ - protected function validateParameterCount(int $count, array $parameters, string $rule) - { - if (count($parameters) < $count) { - throw new InvalidArgumentException(sprintf( - 'The rule "%s" requires at least %d parameters', - $rule, - $count - )); - } - } -} diff --git a/src/Http/Validation/ValidationServiceProvider.php b/src/Http/Validation/ValidationServiceProvider.php index 2f1c6359..14530ae6 100644 --- a/src/Http/Validation/ValidationServiceProvider.php +++ b/src/Http/Validation/ValidationServiceProvider.php @@ -10,9 +10,6 @@ class ValidationServiceProvider extends ServiceProvider { public function register() { - $validates = $this->app->make(Validates::class); - $this->app->instance(Validates::class, $validates); - $validator = $this->app->make(Validator::class); $this->app->instance(Validator::class, $validator); $this->app->instance('validator', $validator); diff --git a/src/Http/Validation/Validator.php b/src/Http/Validation/Validator.php index a9235a5f..0bd846bd 100644 --- a/src/Http/Validation/Validator.php +++ b/src/Http/Validation/Validator.php @@ -4,25 +4,23 @@ namespace Engelsystem\Http\Validation; use Illuminate\Support\Str; use InvalidArgumentException; +use Respect\Validation\Exceptions\ComponentException; +use Respect\Validation\Validator as RespectValidator; class Validator { - /** @var Validates */ - protected $validate; - /** @var string[] */ protected $errors = []; /** @var array */ protected $data = []; - /** - * @param Validates $validate - */ - public function __construct(Validates $validate) - { - $this->validate = $validate; - } + /** @var array */ + protected $mapping = [ + 'accepted' => 'TrueVal', + 'int' => 'IntVal', + 'required' => 'NotEmpty', + ]; /** * @param array $data @@ -35,29 +33,56 @@ class Validator $this->data = []; foreach ($rules as $key => $values) { + $v = new RespectValidator(); + $v->with('\\Engelsystem\\Http\\Validation\\Rules', true); + + $value = isset($data[$key]) ? $data[$key] : null; + foreach (explode('|', $values) as $parameters) { $parameters = explode(':', $parameters); $rule = array_shift($parameters); $rule = Str::camel($rule); + $rule = $this->map($rule); - if (!method_exists($this->validate, $rule)) { - throw new InvalidArgumentException('Unknown validation rule: ' . $rule); + try { + call_user_func_array([$v, $rule], $parameters); + } catch (ComponentException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } - $value = isset($data[$key]) ? $data[$key] : null; - if (!$this->validate->{$rule}($value, $parameters, $data)) { - $this->errors[$key][] = implode('.', ['validation', $key, $rule]); - - continue; + if ($v->validate($value)) { + $this->data[$key] = $value; + } else { + $this->errors[$key][] = implode('.', ['validation', $key, $this->mapBack($rule)]); } - $this->data[$key] = $value; + $v->removeRules(); } } return empty($this->errors); } + /** + * @param string $rule + * @return string + */ + protected function map($rule) + { + return $this->mapping[$rule] ?? $rule; + } + + /** + * @param string $rule + * @return string + */ + protected function mapBack($rule) + { + $mapping = array_flip($this->mapping); + + return $mapping[$rule] ?? $rule; + } + /** * @return array */ diff --git a/tests/Unit/Controllers/AuthControllerTest.php b/tests/Unit/Controllers/AuthControllerTest.php index d3dbfa4b..c3d9659c 100644 --- a/tests/Unit/Controllers/AuthControllerTest.php +++ b/tests/Unit/Controllers/AuthControllerTest.php @@ -8,7 +8,6 @@ use Engelsystem\Http\Exceptions\ValidationException; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Http\UrlGeneratorInterface; -use Engelsystem\Http\Validation\Validates; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\User\Settings; use Engelsystem\Models\User\User; @@ -66,7 +65,7 @@ class AuthControllerTest extends TestCase list(, , $url, $auth) = $this->getMocks(); $session = new Session(new MockArraySessionStorage()); /** @var Validator|MockObject $validator */ - $validator = new Validator(new Validates()); + $validator = new Validator(); $user = new User([ 'name' => 'foo', diff --git a/tests/Unit/Http/Validation/Rules/InTest.php b/tests/Unit/Http/Validation/Rules/InTest.php new file mode 100644 index 00000000..e5688d90 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/InTest.php @@ -0,0 +1,19 @@ +assertEquals(['foo', 'bar'], $rule->haystack); + } +} diff --git a/tests/Unit/Http/Validation/Rules/NotInTest.php b/tests/Unit/Http/Validation/Rules/NotInTest.php new file mode 100644 index 00000000..9be12336 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/NotInTest.php @@ -0,0 +1,20 @@ +assertTrue($rule->validate('lorem')); + $this->assertFalse($rule->validate('foo')); + } +} diff --git a/tests/Unit/Http/Validation/ValidatesTest.php b/tests/Unit/Http/Validation/ValidatesTest.php deleted file mode 100644 index 5cf0447a..00000000 --- a/tests/Unit/Http/Validation/ValidatesTest.php +++ /dev/null @@ -1,308 +0,0 @@ -assertTrue($val->accepted($value) === $result); - } - - /** - * @return array - */ - public function provideBetween() - { - return [ - ['42', [10, 100]], - [42.5, [42, 43]], - [42, [42, 1000]], - [1337, [0, 99], false], - [-17, [32, 45], false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::between - * @param mixed $value - * @param array $parameters - * @param bool $result - * @dataProvider provideBetween - */ - public function testBetween($value, array $parameters, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->between($value, $parameters) === $result); - } - - /** - * @return array - */ - public function provideBool() - { - return [ - ['1'], - [1], - [true], - ['0'], - [0], - [false], - ['true', false], - ['false', false], - ['yes', false], - ['no', false], - ['bool', false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::bool - * @param mixed $value - * @param bool $result - * @dataProvider provideBool - */ - public function testBool($value, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->bool($value) === $result); - } - - /** - * @return array - */ - public function provideIn() - { - return [ - ['lorem', ['lorem,ipsum,dolor']], - [99, ['66,77,88,99,111']], - [4, ['1,3,5,7'], false], - ['toggle', ['on,off'], false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::in - * @param mixed $value - * @param array $parameters - * @param bool $result - * @dataProvider provideIn - */ - public function testIn($value, array $parameters, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->in($value, $parameters) === $result); - } - - /** - * @return array - */ - public function provideInt() - { - return [ - ['1337'], - [42], - ['0'], - [false, false], - ['12asd1', false], - ['one', false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::int - * @param mixed $value - * @param bool $result - * @dataProvider provideInt - */ - public function testInt($value, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->int($value) === $result); - } - - /** - * @return array - */ - public function provideMax() - { - return [ - ['99', [100]], - [-42, [1024]], - [99, [99]], - [100, [10], false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::max - * @param mixed $value - * @param array $parameters - * @param bool $result - * @dataProvider provideMax - */ - public function testMax($value, array $parameters, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->max($value, $parameters) === $result); - } - - /** - * @return array - */ - public function provideMin() - { - return [ - [32, [0]], - [7, [7]], - ['99', [10]], - [3, [42], false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::min - * @param mixed $value - * @param array $parameters - * @param bool $result - * @dataProvider provideMin - */ - public function testMin($value, array $parameters, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->min($value, $parameters) === $result); - } - - /** - * @return array - */ - public function provideNotIn() - { - return [ - [77, ['50,60,70']], - ['test', ['coding,deployment']], - ['PHP', ['Java,PHP,bash'], false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::notIn - * @param mixed $value - * @param array $parameters - * @param bool $result - * @dataProvider provideNotIn - */ - public function testNotIn($value, array $parameters, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->notIn($value, $parameters) === $result); - } - - /** - * @return array - */ - public function provideNumeric() - { - return [ - [77], - ['42'], - ['1337e0'], - ['123f00', false], - [null, false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::numeric - * @param mixed $value - * @param bool $result - * @dataProvider provideNumeric - */ - public function testNumeric($value, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->numeric($value) === $result); - } - - /** - * @return array - */ - public function provideRequired() - { - return [ - ['Lorem ipsum'], - ['1234'], - [1234], - ['0'], - [0], - ['', false], - [' ', false], - [null, false], - ]; - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::required - * @param mixed $value - * @param bool $result - * @dataProvider provideRequired - */ - public function testRequired($value, bool $result = true) - { - $val = new Validates; - $this->assertTrue($val->required($value) === $result); - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::getSize - */ - public function testGetSize() - { - $val = new Validates; - $this->assertTrue($val->max(42, [999])); - $this->assertTrue($val->max('99', [100])); - $this->assertFalse($val->max('101', [100])); - $this->assertTrue($val->max('lorem', [5])); - $this->assertFalse($val->max('Lorem Ipsum', [5])); - } - - /** - * @covers \Engelsystem\Http\Validation\Validates::validateParameterCount - */ - public function testValidateParameterCount() - { - $val = new Validates; - $this->assertTrue($val->between(42, [1, 100])); - - $this->expectException(InvalidArgumentException::class); - $val->between(42, [1]); - } -} diff --git a/tests/Unit/Http/Validation/ValidatorTest.php b/tests/Unit/Http/Validation/ValidatorTest.php index 799265ec..0790b7a8 100644 --- a/tests/Unit/Http/Validation/ValidatorTest.php +++ b/tests/Unit/Http/Validation/ValidatorTest.php @@ -2,7 +2,6 @@ namespace Engelsystem\Test\Unit\Http\Validation; -use Engelsystem\Http\Validation\Validates; use Engelsystem\Http\Validation\Validator; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -10,19 +9,18 @@ use PHPUnit\Framework\TestCase; class ValidatorTest extends TestCase { /** - * @covers \Engelsystem\Http\Validation\Validator::__construct * @covers \Engelsystem\Http\Validation\Validator::validate * @covers \Engelsystem\Http\Validation\Validator::getData * @covers \Engelsystem\Http\Validation\Validator::getErrors */ public function testValidate() { - $val = new Validator(new Validates); + $val = new Validator(); $this->assertTrue($val->validate( - ['foo' => 'bar', 'lorem' => 'on'], - ['foo' => 'required|not_in:lorem,ipsum,dolor', 'lorem' => 'accepted'] + ['foo' => 'bar', 'lorem' => 'on', 'dolor' => 'bla'], + ['lorem' => 'accepted'] )); - $this->assertEquals(['foo' => 'bar', 'lorem' => 'on'], $val->getData()); + $this->assertEquals(['lorem' => 'on'], $val->getData()); $this->assertFalse($val->validate( [], @@ -39,7 +37,7 @@ class ValidatorTest extends TestCase */ public function testValidateNotImplemented() { - $val = new Validator(new Validates); + $val = new Validator(); $this->expectException(InvalidArgumentException::class); $val->validate( @@ -47,4 +45,34 @@ class ValidatorTest extends TestCase ['foo' => 'never_implemented'] ); } + + /** + * @covers \Engelsystem\Http\Validation\Validator::map + * @covers \Engelsystem\Http\Validation\Validator::mapBack + */ + public function testValidateMapping() + { + $val = new Validator(); + $this->assertTrue($val->validate( + ['foo' => 'bar'], + ['foo' => 'required'] + )); + $this->assertTrue($val->validate( + ['foo' => '0'], + ['foo' => 'int'] + )); + $this->assertTrue($val->validate( + ['foo' => 'on'], + ['foo' => 'accepted'] + )); + + $this->assertFalse($val->validate( + [], + ['lorem' => 'required'] + )); + $this->assertEquals( + ['lorem' => ['validation.lorem.required']], + $val->getErrors() + ); + } } -- cgit v1.2.3-70-g09d2 From b25924e868cdf80944e56a76fa6eed4509d9af7b Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 16 Jul 2019 01:39:54 +0200 Subject: Allow nested rules (not and optional) --- src/Http/Validation/Validator.php | 27 ++++++++++-- tests/Unit/Http/Validation/ValidatorTest.php | 64 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) (limited to 'tests/Unit/Http') diff --git a/src/Http/Validation/Validator.php b/src/Http/Validation/Validator.php index 0bd846bd..976f5682 100644 --- a/src/Http/Validation/Validator.php +++ b/src/Http/Validation/Validator.php @@ -22,6 +22,9 @@ class Validator 'required' => 'NotEmpty', ]; + /** @var array */ + protected $nestedRules = ['optional', 'not']; + /** * @param array $data * @param array $rules @@ -37,20 +40,38 @@ class Validator $v->with('\\Engelsystem\\Http\\Validation\\Rules', true); $value = isset($data[$key]) ? $data[$key] : null; + $values = explode('|', $values); + + $packing = []; + foreach ($this->nestedRules as $rule) { + if (in_array($rule, $values)) { + $packing[] = $rule; + } + } - foreach (explode('|', $values) as $parameters) { + $values = array_diff($values, $this->nestedRules); + foreach ($values as $parameters) { $parameters = explode(':', $parameters); $rule = array_shift($parameters); $rule = Str::camel($rule); $rule = $this->map($rule); + // To allow rules nesting + $w = $v; try { - call_user_func_array([$v, $rule], $parameters); + foreach (array_reverse(array_merge($packing, [$rule])) as $rule) { + if (!in_array($rule, $this->nestedRules)) { + call_user_func_array([$w, $rule], $parameters); + continue; + } + + $w = call_user_func_array([new RespectValidator(), $rule], [$w]); + } } catch (ComponentException $e) { throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } - if ($v->validate($value)) { + if ($w->validate($value)) { $this->data[$key] = $value; } else { $this->errors[$key][] = implode('.', ['validation', $key, $this->mapBack($rule)]); diff --git a/tests/Unit/Http/Validation/ValidatorTest.php b/tests/Unit/Http/Validation/ValidatorTest.php index 0790b7a8..450e5d4e 100644 --- a/tests/Unit/Http/Validation/ValidatorTest.php +++ b/tests/Unit/Http/Validation/ValidatorTest.php @@ -16,6 +16,7 @@ class ValidatorTest extends TestCase public function testValidate() { $val = new Validator(); + $this->assertTrue($val->validate( ['foo' => 'bar', 'lorem' => 'on', 'dolor' => 'bla'], ['lorem' => 'accepted'] @@ -32,12 +33,39 @@ class ValidatorTest extends TestCase ); } + /** + * @covers \Engelsystem\Http\Validation\Validator::validate + */ + public function testValidateChaining() + { + $val = new Validator(); + + $this->assertTrue($val->validate( + ['lorem' => 10], + ['lorem' => 'required|min:3|max:10'] + )); + $this->assertTrue($val->validate( + ['lorem' => 3], + ['lorem' => 'required|min:3|max:10'] + )); + + $this->assertFalse($val->validate( + ['lorem' => 2], + ['lorem' => 'required|min:3|max:10'] + )); + $this->assertFalse($val->validate( + ['lorem' => 42], + ['lorem' => 'required|min:3|max:10'] + )); + } + /** * @covers \Engelsystem\Http\Validation\Validator::validate */ public function testValidateNotImplemented() { $val = new Validator(); + $this->expectException(InvalidArgumentException::class); $val->validate( @@ -53,6 +81,7 @@ class ValidatorTest extends TestCase public function testValidateMapping() { $val = new Validator(); + $this->assertTrue($val->validate( ['foo' => 'bar'], ['foo' => 'required'] @@ -75,4 +104,39 @@ class ValidatorTest extends TestCase $val->getErrors() ); } + + /** + * @covers \Engelsystem\Http\Validation\Validator::validate + */ + public function testValidateNesting() + { + $val = new Validator(); + + $this->assertTrue($val->validate( + [], + ['foo' => 'not|required'] + )); + + $this->assertTrue($val->validate( + ['foo' => 'foo'], + ['foo' => 'not|int'] + )); + $this->assertFalse($val->validate( + ['foo' => 1], + ['foo' => 'not|int'] + )); + + $this->assertTrue($val->validate( + [], + ['foo' => 'optional|int'] + )); + $this->assertTrue($val->validate( + ['foo' => '33'], + ['foo' => 'optional|int'] + )); + $this->assertFalse($val->validate( + ['foo' => 'T'], + ['foo' => 'optional|int'] + )); + } } -- cgit v1.2.3-70-g09d2