+ {{ msg() }}
{% for message in errors|default([]) %}
{{ m.alert(__(message), 'danger') }}
{% endfor %}
@@ -61,7 +62,7 @@
diff --git a/resources/views/pages/password/reset-form.twig b/resources/views/pages/password/reset-form.twig
new file mode 100644
index 00000000..60eb2499
--- /dev/null
+++ b/resources/views/pages/password/reset-form.twig
@@ -0,0 +1,18 @@
+{% extends "pages/password/reset.twig" %}
+{% import 'macros/base.twig' as m %}
+{% import 'macros/form.twig' as f %}
+
+{% block row_content %}
+
+{% endblock %}
diff --git a/resources/views/pages/password/reset-success.twig b/resources/views/pages/password/reset-success.twig
new file mode 100644
index 00000000..19b8a93e
--- /dev/null
+++ b/resources/views/pages/password/reset-success.twig
@@ -0,0 +1,12 @@
+{% extends "pages/password/reset.twig" %}
+{% import 'macros/base.twig' as m %}
+
+{% block row_content %}
+
+ {% if type == 'email' %}
+ {{ m.alert(__('We sent you an email containing your password recovery link.'), 'info') }}
+ {% elseif type == 'reset' %}
+ {{ m.alert(__('Password saved.'), 'success') }}
+ {% endif %}
+
+{% endblock %}
diff --git a/resources/views/pages/password/reset.twig b/resources/views/pages/password/reset.twig
new file mode 100644
index 00000000..289152ea
--- /dev/null
+++ b/resources/views/pages/password/reset.twig
@@ -0,0 +1,32 @@
+{% extends 'layouts/app.twig' %}
+{% import 'macros/base.twig' as m %}
+{% import 'macros/form.twig' as f %}
+
+{% block title %}{{ __('Password recovery') }}{% endblock %}
+
+{% block content %}
+
+
{{ __('Password recovery') }}
+
+ {% for message in errors|default([]) %}
+ {{ m.alert(__(message), 'danger') }}
+ {% endfor %}
+
+
+ {% block row_content %}
+
+ {% endblock %}
+
+
+{% endblock %}
diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php
index c69c2377..7892064b 100644
--- a/src/Controllers/AuthController.php
+++ b/src/Controllers/AuthController.php
@@ -88,7 +88,7 @@ class AuthController extends BaseController
$user = $this->auth->authenticate($data['login'], $data['password']);
if (!$user instanceof User) {
- $this->session->set('errors', $this->session->get('errors', []) + ['auth.not-found']);
+ $this->session->set('errors', array_merge($this->session->get('errors', []), ['auth.not-found']));
return $this->showLogin();
}
diff --git a/src/Controllers/PasswordResetController.php b/src/Controllers/PasswordResetController.php
new file mode 100644
index 00000000..505ed8eb
--- /dev/null
+++ b/src/Controllers/PasswordResetController.php
@@ -0,0 +1,167 @@
+ 'login',
+ 'postReset' => 'login',
+ 'resetPassword' => 'login',
+ 'postResetPassword' => 'login',
+ ];
+
+ /**
+ * @param Response $response
+ * @param SessionInterface $session
+ * @param EngelsystemMailer $mail
+ * @param LoggerInterface $log
+ */
+ public function __construct(
+ Response $response,
+ SessionInterface $session,
+ EngelsystemMailer $mail,
+ LoggerInterface $log
+ ) {
+ $this->log = $log;
+ $this->mail = $mail;
+ $this->response = $response;
+ $this->session = $session;
+ }
+
+ /**
+ * @return Response
+ */
+ public function reset(): Response
+ {
+ return $this->showView('pages/password/reset');
+ }
+
+ /**
+ * @param Request $request
+ * @return Response
+ */
+ public function postReset(Request $request): Response
+ {
+ $data = $this->validate($request, [
+ 'email' => 'required|email',
+ ]);
+
+ /** @var User $user */
+ $user = User::whereEmail($data['email'])->first();
+ if ($user) {
+ $reset = PasswordReset::findOrNew($user->id);
+ $reset->user_id = $user->id;
+ $reset->token = md5(random_bytes(64));
+ $reset->save();
+
+ $this->log->info(
+ sprintf('Password recovery for %s (%u)', $user->name, $user->id),
+ ['user' => $user->toJson()]
+ );
+
+ $this->mail->sendViewTranslated(
+ $user,
+ 'Password recovery',
+ 'emails/password-reset',
+ ['username' => $user->name, 'reset' => $reset]
+ );
+ }
+
+ return $this->showView('pages/password/reset-success', ['type' => 'email']);
+ }
+
+ /**
+ * @param Request $request
+ * @return Response
+ */
+ public function resetPassword(Request $request): Response
+ {
+ $this->requireToken($request);
+
+ return $this->showView('pages/password/reset-form');
+ }
+
+ /**
+ * @param Request $request
+ * @return Response
+ */
+ public function postResetPassword(Request $request): Response
+ {
+ $reset = $this->requireToken($request);
+
+ $data = $this->validate($request, [
+ 'password' => 'required|min:' . config('min_password_length'),
+ 'password_confirmation' => 'required',
+ ]);
+
+ if ($data['password'] !== $data['password_confirmation']) {
+ $this->session->set('errors',
+ array_merge($this->session->get('errors', []), ['validation.password.confirmed']));
+
+ return $this->showView('pages/password/reset-form');
+ }
+
+ auth()->setPassword($reset->user, $data['password']);
+ $reset->delete();
+
+ return $this->showView('pages/password/reset-success', ['type' => 'reset']);
+ }
+
+ /**
+ * @param string $view
+ * @param array $data
+ * @return Response
+ */
+ protected function showView($view = 'pages/password/reset', $data = []): Response
+ {
+ $errors = Collection::make(Arr::flatten($this->session->get('errors', [])));
+ $this->session->remove('errors');
+
+ return $this->response->withView(
+ $view,
+ array_merge_recursive(['errors' => $errors], $data)
+ );
+ }
+
+ /**
+ * @param Request $request
+ * @return PasswordReset
+ */
+ protected function requireToken(Request $request): PasswordReset
+ {
+ $token = $request->getAttribute('token');
+ /** @var PasswordReset|null $reset */
+ $reset = PasswordReset::whereToken($token)->first();
+
+ if (!$reset) {
+ throw new HttpNotFound();
+ }
+
+ return $reset;
+ }
+}
diff --git a/src/Helpers/Translation/TranslationServiceProvider.php b/src/Helpers/Translation/TranslationServiceProvider.php
index 62247000..6df9b0fe 100644
--- a/src/Helpers/Translation/TranslationServiceProvider.php
+++ b/src/Helpers/Translation/TranslationServiceProvider.php
@@ -41,8 +41,10 @@ class TranslationServiceProvider extends ServiceProvider
'localeChangeCallback' => [$this, 'setLocale'],
]
);
- $this->app->instance(Translator::class, $translator);
- $this->app->instance('translator', $translator);
+ $this->app->singleton(Translator::class, function () use ($translator) {
+ return $translator;
+ });
+ $this->app->alias(Translator::class, 'translator');
}
/**
diff --git a/src/Http/Exceptions/HttpNotFound.php b/src/Http/Exceptions/HttpNotFound.php
new file mode 100644
index 00000000..324adaf9
--- /dev/null
+++ b/src/Http/Exceptions/HttpNotFound.php
@@ -0,0 +1,23 @@
+view = $view;
+ $this->renderer = $renderer;
parent::__construct($content, $status, $headers);
}
@@ -47,7 +48,7 @@ class Response extends SymfonyResponse implements ResponseInterface
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
* @return static
- * @throws \InvalidArgumentException For invalid status code arguments.
+ * @throws InvalidArgumentException For invalid status code arguments.
*/
public function withStatus($code, $reasonPhrase = '')
{
@@ -107,12 +108,12 @@ class Response extends SymfonyResponse implements ResponseInterface
*/
public function withView($view, $data = [], $status = 200, $headers = [])
{
- if (!$this->view instanceof Renderer) {
- throw new \InvalidArgumentException('Renderer not defined');
+ if (!$this->renderer instanceof Renderer) {
+ throw new InvalidArgumentException('Renderer not defined');
}
$new = clone $this;
- $new->setContent($this->view->render($view, $data));
+ $new->setContent($this->renderer->render($view, $data));
$new->setStatusCode($status, ($status == $this->getStatusCode() ? $this->statusText : null));
foreach ($headers as $key => $values) {
@@ -144,4 +145,14 @@ class Response extends SymfonyResponse implements ResponseInterface
return $response;
}
+
+ /**
+ * Set the renderer to use
+ *
+ * @param Renderer $renderer
+ */
+ public function setRenderer(Renderer $renderer)
+ {
+ $this->renderer = $renderer;
+ }
}
diff --git a/src/Http/Validation/Rules/Between.php b/src/Http/Validation/Rules/Between.php
new file mode 100644
index 00000000..106a93ac
--- /dev/null
+++ b/src/Http/Validation/Rules/Between.php
@@ -0,0 +1,10 @@
+isDateTime($input)
+ ) {
+ $input = Str::length($input);
+ }
+
+ return parent::validate($input);
+ }
+
+ /**
+ * @param mixed $input
+ * @return bool
+ */
+ protected function isDateTime($input): bool
+ {
+ try {
+ new DateTime($input);
+ } catch (Throwable $e) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Mail/EngelsystemMailer.php b/src/Mail/EngelsystemMailer.php
index 81660681..87915d67 100644
--- a/src/Mail/EngelsystemMailer.php
+++ b/src/Mail/EngelsystemMailer.php
@@ -2,6 +2,8 @@
namespace Engelsystem\Mail;
+use Engelsystem\Helpers\Translation\Translator;
+use Engelsystem\Models\User\User;
use Engelsystem\Renderer\Renderer;
use Swift_Mailer as SwiftMailer;
@@ -10,30 +12,75 @@ class EngelsystemMailer extends Mailer
/** @var Renderer|null */
protected $view;
+ /** @var Translator|null */
+ protected $translation;
+
/** @var string */
protected $subjectPrefix = null;
/**
* @param SwiftMailer $mailer
* @param Renderer $view
+ * @param Translator $translation
*/
- public function __construct(SwiftMailer $mailer, Renderer $view = null)
+ public function __construct(SwiftMailer $mailer, Renderer $view = null, Translator $translation = null)
{
parent::__construct($mailer);
+ $this->translation = $translation;
$this->view = $view;
}
+ /**
+ * @param string|string[]|User $to
+ * @param string $subject
+ * @param string $template
+ * @param array $data
+ * @param string|null $locale
+ * @return int
+ */
+ public function sendViewTranslated(
+ $to,
+ string $subject,
+ string $template,
+ array $data = [],
+ ?string $locale = null
+ ): int {
+ if ($to instanceof User) {
+ $locale = $locale ?: $to->settings->language;
+ $to = $to->contact->email ? $to->contact->email : $to->email;
+ }
+
+ $activeLocale = null;
+ if (
+ $locale
+ && $this->translation
+ && isset($this->translation->getLocales()[$locale])
+ ) {
+ $activeLocale = $this->translation->getLocale();
+ $this->translation->setLocale($locale);
+ }
+
+ $subject = $this->translation ? $this->translation->translate($subject) : $subject;
+ $sentMails = $this->sendView($to, $subject, $template, $data);
+
+ if ($activeLocale) {
+ $this->translation->setLocale($activeLocale);
+ }
+
+ return $sentMails;
+ }
+
/**
* Send a template
*
- * @param string $to
- * @param string $subject
- * @param string $template
- * @param array $data
+ * @param string|string[] $to
+ * @param string $subject
+ * @param string $template
+ * @param array $data
* @return int
*/
- public function sendView($to, $subject, $template, $data = []): int
+ public function sendView($to, string $subject, string $template, array $data = []): int
{
$body = $this->view->render($template, $data);
diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php
index 27a15faa..11508e1c 100644
--- a/src/Middleware/LegacyMiddleware.php
+++ b/src/Middleware/LegacyMiddleware.php
@@ -26,7 +26,6 @@ class LegacyMiddleware implements MiddlewareInterface
'shifts_json_export',
'users',
'user_driver_licenses',
- 'user_password_recovery',
'user_worklog',
];
@@ -112,11 +111,6 @@ class LegacyMiddleware implements MiddlewareInterface
case 'shifts_json_export':
require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php');
shifts_json_export_controller();
- case 'user_password_recovery':
- require_once realpath(__DIR__ . '/../../includes/controller/users_controller.php');
- $title = user_password_recovery_title();
- $content = user_password_recovery_controller();
- return [$title, $content];
case 'public_dashboard':
return public_dashboard_controller();
case 'angeltypes':
diff --git a/src/Renderer/Twig/Extensions/Legacy.php b/src/Renderer/Twig/Extensions/Legacy.php
index 79de32cb..55c095fc 100644
--- a/src/Renderer/Twig/Extensions/Legacy.php
+++ b/src/Renderer/Twig/Extensions/Legacy.php
@@ -32,6 +32,7 @@ class Legacy extends TwigExtension
new TwigFunction('menuUserHints', 'header_render_hints', $isSafeHtml),
new TwigFunction('menuUserSubmenu', 'make_user_submenu', $isSafeHtml),
new TwigFunction('page', [$this, 'getPage']),
+ new TwigFunction('msg', 'msg', $isSafeHtml),
];
}
diff --git a/tests/Unit/Controllers/AuthControllerTest.php b/tests/Unit/Controllers/AuthControllerTest.php
index 6c237264..a12ed6d6 100644
--- a/tests/Unit/Controllers/AuthControllerTest.php
+++ b/tests/Unit/Controllers/AuthControllerTest.php
@@ -12,9 +12,8 @@ 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 Engelsystem\Test\Unit\TestCase;
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;
@@ -66,6 +65,7 @@ class AuthControllerTest extends TestCase
$session = new Session(new MockArraySessionStorage());
/** @var Validator|MockObject $validator */
$validator = new Validator();
+ $session->set('errors', [['bar' => 'some.bar.error']]);
$user = new User([
'name' => 'foo',
@@ -89,7 +89,7 @@ class AuthControllerTest extends TestCase
$response->expects($this->once())
->method('withView')
- ->with('pages/login', ['errors' => Collection::make(['auth.not-found'])])
+ ->with('pages/login', ['errors' => collect(['some.bar.error', 'auth.not-found'])])
->willReturn($response);
$response->expects($this->once())
->method('redirectTo')
diff --git a/tests/Unit/Controllers/Metrics/StatsTest.php b/tests/Unit/Controllers/Metrics/StatsTest.php
index fa78d8c3..9204f7db 100644
--- a/tests/Unit/Controllers/Metrics/StatsTest.php
+++ b/tests/Unit/Controllers/Metrics/StatsTest.php
@@ -155,8 +155,8 @@ class StatsTest extends TestCase
$this->initDatabase();
$this->addUsers();
- (new PasswordReset(['use_id' => 1, 'token' => 'loremIpsum123']))->save();
- (new PasswordReset(['use_id' => 3, 'token' => '5omeR4nd0mTok3N']))->save();
+ (new PasswordReset(['user_id' => 1, 'token' => 'loremIpsum123']))->save();
+ (new PasswordReset(['user_id' => 3, 'token' => '5omeR4nd0mTok3N']))->save();
$stats = new Stats($this->database);
$this->assertEquals(2, $stats->passwordResets());
diff --git a/tests/Unit/Controllers/PasswordResetControllerTest.php b/tests/Unit/Controllers/PasswordResetControllerTest.php
new file mode 100644
index 00000000..54046cef
--- /dev/null
+++ b/tests/Unit/Controllers/PasswordResetControllerTest.php
@@ -0,0 +1,266 @@
+getController('pages/password/reset');
+ $response = $controller->reset();
+
+ $this->assertEquals(200, $response->getStatusCode());
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::postReset
+ */
+ public function testPostReset(): void
+ {
+ $this->initDatabase();
+ $request = new Request([], ['email' => 'foo@bar.batz']);
+ $user = $this->createUser();
+
+ $controller = $this->getController(
+ 'pages/password/reset-success',
+ ['type' => 'email', 'errors' => collect()]
+ );
+ /** @var TestLogger $log */
+ $log = $this->args['log'];
+ /** @var EngelsystemMailer|MockObject $mailer */
+ $mailer = $this->args['mailer'];
+ $this->setExpects($mailer, 'sendViewTranslated');
+
+ $controller->postReset($request);
+
+ $this->assertNotEmpty(PasswordReset::find($user->id)->first());
+ $this->assertTrue($log->hasInfoThatContains($user->name));
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::postReset
+ */
+ public function testPostResetInvalidRequest(): void
+ {
+ $request = new Request();
+
+ $controller = $this->getController();
+
+ $this->expectException(ValidationException::class);
+ $controller->postReset($request);
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::postReset
+ */
+ public function testPostResetNoUser(): void
+ {
+ $this->initDatabase();
+ $request = new Request([], ['email' => 'foo@bar.batz']);
+
+ $controller = $this->getController(
+ 'pages/password/reset-success',
+ ['type' => 'email', 'errors' => collect()]
+ );
+
+ $controller->postReset($request);
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::resetPassword
+ * @covers \Engelsystem\Controllers\PasswordResetController::requireToken
+ */
+ public function testResetPassword(): void
+ {
+ $this->initDatabase();
+
+ $user = $this->createUser();
+ $token = $this->createToken($user);
+ $request = new Request([], [], ['token' => $token->token]);
+
+ $controller = $this->getController('pages/password/reset-form');
+
+ $controller->resetPassword($request);
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::resetPassword
+ * @covers \Engelsystem\Controllers\PasswordResetController::requireToken
+ */
+ public function testResetPasswordNoToken(): void
+ {
+ $this->initDatabase();
+ $controller = $this->getController();
+
+ $this->expectException(HttpNotFound::class);
+ $controller->resetPassword(new Request());
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::postResetPassword
+ */
+ public function testPostResetPassword(): void
+ {
+ $this->initDatabase();
+
+ $this->app->instance('config', new Config(['min_password_length' => 3]));
+ $user = $this->createUser();
+ $token = $this->createToken($user);
+ $password = 'SomeRandomPasswordForAmazingSecurity';
+ $request = new Request(
+ [],
+ ['password' => $password, 'password_confirmation' => $password],
+ ['token' => $token->token]
+ );
+
+ $controller = $this->getController(
+ 'pages/password/reset-success',
+ ['type' => 'reset', 'errors' => collect()]
+ );
+
+ $auth = new Authenticator($request, $this->args['session'], $user);
+ $this->app->instance('authenticator', $auth);
+
+ $response = $controller->postResetPassword($request);
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $this->assertEmpty(PasswordReset::find($user->id));
+ $this->assertNotNull(auth()->authenticate($user->name, $password));
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::postResetPassword
+ * @covers \Engelsystem\Controllers\PasswordResetController::showView
+ */
+ public function testPostResetPasswordNotMatching(): void
+ {
+ $this->initDatabase();
+
+ $this->app->instance('config', new Config(['min_password_length' => 3]));
+ $user = $this->createUser();
+ $token = $this->createToken($user);
+ $password = 'SomeRandomPasswordForAmazingSecurity';
+ $request = new Request(
+ [],
+ ['password' => $password, 'password_confirmation' => $password . 'OrNot'],
+ ['token' => $token->token]
+ );
+
+ $controller = $this->getController(
+ 'pages/password/reset-form',
+ ['errors' => collect(['some.other.error', 'validation.password.confirmed'])]
+ );
+ /** @var Session $session */
+ $session = $this->args['session'];
+ $session->set('errors', ['foo' => ['bar' => 'some.other.error']]);
+
+ $controller->postResetPassword($request);
+ $this->assertEmpty($session->get('errors'));
+ }
+
+ /**
+ * @return array
+ */
+ protected function getControllerArgs(): array
+ {
+ $response = new Response();
+ $session = new Session(new MockArraySessionStorage());
+ /** @var EngelsystemMailer|MockObject $mailer */
+ $mailer = $this->createMock(EngelsystemMailer::class);
+ $log = new TestLogger();
+ $renderer = $this->createMock(Renderer::class);
+ $response->setRenderer($renderer);
+
+ return $this->args = [
+ 'response' => $response,
+ 'session' => $session,
+ 'mailer' => $mailer,
+ 'log' => $log,
+ 'renderer' => $renderer
+ ];
+ }
+
+ /**
+ * @param string $view
+ * @param array $data
+ * @return PasswordResetController
+ */
+ protected function getController(?string $view = null, ?array $data = null): PasswordResetController
+ {
+ /** @var Response $response */
+ /** @var Session $session */
+ /** @var EngelsystemMailer|MockObject $mailer */
+ /** @var TestLogger $log */
+ /** @var Renderer|MockObject $renderer */
+ list($response, $session, $mailer, $log, $renderer) = array_values($this->getControllerArgs());
+ $controller = new PasswordResetController($response, $session, $mailer, $log);
+ $controller->setValidator(new Validator());
+
+ if ($view) {
+ $args = [$view];
+ if ($data) {
+ $args[] = $data;
+ }
+
+ $this->setExpects($renderer, 'render', $args, 'Foo');
+ }
+
+ return $controller;
+ }
+
+ /**
+ * @return User
+ */
+ protected function createUser(): User
+ {
+ $user = new User([
+ 'name' => 'foo',
+ 'password' => '',
+ 'email' => 'foo@bar.batz',
+ 'api_key' => '',
+ ]);
+ $user->save();
+
+ return $user;
+ }
+
+ /**
+ * @param User $user
+ * @return PasswordReset
+ */
+ protected function createToken(User $user): PasswordReset
+ {
+ $reset = new PasswordReset(['user_id' => $user->id, 'token' => 'SomeTestToken123']);
+ $reset->save();
+
+ return $reset;
+ }
+}
diff --git a/tests/Unit/HasDatabase.php b/tests/Unit/HasDatabase.php
index 7a58bb2b..dbaa253e 100644
--- a/tests/Unit/HasDatabase.php
+++ b/tests/Unit/HasDatabase.php
@@ -2,7 +2,6 @@
namespace Engelsystem\Test\Unit;
-use Engelsystem\Application;
use Engelsystem\Database\Database;
use Engelsystem\Database\Migration\Migrate;
use Engelsystem\Database\Migration\MigrationServiceProvider;
@@ -27,12 +26,11 @@ trait HasDatabase
$connection->getPdo()->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->database = new Database($connection);
- $app = new Application();
- $app->instance(Database::class, $this->database);
- $app->register(MigrationServiceProvider::class);
+ $this->app->instance(Database::class, $this->database);
+ $this->app->register(MigrationServiceProvider::class);
/** @var Migrate $migration */
- $migration = $app->get('db.migration');
+ $migration = $this->app->get('db.migration');
$migration->initMigration();
$this->database
diff --git a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
index dc8e8af5..e55fdf02 100644
--- a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
+++ b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
@@ -21,7 +21,7 @@ class TranslationServiceProviderTest extends ServiceProviderTest
$locales = ['fo_OO' => 'Foo', 'fo_OO.BAR' => 'Foo (Bar)', 'te_ST.WTF-9' => 'WTF\'s Testing?'];
$config = new Config(['locales' => $locales, 'default_locale' => $defaultLocale]);
- $app = $this->getApp(['make', 'instance', 'get']);
+ $app = $this->getApp(['make', 'singleton', 'alias', 'get']);
/** @var Session|MockObject $session */
$session = $this->createMock(Session::class);
/** @var Translator|MockObject $translator */
@@ -60,12 +60,16 @@ class TranslationServiceProviderTest extends ServiceProviderTest
)
->willReturn($translator);
- $app->expects($this->exactly(2))
- ->method('instance')
- ->withConsecutive(
- [Translator::class, $translator],
- ['translator', $translator]
- );
+ $app->expects($this->once())
+ ->method('singleton')
+ ->willReturnCallback(function (string $abstract, callable $callback) use ($translator) {
+ $this->assertEquals(Translator::class, $abstract);
+ $this->assertEquals($translator, $callback());
+ });
+
+ $app->expects($this->once())
+ ->method('alias')
+ ->with(Translator::class, 'translator');
$serviceProvider->register();
}
diff --git a/tests/Unit/Http/Exceptions/HttpNotFoundTest.php b/tests/Unit/Http/Exceptions/HttpNotFoundTest.php
new file mode 100644
index 00000000..a39ea087
--- /dev/null
+++ b/tests/Unit/Http/Exceptions/HttpNotFoundTest.php
@@ -0,0 +1,22 @@
+assertEquals(404, $exception->getStatusCode());
+ $this->assertEquals('', $exception->getMessage());
+
+ $exception = new HttpNotFound('Nothing to see here!');
+ $this->assertEquals('Nothing to see here!', $exception->getMessage());
+ }
+}
diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php
index 34f76513..b8e6e527 100644
--- a/tests/Unit/Http/ResponseTest.php
+++ b/tests/Unit/Http/ResponseTest.php
@@ -55,6 +55,7 @@ class ResponseTest extends TestCase
/**
* @covers \Engelsystem\Http\Response::withView
+ * @covers \Engelsystem\Http\Response::setRenderer
*/
public function testWithView()
{
@@ -73,6 +74,17 @@ class ResponseTest extends TestCase
$this->assertEquals('Foo ipsum!', $newResponse->getContent());
$this->assertEquals(505, $newResponse->getStatusCode());
$this->assertArraySubset(['test' => ['er']], $newResponse->getHeaders());
+
+ /** @var REnderer|MockObject $renderer */
+ $anotherRenderer = $this->createMock(Renderer::class);
+ $anotherRenderer->expects($this->once())
+ ->method('render')
+ ->with('bar')
+ ->willReturn('Stuff');
+
+ $response->setRenderer($anotherRenderer);
+ $response = $response->withView('bar');
+ $this->assertEquals('Stuff', $response->getContent());
}
/**
diff --git a/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php b/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php
index 14f23c00..0325ccfe 100644
--- a/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php
+++ b/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php
@@ -4,7 +4,7 @@ namespace Engelsystem\Test\Unit\Http\SessionHandlers;
use Engelsystem\Http\SessionHandlers\DatabaseHandler;
use Engelsystem\Test\Unit\HasDatabase;
-use PHPUnit\Framework\TestCase;
+use Engelsystem\Test\Unit\TestCase;
class DatabaseHandlerTest extends TestCase
{
@@ -90,6 +90,7 @@ class DatabaseHandlerTest extends TestCase
*/
protected function setUp(): void
{
+ parent::setUp();
$this->initDatabase();
}
}
diff --git a/tests/Unit/Http/Validation/Rules/BetweenTest.php b/tests/Unit/Http/Validation/Rules/BetweenTest.php
new file mode 100644
index 00000000..130d2f93
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/BetweenTest.php
@@ -0,0 +1,28 @@
+assertFalse($rule->validate(1));
+ $this->assertFalse($rule->validate('11'));
+ $this->assertTrue($rule->validate(5));
+ $this->assertFalse($rule->validate('AS'));
+ $this->assertFalse($rule->validate('TestContentThatCounts'));
+ $this->assertTrue($rule->validate('TESTING'));
+
+ $rule = new Between('2042-01-01', '2042-10-10');
+ $this->assertFalse($rule->validate('2000-01-01'));
+ $this->assertFalse($rule->validate('3000-01-01'));
+ $this->assertTrue($rule->validate('2042-05-11'));
+ }
+}
diff --git a/tests/Unit/Http/Validation/Rules/MaxTest.php b/tests/Unit/Http/Validation/Rules/MaxTest.php
new file mode 100644
index 00000000..3f4d9516
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/MaxTest.php
@@ -0,0 +1,26 @@
+assertFalse($rule->validate(10));
+ $this->assertFalse($rule->validate('22'));
+ $this->assertTrue($rule->validate(3));
+ $this->assertFalse($rule->validate('TEST'));
+ $this->assertTrue($rule->validate('AS'));
+
+ $rule = new Max('2042-01-01');
+ $this->assertFalse($rule->validate('2100-01-01'));
+ $this->assertTrue($rule->validate('2000-01-01'));
+ }
+}
diff --git a/tests/Unit/Http/Validation/Rules/MinTest.php b/tests/Unit/Http/Validation/Rules/MinTest.php
new file mode 100644
index 00000000..56350802
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/MinTest.php
@@ -0,0 +1,26 @@
+assertFalse($rule->validate(1));
+ $this->assertFalse($rule->validate('2'));
+ $this->assertTrue($rule->validate(3));
+ $this->assertFalse($rule->validate('AS'));
+ $this->assertTrue($rule->validate('TEST'));
+
+ $rule = new Min('2042-01-01');
+ $this->assertFalse($rule->validate('2000-01-01'));
+ $this->assertTrue($rule->validate('2345-01-01'));
+ }
+}
diff --git a/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php b/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php
new file mode 100644
index 00000000..5c4dc512
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php
@@ -0,0 +1,37 @@
+validate($input);
+
+ $this->assertEquals($expectedInput, $rule->lastInput);
+ }
+
+ /**
+ * @return array[]
+ */
+ public function validateProvider()
+ {
+ return [
+ ['TEST', 4],
+ ['?', 1],
+ ['2042-01-01 00:00', '2042-01-01 00:00'],
+ ['3', '3'],
+ ];
+ }
+}
diff --git a/tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php b/tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php
new file mode 100644
index 00000000..1b6aaaf5
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php
@@ -0,0 +1,23 @@
+lastInput = $input;
+
+ return $this->validateResult;
+ }
+}
diff --git a/tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php b/tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php
new file mode 100644
index 00000000..3522304c
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php
@@ -0,0 +1,10 @@
+assertFalse($val->validate(
- ['lorem' => 2],
- ['lorem' => 'required|min:3|max:10']
+ ['lorem' => 'OMG'],
+ ['lorem' => 'required|min:4|max:10']
));
+ $this->assertEquals(['lorem' => ['validation.lorem.min']], $val->getErrors());
$this->assertFalse($val->validate(
['lorem' => 42],
['lorem' => 'required|min:3|max:10']
diff --git a/tests/Unit/Mail/EngelsystemMailerTest.php b/tests/Unit/Mail/EngelsystemMailerTest.php
index 12dc3b0b..cdbdf435 100644
--- a/tests/Unit/Mail/EngelsystemMailerTest.php
+++ b/tests/Unit/Mail/EngelsystemMailerTest.php
@@ -2,15 +2,22 @@
namespace Engelsystem\Test\Unit\Mail;
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Mail\EngelsystemMailer;
+use Engelsystem\Models\User\Contact;
+use Engelsystem\Models\User\Settings;
+use Engelsystem\Models\User\User;
use Engelsystem\Renderer\Renderer;
+use Engelsystem\Test\Unit\HasDatabase;
+use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
-use PHPUnit\Framework\TestCase;
use Swift_Mailer as SwiftMailer;
use Swift_Message as SwiftMessage;
class EngelsystemMailerTest extends TestCase
{
+ use HasDatabase;
+
/**
* @covers \Engelsystem\Mail\EngelsystemMailer::__construct
* @covers \Engelsystem\Mail\EngelsystemMailer::sendView
@@ -24,21 +31,69 @@ class EngelsystemMailerTest extends TestCase
/** @var EngelsystemMailer|MockObject $mailer */
$mailer = $this->getMockBuilder(EngelsystemMailer::class)
->setConstructorArgs(['mailer' => $swiftMailer, 'view' => $view])
- ->setMethods(['send'])
+ ->onlyMethods(['send'])
->getMock();
- $mailer->expects($this->once())
- ->method('send')
- ->with('foo@bar.baz', 'Lorem dolor', 'Rendered Stuff!')
- ->willReturn(1);
- $view->expects($this->once())
- ->method('render')
- ->with('test/template.tpl', ['dev' => true])
- ->willReturn('Rendered Stuff!');
+ $this->setExpects($mailer, 'send', ['foo@bar.baz', 'Lorem dolor', 'Rendered Stuff!'], 1);
+ $this->setExpects($view, 'render', ['test/template.tpl', ['dev' => true]], 'Rendered Stuff!');
$return = $mailer->sendView('foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]);
$this->equalTo(1, $return);
}
+ /**
+ * @covers \Engelsystem\Mail\EngelsystemMailer::sendViewTranslated
+ */
+ public function testSendViewTranslated()
+ {
+ $this->initDatabase();
+
+ $settings = new Settings([
+ 'language' => 'de_DE',
+ 'theme' => '',
+ ]);
+ $contact = new Contact(['email' => null]);
+ $user = new User([
+ 'id' => 42,
+ 'name' => 'username',
+ 'email' => 'foo@bar.baz',
+ 'password' => '',
+ 'api_key' => '',
+ ]);
+ $user->save();
+ $settings->user()->associate($user)->save();
+ $contact->user()->associate($user)->save();
+
+ /** @var Renderer|MockObject $view */
+ $view = $this->createMock(Renderer::class);
+ /** @var SwiftMailer|MockObject $swiftMailer */
+ $swiftMailer = $this->createMock(SwiftMailer::class);
+ /** @var Translator|MockObject $translator */
+ $translator = $this->createMock(Translator::class);
+
+ /** @var EngelsystemMailer|MockObject $mailer */
+ $mailer = $this->getMockBuilder(EngelsystemMailer::class)
+ ->setConstructorArgs(['mailer' => $swiftMailer, 'view' => $view, 'translation' => $translator])
+ ->onlyMethods(['sendView'])
+ ->getMock();
+
+ $this->setExpects($mailer, 'sendView', ['foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]], 1);
+ $this->setExpects($translator, 'getLocales', null, ['de_DE' => 'de_DE', 'en_US' => 'en_US']);
+ $this->setExpects($translator, 'getLocale', null, 'en_US');
+ $this->setExpects($translator, 'translate', ['translatable.text'], 'Lorem dolor');
+ $translator->expects($this->exactly(2))
+ ->method('setLocale')
+ ->withConsecutive(['de_DE'], ['en_US']);
+
+ $return = $mailer->sendViewTranslated(
+ $user,
+ 'translatable.text',
+ 'test/template.tpl',
+ ['dev' => true],
+ 'de_DE'
+ );
+ $this->equalTo(1, $return);
+ }
+
/**
* @covers \Engelsystem\Mail\EngelsystemMailer::getSubjectPrefix
* @covers \Engelsystem\Mail\EngelsystemMailer::send
@@ -50,32 +105,12 @@ class EngelsystemMailerTest extends TestCase
$message = $this->createMock(SwiftMessage::class);
/** @var SwiftMailer|MockObject $swiftMailer */
$swiftMailer = $this->createMock(SwiftMailer::class);
- $swiftMailer->expects($this->once())
- ->method('createMessage')
- ->willReturn($message);
- $swiftMailer->expects($this->once())
- ->method('send')
- ->willReturn(1);
-
- $message->expects($this->once())
- ->method('setTo')
- ->with(['to@xam.pel'])
- ->willReturn($message);
-
- $message->expects($this->once())
- ->method('setFrom')
- ->with('foo@bar.baz', 'Lorem Ipsum')
- ->willReturn($message);
-
- $message->expects($this->once())
- ->method('setSubject')
- ->with('[Mail test] Foo Bar')
- ->willReturn($message);
-
- $message->expects($this->once())
- ->method('setBody')
- ->with('Lorem Ipsum!')
- ->willReturn($message);
+ $this->setExpects($swiftMailer, 'createMessage', null, $message);
+ $this->setExpects($swiftMailer, 'send', null, 1);
+ $this->setExpects($message, 'setTo', [['to@xam.pel']], $message);
+ $this->setExpects($message, 'setFrom', ['foo@bar.baz', 'Lorem Ipsum'], $message);
+ $this->setExpects($message, 'setSubject', ['[Mail test] Foo Bar'], $message);
+ $this->setExpects($message, 'setBody', ['Lorem Ipsum!'], $message);
$mailer = new EngelsystemMailer($swiftMailer);
$mailer->setFromAddress('foo@bar.baz');
diff --git a/tests/Unit/Models/EventConfigTest.php b/tests/Unit/Models/EventConfigTest.php
index e2ab5d10..18d27007 100644
--- a/tests/Unit/Models/EventConfigTest.php
+++ b/tests/Unit/Models/EventConfigTest.php
@@ -5,7 +5,7 @@ namespace Engelsystem\Test\Unit\Models;
use Carbon\Carbon;
use Engelsystem\Models\EventConfig;
use Engelsystem\Test\Unit\HasDatabase;
-use PHPUnit\Framework\TestCase;
+use Engelsystem\Test\Unit\TestCase;
class EventConfigTest extends TestCase
{
@@ -102,7 +102,8 @@ class EventConfigTest extends TestCase
*/
protected function getEventConfig()
{
- return new class extends EventConfig {
+ return new class extends EventConfig
+ {
/**
* @param string $value
* @param string $type
@@ -122,6 +123,7 @@ class EventConfigTest extends TestCase
*/
protected function setUp(): void
{
+ parent::setUp();
$this->initDatabase();
}
}
diff --git a/tests/Unit/Models/LogEntryTest.php b/tests/Unit/Models/LogEntryTest.php
index 0a0efa3c..4b772cd0 100644
--- a/tests/Unit/Models/LogEntryTest.php
+++ b/tests/Unit/Models/LogEntryTest.php
@@ -4,7 +4,7 @@ namespace Engelsystem\Test\Unit\Models;
use Engelsystem\Models\LogEntry;
use Engelsystem\Test\Unit\HasDatabase;
-use PHPUnit\Framework\TestCase;
+use Engelsystem\Test\Unit\TestCase;
use Psr\Log\LogLevel;
class LogEntryTest extends TestCase
@@ -38,6 +38,7 @@ class LogEntryTest extends TestCase
*/
protected function setUp(): void
{
+ parent::setUp();
$this->initDatabase();
}
}
diff --git a/tests/Unit/Models/User/HasUserModelTest.php b/tests/Unit/Models/User/HasUserModelTest.php
index 58c01e1e..4f6da9ad 100644
--- a/tests/Unit/Models/User/HasUserModelTest.php
+++ b/tests/Unit/Models/User/HasUserModelTest.php
@@ -5,8 +5,8 @@ namespace Engelsystem\Test\Unit\Models;
use Engelsystem\Models\User\HasUserModel;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\Models\User\Stub\HasUserModelImplementation;
+use Engelsystem\Test\Unit\TestCase;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
-use PHPUnit\Framework\TestCase;
class HasUserModelTest extends TestCase
{
@@ -28,6 +28,7 @@ class HasUserModelTest extends TestCase
*/
protected function setUp(): void
{
+ parent::setUp();
$this->initDatabase();
}
}
diff --git a/tests/Unit/Models/User/UserTest.php b/tests/Unit/Models/User/UserTest.php
index 0e17d137..3e793832 100644
--- a/tests/Unit/Models/User/UserTest.php
+++ b/tests/Unit/Models/User/UserTest.php
@@ -10,7 +10,7 @@ use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
-use PHPUnit\Framework\TestCase;
+use Engelsystem\Test\Unit\TestCase;
class UserTest extends TestCase
{
@@ -95,6 +95,7 @@ class UserTest extends TestCase
*/
protected function setUp(): void
{
+ parent::setUp();
$this->initDatabase();
}
}
diff --git a/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php b/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php
index b6c19d14..7190c979 100644
--- a/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php
+++ b/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php
@@ -26,6 +26,7 @@ class LegacyTest extends ExtensionTest
$this->assertExtensionExists('menuUserHints', 'header_render_hints', $functions, $isSafeHtml);
$this->assertExtensionExists('menuUserSubmenu', 'make_user_submenu', $functions, $isSafeHtml);
$this->assertExtensionExists('page', [$extension, 'getPage'], $functions);
+ $this->assertExtensionExists('msg', 'msg', $functions, $isSafeHtml);
}
/**
diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php
index d09104d4..dba8c989 100644
--- a/tests/Unit/TestCase.php
+++ b/tests/Unit/TestCase.php
@@ -2,18 +2,22 @@
namespace Engelsystem\Test\Unit;
-use PHPUnit\Framework\MockObject\Matcher\InvokedRecorder;
+use Engelsystem\Application;
use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
abstract class TestCase extends PHPUnitTestCase
{
+ /** @var Application */
+ protected $app;
+
/**
* @param MockObject $object
* @param string $method
* @param array $arguments
* @param mixed $return
- * @param InvokedRecorder $times
+ * @param InvocationOrder $times
*/
protected function setExpects($object, $method, $arguments = null, $return = null, $times = null)
{
@@ -34,4 +38,12 @@ abstract class TestCase extends PHPUnitTestCase
$invocation->willReturn($return);
}
}
+
+ /**
+ * Called before each test run
+ */
+ protected function setUp(): void
+ {
+ $this->app = new Application(__DIR__ . '/../../');
+ }
}