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 --- src/Controllers/AuthController.php | 90 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) (limited to 'src/Controllers') diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index cdaee167..e5fc40e3 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -2,8 +2,12 @@ namespace Engelsystem\Controllers; +use Carbon\Carbon; +use Engelsystem\Helpers\Authenticator; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Http\UrlGeneratorInterface; +use Engelsystem\Models\User\User; use Symfony\Component\HttpFoundation\Session\SessionInterface; class AuthController extends BaseController @@ -17,20 +21,100 @@ class AuthController extends BaseController /** @var UrlGeneratorInterface */ protected $url; - public function __construct(Response $response, SessionInterface $session, UrlGeneratorInterface $url) - { + /** @var Authenticator */ + protected $auth; + + /** @var array */ + protected $permissions = [ + 'login' => 'login', + 'postLogin' => 'login', + ]; + + /** + * @param Response $response + * @param SessionInterface $session + * @param UrlGeneratorInterface $url + * @param Authenticator $auth + */ + public function __construct( + Response $response, + SessionInterface $session, + UrlGeneratorInterface $url, + Authenticator $auth + ) { $this->response = $response; $this->session = $session; $this->url = $url; + $this->auth = $auth; + } + + /** + * @return Response + */ + public function login() + { + return $this->response->withView('pages/login'); + } + + /** + * Posted login form + * + * @param Request $request + * @return Response + */ + public function postLogin(Request $request): Response + { + $return = $this->authenticateUser($request->get('login', ''), $request->get('password', '')); + if (!$return instanceof User) { + return $this->response->withView( + 'pages/login', + ['errors' => [$return], 'show_password_recovery' => true] + ); + } + + $user = $return; + + $this->session->invalidate(); + $this->session->set('user_id', $user->id); + $this->session->set('locale', $user->settings->language); + + $user->last_login_at = new Carbon(); + $user->save(['touch' => false]); + + return $this->response->redirectTo('news'); } /** * @return Response */ - public function logout() + public function logout(): Response { $this->session->invalidate(); return $this->response->redirectTo($this->url->to('/')); } + + /** + * Verify the user and password + * + * @param $login + * @param $password + * @return User|string + */ + protected function authenticateUser(string $login, string $password) + { + if (!$login) { + return 'auth.no-nickname'; + } + + if (!$password) { + return 'auth.no-password'; + } + + if (!$user = $this->auth->authenticate($login, $password)) { + return 'auth.not-found'; + } + + return $user; + } } -- cgit v1.2.3-54-g00ecf From e9f157ec5ccdfae73b4c9e82c9ae7c37bcfa1513 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Mon, 3 Dec 2018 23:39:50 +0100 Subject: Renderer: Added shared data --- src/Controllers/Metrics/MetricsEngine.php | 18 ++++++++---- src/Renderer/Engine.php | 22 +++++++++++++++ src/Renderer/EngineInterface.php | 10 +++++-- src/Renderer/HtmlEngine.php | 8 ++++-- src/Renderer/TwigEngine.php | 8 ++++-- .../Unit/Controllers/Metrics/MetricsEngineTest.php | 11 ++++++++ tests/Unit/Renderer/EngineTest.php | 25 +++++++++++++++++ tests/Unit/Renderer/HtmlEngineTest.php | 5 ++-- tests/Unit/Renderer/Stub/EngineImplementation.php | 32 ++++++++++++++++++++++ tests/Unit/Renderer/TwigEngineTest.php | 12 ++++---- 10 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 src/Renderer/Engine.php create mode 100644 tests/Unit/Renderer/EngineTest.php create mode 100644 tests/Unit/Renderer/Stub/EngineImplementation.php (limited to 'src/Controllers') diff --git a/src/Controllers/Metrics/MetricsEngine.php b/src/Controllers/Metrics/MetricsEngine.php index 1e0f6957..21ae8fd0 100644 --- a/src/Controllers/Metrics/MetricsEngine.php +++ b/src/Controllers/Metrics/MetricsEngine.php @@ -9,13 +9,13 @@ class MetricsEngine implements EngineInterface /** * Render metrics * - * @example $data = ['foo' => [['labels' => ['foo'=>'bar'], 'value'=>42]], 'bar'=>123] - * * @param string $path * @param mixed[] $data * @return string + * + * @example $data = ['foo' => [['labels' => ['foo'=>'bar'], 'value'=>42]], 'bar'=>123] */ - public function get($path, $data = []): string + public function get(string $path, array $data = []): string { $return = []; foreach ($data as $name => $list) { @@ -52,7 +52,7 @@ class MetricsEngine implements EngineInterface * @param string $path * @return bool */ - public function canRender($path): bool + public function canRender(string $path): bool { return $path == '/metrics'; } @@ -60,8 +60,8 @@ class MetricsEngine implements EngineInterface /** * @param string $name * @param array|mixed $row - * @see https://prometheus.io/docs/instrumenting/exposition_formats/ * @return string + * @see https://prometheus.io/docs/instrumenting/exposition_formats/ */ protected function formatData($name, $row): string { @@ -135,4 +135,12 @@ class MetricsEngine implements EngineInterface $value ); } + + /** + * Does nothing as shared data will onyly result in unexpected behaviour + * + * @param string|mixed[] $key + * @param mixed $value + */ + public function share($key, $value = null) { } } diff --git a/src/Renderer/Engine.php b/src/Renderer/Engine.php new file mode 100644 index 00000000..60f1d686 --- /dev/null +++ b/src/Renderer/Engine.php @@ -0,0 +1,22 @@ + $value]; + } + + $this->sharedData = array_replace_recursive($this->sharedData, $key); + } +} diff --git a/src/Renderer/EngineInterface.php b/src/Renderer/EngineInterface.php index ca468db5..3bce9c02 100644 --- a/src/Renderer/EngineInterface.php +++ b/src/Renderer/EngineInterface.php @@ -11,11 +11,17 @@ interface EngineInterface * @param mixed[] $data * @return string */ - public function get($path, $data = []); + public function get(string $path, array $data = []): string; /** * @param string $path * @return bool */ - public function canRender($path); + public function canRender(string $path): bool; + + /** + * @param string|mixed[] $key + * @param mixed $value + */ + public function share($key, $value = null); } diff --git a/src/Renderer/HtmlEngine.php b/src/Renderer/HtmlEngine.php index 1feafcda..0ccffa65 100644 --- a/src/Renderer/HtmlEngine.php +++ b/src/Renderer/HtmlEngine.php @@ -2,7 +2,7 @@ namespace Engelsystem\Renderer; -class HtmlEngine implements EngineInterface +class HtmlEngine extends Engine { /** * Render a template @@ -11,9 +11,11 @@ class HtmlEngine implements EngineInterface * @param mixed[] $data * @return string */ - public function get($path, $data = []) + public function get(string $path, array $data = []): string { + $data = array_replace_recursive($this->sharedData, $data); $template = file_get_contents($path); + if (is_array($data)) { foreach ($data as $name => $content) { $template = str_replace('%' . $name . '%', $content, $template); @@ -27,7 +29,7 @@ class HtmlEngine implements EngineInterface * @param string $path * @return bool */ - public function canRender($path) + public function canRender(string $path): bool { return mb_strpos($path, '.htm') !== false && file_exists($path); } diff --git a/src/Renderer/TwigEngine.php b/src/Renderer/TwigEngine.php index 55a2e299..aa51a177 100644 --- a/src/Renderer/TwigEngine.php +++ b/src/Renderer/TwigEngine.php @@ -7,7 +7,7 @@ use Twig_Error_Loader as LoaderError; use Twig_Error_Runtime as RuntimeError; use Twig_Error_Syntax as SyntaxError; -class TwigEngine implements EngineInterface +class TwigEngine extends Engine { /** @var Twig */ protected $twig; @@ -25,8 +25,10 @@ class TwigEngine implements EngineInterface * @return string * @throws LoaderError|RuntimeError|SyntaxError */ - public function get($path, $data = []) + public function get(string $path, array $data = []): string { + $data = array_replace_recursive($this->sharedData, $data); + return $this->twig->render($path, $data); } @@ -34,7 +36,7 @@ class TwigEngine implements EngineInterface * @param string $path * @return bool */ - public function canRender($path) + public function canRender(string $path): bool { return $this->twig->getLoader()->exists($path); } diff --git a/tests/Unit/Controllers/Metrics/MetricsEngineTest.php b/tests/Unit/Controllers/Metrics/MetricsEngineTest.php index 38817b36..87a7dc88 100644 --- a/tests/Unit/Controllers/Metrics/MetricsEngineTest.php +++ b/tests/Unit/Controllers/Metrics/MetricsEngineTest.php @@ -66,4 +66,15 @@ class MetricsEngineTest extends TestCase $this->assertFalse($engine->canRender('/metrics.foo')); $this->assertTrue($engine->canRender('/metrics')); } + + /** + * @covers \Engelsystem\Controllers\Metrics\MetricsEngine::share + */ + public function testShare() + { + $engine = new MetricsEngine(); + + $engine->share('foo', 42); + $this->assertEquals('', $engine->get('/metrics')); + } } diff --git a/tests/Unit/Renderer/EngineTest.php b/tests/Unit/Renderer/EngineTest.php new file mode 100644 index 00000000..659d85c5 --- /dev/null +++ b/tests/Unit/Renderer/EngineTest.php @@ -0,0 +1,25 @@ +share(['foo' => ['bar' => 'baz', 'lorem' => 'ipsum']]); + $engine->share(['foo' => ['lorem' => 'dolor']]); + $engine->share('key', 'value'); + + $this->assertEquals( + ['foo' => ['bar' => 'baz', 'lorem' => 'dolor'], 'key' => 'value'], + $engine->getSharedData() + ); + } +} diff --git a/tests/Unit/Renderer/HtmlEngineTest.php b/tests/Unit/Renderer/HtmlEngineTest.php index 4a31e4bc..f76e7528 100644 --- a/tests/Unit/Renderer/HtmlEngineTest.php +++ b/tests/Unit/Renderer/HtmlEngineTest.php @@ -16,11 +16,12 @@ class HtmlEngineTest extends TestCase public function testGet() { $engine = new HtmlEngine(); + $engine->share('shared_data', 'tester'); - $file = $this->createTempFile('
%main_content%
'); + $file = $this->createTempFile('
%main_content% is a %shared_data%
'); $data = $engine->get($file, ['main_content' => 'Lorem ipsum dolor sit']); - $this->assertEquals('
Lorem ipsum dolor sit
', $data); + $this->assertEquals('
Lorem ipsum dolor sit is a tester
', $data); } /** diff --git a/tests/Unit/Renderer/Stub/EngineImplementation.php b/tests/Unit/Renderer/Stub/EngineImplementation.php new file mode 100644 index 00000000..fc436569 --- /dev/null +++ b/tests/Unit/Renderer/Stub/EngineImplementation.php @@ -0,0 +1,32 @@ +sharedData; + } +} diff --git a/tests/Unit/Renderer/TwigEngineTest.php b/tests/Unit/Renderer/TwigEngineTest.php index 9d0618f1..5e5e59d9 100644 --- a/tests/Unit/Renderer/TwigEngineTest.php +++ b/tests/Unit/Renderer/TwigEngineTest.php @@ -20,16 +20,16 @@ class TwigEngineTest extends TestCase $twig = $this->createMock(Twig::class); $path = 'foo.twig'; - $data = ['lorem' => 'ipsum']; - $twig->expects($this->once()) ->method('render') - ->with($path, $data) - ->willReturn('LoremIpsum!'); + ->with($path, ['lorem' => 'ipsum', 'shared' => 'data']) + ->willReturn('LoremIpsum data!'); $engine = new TwigEngine($twig); - $return = $engine->get($path, $data); - $this->assertEquals('LoremIpsum!', $return); + $engine->share('shared', 'data'); + + $return = $engine->get($path, ['lorem' => 'ipsum']); + $this->assertEquals('LoremIpsum data!', $return); } -- cgit v1.2.3-54-g00ecf 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 'src/Controllers') 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-54-g00ecf From 6d5ada252202bfb29eba884cf9567e969d798607 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 9 Jul 2019 22:02:07 +0200 Subject: Added validation to AuthController --- resources/lang/de_DE/default.mo | Bin 46271 -> 46206 bytes resources/lang/de_DE/default.po | 18 +++++--- resources/lang/en_US/default.mo | Bin 745 -> 770 bytes resources/lang/en_US/default.po | 16 ++++--- src/Controllers/AuthController.php | 62 ++++++++++++-------------- tests/Unit/Controllers/AuthControllerTest.php | 61 +++++++++++++++---------- 6 files changed, 88 insertions(+), 69 deletions(-) (limited to 'src/Controllers') diff --git a/resources/lang/de_DE/default.mo b/resources/lang/de_DE/default.mo index 35ad80b7..fb93d590 100644 Binary files a/resources/lang/de_DE/default.mo and b/resources/lang/de_DE/default.mo differ diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index cd696610..1f0372af 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Engelsystem\n" "POT-Creation-Date: 2019-04-28 15:23+0200\n" -"PO-Revision-Date: 2019-06-12 16:07+0200\n" +"PO-Revision-Date: 2019-06-13 11:54+0200\n" "Last-Translator: msquare \n" "Language-Team: \n" "Language: de_DE\n" @@ -1529,9 +1529,8 @@ msgstr "Nachname" msgid "Entry required!" msgstr "Pflichtfeld!" -#: includes/pages/guest_login.php:414 -msgid "auth.no-password" -msgstr "Gib bitte ein Passwort ein." +#~ msgid "auth.no-password" +#~ msgstr "Gib bitte ein Passwort ein." #: includes/pages/guest_login.php:418 msgid "auth.not-found" @@ -1539,9 +1538,8 @@ msgstr "" "Es wurde kein Engel gefunden. Probiere es bitte noch einmal. Wenn das Problem " "weiterhin besteht, melde dich im Himmel." -#: includes/pages/guest_login.php:451 includes/view/User_view.php:130 -msgid "auth.no-nickname" -msgstr "Gib bitte einen Nick an." +#~ msgid "auth.no-nickname" +#~ msgstr "Gib bitte einen Nick an." #: includes/pages/guest_login.php:481 #: includes/view/User_view.php:122 @@ -2765,3 +2763,9 @@ msgid "" msgstr "" "Diese Seite existiert nicht oder Du hast keinen Zugriff. Melde Dich an um " "Zugriff zu erhalten!" + +msgid "validation.password.required" +msgstr "Bitte gib ein Passwort an." + +msgid "validation.login.required" +msgstr "Bitte gib einen Loginnamen an." diff --git a/resources/lang/en_US/default.mo b/resources/lang/en_US/default.mo index e95ae703..7ef9c3b2 100644 Binary files a/resources/lang/en_US/default.mo and b/resources/lang/en_US/default.mo differ diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 22566e52..54847e61 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Engelsystem 2.0\n" "POT-Creation-Date: 2017-12-29 19:01+0100\n" -"PO-Revision-Date: 2018-11-27 00:28+0100\n" +"PO-Revision-Date: 2019-06-04 23:41+0200\n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -16,11 +16,17 @@ msgstr "" "Language: en_US\n" "X-Poedit-SearchPath-0: .\n" -msgid "auth.no-nickname" -msgstr "Please enter a nickname." +#~ msgid "auth.no-nickname" +#~ msgstr "Please enter a nickname." -msgid "auth.no-password" -msgstr "Please enter a password." +#~ msgid "auth.no-password" +#~ msgstr "Please enter a password." msgid "auth.not-found" msgstr "No user was found. Please try again. If you are still having problems, ask Heaven." + +msgid "validation.password.required" +msgstr "The password is required." + +msgid "validation.login.required" +msgstr "The login name is required." diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index e5fc40e3..a8cc1ace 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -8,6 +8,8 @@ use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Http\UrlGeneratorInterface; use Engelsystem\Models\User\User; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Symfony\Component\HttpFoundation\Session\SessionInterface; class AuthController extends BaseController @@ -53,7 +55,22 @@ class AuthController extends BaseController */ public function login() { - return $this->response->withView('pages/login'); + return $this->showLogin(); + } + + /** + * @param bool $showRecovery + * @return Response + */ + protected function showLogin($showRecovery = false) + { + $errors = Collection::make(Arr::flatten($this->session->get('errors', []))); + $this->session->remove('errors'); + + return $this->response->withView( + 'pages/login', + ['errors' => $errors, 'show_password_recovery' => $showRecovery] + ); } /** @@ -64,15 +81,18 @@ class AuthController extends BaseController */ public function postLogin(Request $request): Response { - $return = $this->authenticateUser($request->get('login', ''), $request->get('password', '')); - if (!$return instanceof User) { - return $this->response->withView( - 'pages/login', - ['errors' => [$return], 'show_password_recovery' => true] - ); - } + $data = $this->validate($request, [ + 'login' => 'required', + 'password' => 'required', + ]); + + $user = $this->auth->authenticate($data['login'], $data['password']); - $user = $return; + if (!$user instanceof User) { + $this->session->set('errors', $this->session->get('errors', []) + ['auth.not-found']); + + return $this->showLogin(true); + } $this->session->invalidate(); $this->session->set('user_id', $user->id); @@ -93,28 +113,4 @@ class AuthController extends BaseController return $this->response->redirectTo($this->url->to('/')); } - - /** - * Verify the user and password - * - * @param $login - * @param $password - * @return User|string - */ - protected function authenticateUser(string $login, string $password) - { - if (!$login) { - return 'auth.no-nickname'; - } - - if (!$password) { - return 'auth.no-password'; - } - - if (!$user = $this->auth->authenticate($login, $password)) { - return 'auth.not-found'; - } - - return $user; - } } diff --git a/tests/Unit/Controllers/AuthControllerTest.php b/tests/Unit/Controllers/AuthControllerTest.php index 0fad3b6d..d3dbfa4b 100644 --- a/tests/Unit/Controllers/AuthControllerTest.php +++ b/tests/Unit/Controllers/AuthControllerTest.php @@ -4,15 +4,21 @@ namespace Engelsystem\Test\Unit\Controllers; use Engelsystem\Controllers\AuthController; use Engelsystem\Helpers\Authenticator; +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; use Engelsystem\Test\Unit\HasDatabase; +use Illuminate\Support\Collection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; class AuthControllerTest extends TestCase { @@ -21,6 +27,7 @@ class AuthControllerTest extends TestCase /** * @covers \Engelsystem\Controllers\AuthController::__construct * @covers \Engelsystem\Controllers\AuthController::login + * @covers \Engelsystem\Controllers\AuthController::showLogin */ public function testLogin() { @@ -31,6 +38,10 @@ class AuthControllerTest extends TestCase /** @var Authenticator|MockObject $auth */ list(, $session, $url, $auth) = $this->getMocks(); + $session->expects($this->once()) + ->method('get') + ->with('errors', []) + ->willReturn(['foo' => 'bar']); $response->expects($this->once()) ->method('withView') ->with('pages/login') @@ -42,7 +53,6 @@ class AuthControllerTest extends TestCase /** * @covers \Engelsystem\Controllers\AuthController::postLogin - * @covers \Engelsystem\Controllers\AuthController::authenticateUser */ public function testPostLogin() { @@ -51,10 +61,12 @@ class AuthControllerTest extends TestCase $request = new Request(); /** @var Response|MockObject $response */ $response = $this->createMock(Response::class); - /** @var SessionInterface|MockObject $session */ /** @var UrlGeneratorInterface|MockObject $url */ /** @var Authenticator|MockObject $auth */ - list(, $session, $url, $auth) = $this->getMocks(); + list(, , $url, $auth) = $this->getMocks(); + $session = new Session(new MockArraySessionStorage()); + /** @var Validator|MockObject $validator */ + $validator = new Validator(new Validates()); $user = new User([ 'name' => 'foo', @@ -63,7 +75,7 @@ class AuthControllerTest extends TestCase 'api_key' => '', 'last_login_at' => null, ]); - $user->forceFill(['id' => 42,]); + $user->forceFill(['id' => 42]); $user->save(); $settings = new Settings(['language' => 'de_DE', 'theme' => '']); @@ -76,41 +88,42 @@ class AuthControllerTest extends TestCase ->with('foo', 'bar') ->willReturnOnConsecutiveCalls(null, $user); - $response->expects($this->exactly(3)) + $response->expects($this->once()) ->method('withView') - ->withConsecutive( - ['pages/login', ['errors' => ['auth.no-nickname'], 'show_password_recovery' => true]], - ['pages/login', ['errors' => ['auth.no-password'], 'show_password_recovery' => true]], - ['pages/login', ['errors' => ['auth.not-found'], 'show_password_recovery' => true]]) + ->with('pages/login', ['errors' => Collection::make(['auth.not-found']), 'show_password_recovery' => true]) ->willReturn($response); $response->expects($this->once()) ->method('redirectTo') ->with('news') ->willReturn($response); - $session->expects($this->once()) - ->method('invalidate'); - - $session->expects($this->exactly(2)) - ->method('set') - ->withConsecutive( - ['user_id', 42], - ['locale', 'de_DE'] - ); - + // No credentials $controller = new AuthController($response, $session, $url, $auth); - $controller->postLogin($request); + $controller->setValidator($validator); + try { + $controller->postLogin($request); + $this->fail('Login without credentials possible'); + } catch (ValidationException $e) { + } + + // Missing password + $request = new Request([], ['login' => 'foo']); + try { + $controller->postLogin($request); + $this->fail('Login without password possible'); + } catch (ValidationException $e) { + } - $request = new Request(['login' => 'foo']); - $controller->postLogin($request); - - $request = new Request(['login' => 'foo', 'password' => 'bar']); // No user found + $request = new Request([], ['login' => 'foo', 'password' => 'bar']); $controller->postLogin($request); + $this->assertEquals([], $session->all()); + // Authenticated user $controller->postLogin($request); $this->assertNotNull($user->last_login_at); + $this->assertEquals(['user_id' => 42, 'locale' => 'de_DE'], $session->all()); } /** -- cgit v1.2.3-54-g00ecf From 4582f808f05205e7a32ecd6ae42dee00295872f1 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sun, 21 Jul 2019 02:34:52 +0200 Subject: Added version to credits and metrics page --- .gitlab-ci.yml | 4 ++- config/app.php | 1 + contrib/Dockerfile | 3 ++ resources/views/pages/credits.twig | 1 + src/Application.php | 1 + src/Controllers/CreditsController.php | 13 +++++-- src/Controllers/Metrics/Controller.php | 21 +++++++++++- src/Helpers/Version.php | 42 +++++++++++++++++++++++ src/Helpers/VersionServiceProvider.php | 15 ++++++++ storage/app/.gitignore | 2 ++ tests/Unit/Controllers/CreditsControllerTest.php | 16 ++++++--- tests/Unit/Controllers/Metrics/ControllerTest.php | 22 ++++++++---- tests/Unit/Helpers/Stub/files/VERSION | 1 + tests/Unit/Helpers/VersionServiceProviderTest.php | 25 ++++++++++++++ tests/Unit/Helpers/VersionTest.php | 28 +++++++++++++++ 15 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 src/Helpers/Version.php create mode 100644 src/Helpers/VersionServiceProvider.php create mode 100644 storage/app/.gitignore create mode 100644 tests/Unit/Helpers/Stub/files/VERSION create mode 100644 tests/Unit/Helpers/VersionServiceProviderTest.php create mode 100644 tests/Unit/Helpers/VersionTest.php (limited to 'src/Controllers') diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db26ce4c..b9bb8654 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,9 @@ build-image: <<: *docker_definition stage: build script: - - docker build --pull --build-arg NGINX_IMAGE="${TEST_IMAGE}-nginx" -t "${TEST_IMAGE}" -f contrib/Dockerfile . + - apk -q add git + - VERSION="$(git describe --abbrev=0 --tags)-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}" + - docker build --pull --build-arg NGINX_IMAGE="${TEST_IMAGE}-nginx" --build-arg VERSION="${VERSION}" -t "${TEST_IMAGE}" -f contrib/Dockerfile . - docker push "${TEST_IMAGE}" test: diff --git a/config/app.php b/config/app.php index 17fdee11..8850f74e 100644 --- a/config/app.php +++ b/config/app.php @@ -27,6 +27,7 @@ return [ \Engelsystem\Middleware\SessionHandlerServiceProvider::class, // Additional services + \Engelsystem\Helpers\VersionServiceProvider::class, \Engelsystem\Mail\MailerServiceProvider::class, ], diff --git a/contrib/Dockerfile b/contrib/Dockerfile index dd3bd308..f04fff11 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -32,6 +32,9 @@ COPY --from=composer /app/composer.lock /app/ RUN find /app/storage/ -type f -not -name .gitignore -exec rm {} \; RUN rm -f /app/import/* /app/config/config.php +ARG VERSION +RUN if [[ ! -f /app/storage/app/VERSION ]] && [[ ! -z "${VERSION}" ]]; then echo -n "${VERSION}" > /app/storage/app/VERSION; fi + # Build the PHP container FROM php:7-fpm-alpine WORKDIR /var/www diff --git a/resources/views/pages/credits.twig b/resources/views/pages/credits.twig index eb98c7e7..3bb04895 100644 --- a/resources/views/pages/credits.twig +++ b/resources/views/pages/credits.twig @@ -15,6 +15,7 @@

Source code

+

Version: {{ version }}

The original engelsystem was written by cookie. diff --git a/src/Application.php b/src/Application.php index ac69c20a..99c68231 100644 --- a/src/Application.php +++ b/src/Application.php @@ -111,6 +111,7 @@ class Application extends Container $this->instance('path.lang', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'lang'); $this->instance('path.views', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'views'); $this->instance('path.storage', $appPath . DIRECTORY_SEPARATOR . 'storage'); + $this->instance('path.storage.app', $this->get('path.storage') . DIRECTORY_SEPARATOR . 'app'); $this->instance('path.cache', $this->get('path.storage') . DIRECTORY_SEPARATOR . 'cache'); $this->instance('path.cache.routes', $this->get('path.cache') . DIRECTORY_SEPARATOR . 'routes.cache.php'); $this->instance('path.cache.views', $this->get('path.cache') . DIRECTORY_SEPARATOR . 'views'); diff --git a/src/Controllers/CreditsController.php b/src/Controllers/CreditsController.php index b2805b84..ade97649 100644 --- a/src/Controllers/CreditsController.php +++ b/src/Controllers/CreditsController.php @@ -3,6 +3,7 @@ namespace Engelsystem\Controllers; use Engelsystem\Config\Config; +use Engelsystem\Helpers\Version; use Engelsystem\Http\Response; class CreditsController extends BaseController @@ -13,14 +14,19 @@ class CreditsController extends BaseController /** @var Response */ protected $response; + /** @var Version */ + protected $version; + /** * @param Response $response * @param Config $config + * @param Version $version */ - public function __construct(Response $response, Config $config) + public function __construct(Response $response, Config $config, Version $version) { $this->config = $config; $this->response = $response; + $this->version = $version; } /** @@ -30,7 +36,10 @@ class CreditsController extends BaseController { return $this->response->withView( 'pages/credits.twig', - ['credits' => $this->config->get('credits')] + [ + 'credits' => $this->config->get('credits'), + 'version' => $this->version->getVersion(), + ] ); } } diff --git a/src/Controllers/Metrics/Controller.php b/src/Controllers/Metrics/Controller.php index f6ea3967..ffb2a41b 100644 --- a/src/Controllers/Metrics/Controller.php +++ b/src/Controllers/Metrics/Controller.php @@ -4,6 +4,7 @@ namespace Engelsystem\Controllers\Metrics; use Engelsystem\Config\Config; use Engelsystem\Controllers\BaseController; +use Engelsystem\Helpers\Version; use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\Http\Request; use Engelsystem\Http\Response; @@ -26,25 +27,31 @@ class Controller extends BaseController /** @var Stats */ protected $stats; + /** @var Version */ + protected $version; + /** * @param Response $response * @param MetricsEngine $engine * @param Config $config * @param Request $request * @param Stats $stats + * @param Version $version */ public function __construct( Response $response, MetricsEngine $engine, Config $config, Request $request, - Stats $stats + Stats $stats, + Version $version ) { $this->config = $config; $this->engine = $engine; $this->request = $request; $this->response = $response; $this->stats = $stats; + $this->version = $version; } /** @@ -68,6 +75,18 @@ class Controller extends BaseController $data = [ $this->config->get('app_name') . ' stats', + 'info' => [ + 'type' => 'gauge', + 'help' => 'About the environment', + [ + 'labels' => [ + 'os' => PHP_OS_FAMILY, + 'php' => implode('.', [PHP_MAJOR_VERSION, PHP_MINOR_VERSION]), + 'version' => $this->version->getVersion(), + ], + 'value' => 1, + ], + ], 'users' => [ 'type' => 'gauge', ['labels' => ['state' => 'incoming'], 'value' => $this->stats->newUsers()], diff --git a/src/Helpers/Version.php b/src/Helpers/Version.php new file mode 100644 index 00000000..97fe6ef3 --- /dev/null +++ b/src/Helpers/Version.php @@ -0,0 +1,42 @@ +storage = $storage; + $this->config = $config; + } + + /** + * @return string + */ + public function getVersion() + { + $file = $this->storage . DIRECTORY_SEPARATOR . $this->versionFile; + + $version = 'n/a'; + if (file_exists($file)) { + $version = trim(file_get_contents($file)); + } + + return $this->config->get('version', $version); + } +} diff --git a/src/Helpers/VersionServiceProvider.php b/src/Helpers/VersionServiceProvider.php new file mode 100644 index 00000000..41e10158 --- /dev/null +++ b/src/Helpers/VersionServiceProvider.php @@ -0,0 +1,15 @@ +app->when(Version::class) + ->needs('$storage') + ->give($this->app->get('path.storage.app')); + } +} diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 00000000..78d91016 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,2 @@ +/* +!.gitignore diff --git a/tests/Unit/Controllers/CreditsControllerTest.php b/tests/Unit/Controllers/CreditsControllerTest.php index 42ea4ea1..303bf60e 100644 --- a/tests/Unit/Controllers/CreditsControllerTest.php +++ b/tests/Unit/Controllers/CreditsControllerTest.php @@ -4,9 +4,10 @@ namespace Engelsystem\Test\Unit\Controllers; use Engelsystem\Config\Config; use Engelsystem\Controllers\CreditsController; +use Engelsystem\Helpers\Version; use Engelsystem\Http\Response; +use Engelsystem\Test\Unit\TestCase; use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; class CreditsControllerTest extends TestCase { @@ -19,12 +20,17 @@ class CreditsControllerTest extends TestCase /** @var Response|MockObject $response */ $response = $this->createMock(Response::class); $config = new Config(['foo' => 'bar', 'credits' => ['lor' => 'em']]); + /** @var Version|MockObject $version */ + $version = $this->createMock(Version::class); - $response->expects($this->once()) - ->method('withView') - ->with('pages/credits.twig', ['credits' => ['lor' => 'em']]); + $this->setExpects( + $response, + 'withView', + ['pages/credits.twig', ['credits' => ['lor' => 'em'], 'version' => '42.1.0-test']] + ); + $this->setExpects($version, 'getVersion', [], '42.1.0-test'); - $controller = new CreditsController($response, $config); + $controller = new CreditsController($response, $config, $version); $controller->index(); } } diff --git a/tests/Unit/Controllers/Metrics/ControllerTest.php b/tests/Unit/Controllers/Metrics/ControllerTest.php index 18daa96a..f203200c 100644 --- a/tests/Unit/Controllers/Metrics/ControllerTest.php +++ b/tests/Unit/Controllers/Metrics/ControllerTest.php @@ -6,6 +6,7 @@ use Engelsystem\Config\Config; use Engelsystem\Controllers\Metrics\Controller; use Engelsystem\Controllers\Metrics\MetricsEngine; use Engelsystem\Controllers\Metrics\Stats; +use Engelsystem\Helpers\Version; use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\Http\Request; use Engelsystem\Http\Response; @@ -28,7 +29,8 @@ class ControllerTest extends TestCase /** @var MetricsEngine|MockObject $engine */ /** @var Stats|MockObject $stats */ /** @var Config $config */ - list($response, $request, $engine, $stats, $config) = $this->getMocks(); + /** @var Version|MockObject $version */ + list($response, $request, $engine, $stats, $config, $version) = $this->getMocks(); $request->server = new ServerBag(); $request->server->set('REQUEST_TIME_FLOAT', 0.0123456789); @@ -37,6 +39,7 @@ class ControllerTest extends TestCase ->method('get') ->willReturnCallback(function ($path, $data) use ($response) { $this->assertEquals('/metrics', $path); + $this->assertArrayHasKey('info', $data); $this->assertArrayHasKey('users', $data); $this->assertArrayHasKey('licenses', $data); $this->assertArrayHasKey('users_working', $data); @@ -122,7 +125,9 @@ class ControllerTest extends TestCase 'XL' => 'X Large', ]); - $controller = new Controller($response, $engine, $config, $request, $stats); + $this->setExpects($version, 'getVersion', [], '0.42.42'); + + $controller = new Controller($response, $engine, $config, $request, $stats, $version); $controller->metrics(); } @@ -137,7 +142,8 @@ class ControllerTest extends TestCase /** @var MetricsEngine|MockObject $engine */ /** @var Stats|MockObject $stats */ /** @var Config $config */ - list($response, $request, $engine, $stats, $config) = $this->getMocks(); + /** @var Version|MockObject $version */ + list($response, $request, $engine, $stats, $config, $version) = $this->getMocks(); $response->expects($this->once()) ->method('withHeader') @@ -168,7 +174,7 @@ class ControllerTest extends TestCase $this->setExpects($stats, 'arrivedUsers', null, 10, $this->exactly(2)); $this->setExpects($stats, 'currentlyWorkingUsers', null, 5); - $controller = new Controller($response, $engine, $config, $request, $stats); + $controller = new Controller($response, $engine, $config, $request, $stats, $version); $controller->stats(); } @@ -182,7 +188,8 @@ class ControllerTest extends TestCase /** @var MetricsEngine|MockObject $engine */ /** @var Stats|MockObject $stats */ /** @var Config $config */ - list($response, $request, $engine, $stats, $config) = $this->getMocks(); + /** @var Version|MockObject $version */ + list($response, $request, $engine, $stats, $config, $version) = $this->getMocks(); $request->expects($this->once()) ->method('get') @@ -191,7 +198,7 @@ class ControllerTest extends TestCase $config->set('api_key', 'fooBar!'); - $controller = new Controller($response, $engine, $config, $request, $stats); + $controller = new Controller($response, $engine, $config, $request, $stats, $version); $this->expectException(HttpForbidden::class); $this->expectExceptionMessage(json_encode(['error' => 'The api_key is invalid'])); @@ -212,7 +219,8 @@ class ControllerTest extends TestCase /** @var Stats|MockObject $stats */ $stats = $this->createMock(Stats::class); $config = new Config(); + $version = $this->createMock(Version::class); - return [$response, $request, $engine, $stats, $config]; + return [$response, $request, $engine, $stats, $config, $version]; } } diff --git a/tests/Unit/Helpers/Stub/files/VERSION b/tests/Unit/Helpers/Stub/files/VERSION new file mode 100644 index 00000000..749a96f3 --- /dev/null +++ b/tests/Unit/Helpers/Stub/files/VERSION @@ -0,0 +1 @@ +0.42.0-testing diff --git a/tests/Unit/Helpers/VersionServiceProviderTest.php b/tests/Unit/Helpers/VersionServiceProviderTest.php new file mode 100644 index 00000000..609c649d --- /dev/null +++ b/tests/Unit/Helpers/VersionServiceProviderTest.php @@ -0,0 +1,25 @@ +instance('path.storage.app', '/tmp'); + + $serviceProvider = new VersionServiceProvider($app); + $serviceProvider->register(); + + $this->assertArrayHasKey(Version::class, $app->contextual); + } +} diff --git a/tests/Unit/Helpers/VersionTest.php b/tests/Unit/Helpers/VersionTest.php new file mode 100644 index 00000000..40569abb --- /dev/null +++ b/tests/Unit/Helpers/VersionTest.php @@ -0,0 +1,28 @@ +assertEquals('n/a', $version->getVersion()); + + $version = new Version(__DIR__ . '/Stub/files', $config); + $this->assertEquals('0.42.0-testing', $version->getVersion()); + + $config->set('version', '1.2.3-dev'); + $this->assertEquals('1.2.3-dev', $version->getVersion()); + } +} -- cgit v1.2.3-54-g00ecf From b03102e3c613bd057f117a145d94aec4c977006c Mon Sep 17 00:00:00 2001 From: msquare Date: Sun, 21 Jul 2019 12:37:01 +0200 Subject: AuthController return types --- src/Controllers/AuthController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/Controllers') diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index a8cc1ace..55dd56b0 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -53,7 +53,7 @@ class AuthController extends BaseController /** * @return Response */ - public function login() + public function login(): Response { return $this->showLogin(); } @@ -62,7 +62,7 @@ class AuthController extends BaseController * @param bool $showRecovery * @return Response */ - protected function showLogin($showRecovery = false) + protected function showLogin($showRecovery = false): Response { $errors = Collection::make(Arr::flatten($this->session->get('errors', []))); $this->session->remove('errors'); -- cgit v1.2.3-54-g00ecf