summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/routes.php6
-rw-r--r--includes/controller/users_controller.php115
-rw-r--r--includes/model/User_model.php19
-rw-r--r--includes/pages/guest_login.php4
-rw-r--r--includes/view/User_view.php93
-rw-r--r--resources/lang/de_DE/default.po22
-rw-r--r--resources/lang/en_US/default.po18
-rw-r--r--resources/lang/pt_BR/default.po2
-rw-r--r--resources/views/emails/mail.twig8
-rw-r--r--resources/views/emails/password-reset.twig3
-rw-r--r--resources/views/macros/form.twig18
-rw-r--r--resources/views/pages/login.twig3
-rw-r--r--resources/views/pages/password/reset-form.twig18
-rw-r--r--resources/views/pages/password/reset-success.twig12
-rw-r--r--resources/views/pages/password/reset.twig32
-rw-r--r--src/Controllers/AuthController.php2
-rw-r--r--src/Controllers/PasswordResetController.php167
-rw-r--r--src/Helpers/Translation/TranslationServiceProvider.php6
-rw-r--r--src/Http/Exceptions/HttpNotFound.php23
-rw-r--r--src/Http/Response.php27
-rw-r--r--src/Http/Validation/Rules/Between.php10
-rw-r--r--src/Http/Validation/Rules/Max.php10
-rw-r--r--src/Http/Validation/Rules/Min.php10
-rw-r--r--src/Http/Validation/Rules/StringInputLength.php44
-rw-r--r--src/Mail/EngelsystemMailer.php59
-rw-r--r--src/Middleware/LegacyMiddleware.php6
-rw-r--r--src/Renderer/Twig/Extensions/Legacy.php1
-rw-r--r--tests/Unit/Controllers/AuthControllerTest.php6
-rw-r--r--tests/Unit/Controllers/Metrics/StatsTest.php4
-rw-r--r--tests/Unit/Controllers/PasswordResetControllerTest.php266
-rw-r--r--tests/Unit/HasDatabase.php8
-rw-r--r--tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php18
-rw-r--r--tests/Unit/Http/Exceptions/HttpNotFoundTest.php22
-rw-r--r--tests/Unit/Http/ResponseTest.php12
-rw-r--r--tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php3
-rw-r--r--tests/Unit/Http/Validation/Rules/BetweenTest.php28
-rw-r--r--tests/Unit/Http/Validation/Rules/MaxTest.php26
-rw-r--r--tests/Unit/Http/Validation/Rules/MinTest.php26
-rw-r--r--tests/Unit/Http/Validation/Rules/StringInputLengthTest.php37
-rw-r--r--tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php23
-rw-r--r--tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php10
-rw-r--r--tests/Unit/Http/Validation/ValidatorTest.php5
-rw-r--r--tests/Unit/Mail/EngelsystemMailerTest.php107
-rw-r--r--tests/Unit/Models/EventConfigTest.php6
-rw-r--r--tests/Unit/Models/LogEntryTest.php3
-rw-r--r--tests/Unit/Models/User/HasUserModelTest.php3
-rw-r--r--tests/Unit/Models/User/UserTest.php3
-rw-r--r--tests/Unit/Renderer/Twig/Extensions/LegacyTest.php1
-rw-r--r--tests/Unit/TestCase.php16
49 files changed, 1061 insertions, 310 deletions
diff --git a/config/routes.php b/config/routes.php
index 02fd3abd..e57d3079 100644
--- a/config/routes.php
+++ b/config/routes.php
@@ -13,6 +13,12 @@ $route->get('/login', 'AuthController@login');
$route->post('/login', 'AuthController@postLogin');
$route->get('/logout', 'AuthController@logout');
+// Password recovery
+$route->get('/password/reset', 'PasswordResetController@reset');
+$route->post('/password/reset', 'PasswordResetController@postReset');
+$route->get('/password/reset/{token:.+}', 'PasswordResetController@resetPassword');
+$route->post('/password/reset/{token:.+}', 'PasswordResetController@postResetPassword');
+
// Stats
$route->get('/metrics', 'Metrics\\Controller@metrics');
$route->get('/stats', 'Metrics\\Controller@stats');
diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php
index 892089e7..3ad2ffd9 100644
--- a/includes/controller/users_controller.php
+++ b/includes/controller/users_controller.php
@@ -1,7 +1,6 @@
<?php
use Engelsystem\Database\DB;
-use Engelsystem\Models\User\PasswordReset;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
use Engelsystem\ShiftCalendarRenderer;
@@ -312,120 +311,6 @@ function users_list_controller()
}
/**
- * Second step of password recovery: set a new password using the token link from email
- *
- * @return string
- */
-function user_password_recovery_set_new_controller()
-{
- $request = request();
- $passwordReset = PasswordReset::whereToken($request->input('token'))->first();
- if (!$passwordReset) {
- error(__('Token is not correct.'));
- redirect(page_link_to('login'));
- }
-
- if ($request->hasPostData('submit')) {
- $valid = true;
-
- if (
- $request->has('password')
- && strlen($request->postData('password')) >= config('min_password_length')
- ) {
- if ($request->postData('password') != $request->postData('password2')) {
- $valid = false;
- error(__('Your passwords don\'t match.'));
- }
- } else {
- $valid = false;
- error(__('Your password is to short (please use at least 6 characters).'));
- }
-
- if ($valid) {
- auth()->setPassword($passwordReset->user, $request->postData('password'));
- success(__('Password saved.'));
- $passwordReset->delete();
- redirect(page_link_to('login'));
- }
- }
-
- return User_password_set_view();
-}
-
-/**
- * First step of password recovery: display a form that asks for your email and send email with recovery link
- *
- * @return string
- */
-function user_password_recovery_start_controller()
-{
- $request = request();
- if ($request->hasPostData('submit')) {
- $valid = true;
-
- $user_source = null;
- if ($request->has('email') && strlen(strip_request_item('email')) > 0) {
- $email = strip_request_item('email');
- if (check_email($email)) {
- /** @var User $user_source */
- $user_source = User::whereEmail($email)->first();
- if (!$user_source) {
- $valid = false;
- error(__('E-mail address is not correct.'));
- }
- } else {
- $valid = false;
- error(__('E-mail address is not correct.'));
- }
- } else {
- $valid = false;
- error(__('Please enter your e-mail.'));
- }
-
- if ($valid) {
- $token = User_generate_password_recovery_token($user_source);
- engelsystem_email_to_user(
- $user_source,
- __('Password recovery'),
- sprintf(
- __('Please visit %s to recover your password.'),
- page_link_to('user_password_recovery', ['token' => $token])
- )
- );
- success(__('We sent an email containing your password recovery link.'));
- redirect(page_link_to('login'));
- }
- }
-
- return User_password_recovery_view();
-}
-
-/**
- * User password recovery in 2 steps.
- * (By email)
- *
- * @return string
- */
-function user_password_recovery_controller()
-{
- if (request()->has('token')) {
- return user_password_recovery_set_new_controller();
- }
-
- return user_password_recovery_start_controller();
-}
-
-/**
- * Menu title for password recovery.
- *
- * @return string
- */
-function user_password_recovery_title()
-{
- return __('Password recovery');
-}
-
-/**
* Loads a user from param user_id.
*
* @return User
diff --git a/includes/model/User_model.php b/includes/model/User_model.php
index 1994bc47..681e70aa 100644
--- a/includes/model/User_model.php
+++ b/includes/model/User_model.php
@@ -2,7 +2,6 @@
use Carbon\Carbon;
use Engelsystem\Database\DB;
-use Engelsystem\Models\User\PasswordReset;
use Engelsystem\Models\User\User;
use Engelsystem\ValidationResult;
use Illuminate\Database\Query\JoinClause;
@@ -228,24 +227,6 @@ function User_reset_api_key($user, $log = true)
}
/**
- * Generates a new password recovery token for given user.
- *
- * @param User $user
- * @return string
- */
-function User_generate_password_recovery_token($user)
-{
- $reset = PasswordReset::findOrNew($user->id);
- $reset->user_id = $user->id;
- $reset->token = md5($user->name . time() . rand());
- $reset->save();
-
- engelsystem_log('Password recovery for ' . User_Nick_render($user, true) . ' started.');
-
- return $reset->token;
-}
-
-/**
* @param User $user
* @return float
*/
diff --git a/includes/pages/guest_login.php b/includes/pages/guest_login.php
index 280743e5..170572e4 100644
--- a/includes/pages/guest_login.php
+++ b/includes/pages/guest_login.php
@@ -242,9 +242,9 @@ function guest_register()
redirect(page_link_to('register'));
}
- // If a welcome message is present, display registration success page.
+ // If a welcome message is present, display it on the next page
if ($message = $config->get('welcome_msg')) {
- return User_registration_success_view($message);
+ info((new Parsedown())->text($message));
}
redirect(page_link_to('/'));
diff --git a/includes/view/User_view.php b/includes/view/User_view.php
index bfe7e02c..95ecb626 100644
--- a/includes/view/User_view.php
+++ b/includes/view/User_view.php
@@ -108,46 +108,6 @@ function User_settings_view(
}
/**
- * Displays the welcome message to the user and shows a login form.
- *
- * @param string $event_welcome_message
- * @return string
- */
-function User_registration_success_view($event_welcome_message)
-{
- $parsedown = new Parsedown();
- $event_welcome_message = $parsedown->text($event_welcome_message);
-
- return page_with_title(__('Registration successful'), [
- msg(),
- div('row', [
- div('col-md-4', [
- $event_welcome_message
- ]),
- div('col-md-4', [
- '<h2>' . __('Login') . '</h2>',
- form([
- form_text('login', __('Nick'), ''),
- form_password('password', __('Password')),
- form_submit('submit', __('Login')),
- buttons([
- button(page_link_to('user_password_recovery'), __('I forgot my password'))
- ]),
- info(__('Please note: You have to activate cookies!'), true)
- ], page_link_to('login'))
- ]),
- div('col-md-4', [
- '<h2>' . __('What can I do?') . '</h2>',
- '<p>' . __('Please read about the jobs you can do to help us.') . '</p>',
- buttons([
- button(page_link_to('angeltypes', ['action' => 'about']), __('Teams/Job description') . ' &raquo;')
- ])
- ])
- ])
- ]);
-}
-
-/**
* Gui for deleting user with password field.
*
* @param User $user
@@ -255,13 +215,13 @@ function Users_view(
];
$user_table_headers = [
- 'name' => Users_table_header_link('name', __('Nick'), $order_by)
+ 'name' => Users_table_header_link('name', __('Nick'), $order_by)
];
- if(config('enable_user_name')) {
+ if (config('enable_user_name')) {
$user_table_headers['first_name'] = Users_table_header_link('first_name', __('Prename'), $order_by);
$user_table_headers['last_name'] = Users_table_header_link('last_name', __('Name'), $order_by);
}
- if(config('enable_dect')) {
+ if (config('enable_dect')) {
$user_table_headers['dect'] = Users_table_header_link('dect', __('DECT'), $order_by);
}
$user_table_headers['arrived'] = Users_table_header_link('arrived', __('Arrived'), $order_by);
@@ -271,8 +231,16 @@ function Users_view(
$user_table_headers['force_active'] = Users_table_header_link('force_active', __('Forced'), $order_by);
$user_table_headers['got_shirt'] = Users_table_header_link('got_shirt', __('T-Shirt'), $order_by);
$user_table_headers['shirt_size'] = Users_table_header_link('shirt_size', __('Size'), $order_by);
- $user_table_headers['arrival_date'] = Users_table_header_link('planned_arrival_date', __('Planned arrival'), $order_by);
- $user_table_headers['departure_date'] = Users_table_header_link('planned_departure_date', __('Planned departure'), $order_by);
+ $user_table_headers['arrival_date'] = Users_table_header_link(
+ 'planned_arrival_date',
+ __('Planned arrival'),
+ $order_by
+ );
+ $user_table_headers['departure_date'] = Users_table_header_link(
+ 'planned_departure_date',
+ __('Planned departure'),
+ $order_by
+ );
$user_table_headers['last_login_at'] = Users_table_header_link('last_login_at', __('Last login'), $order_by);
$user_table_headers['actions'] = '';
@@ -792,41 +760,6 @@ function User_view_state_admin($freeloader, $user_source)
}
/**
- * View for password recovery step 1: E-Mail
- *
- * @return string
- */
-function User_password_recovery_view()
-{
- return page_with_title(user_password_recovery_title(), [
- msg(),
- __('We will send you an e-mail with a password recovery link. Please use the email address you used for registration.'),
- form([
- form_text('email', __('E-Mail'), ''),
- form_submit('submit', __('Recover'))
- ])
- ]);
-}
-
-/**
- * View for password recovery step 2: New password
- *
- * @return string
- */
-function User_password_set_view()
-{
- return page_with_title(user_password_recovery_title(), [
- msg(),
- __('Please enter a new password.'),
- form([
- form_password('password', __('Password')),
- form_password('password2', __('Confirm password')),
- form_submit('submit', __('Save'))
- ])
- ]);
-}
-
-/**
* @param array[] $user_angeltypes
* @return string
*/
diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po
index 091e1114..d6dc38bb 100644
--- a/resources/lang/de_DE/default.po
+++ b/resources/lang/de_DE/default.po
@@ -619,9 +619,9 @@ msgid "Please visit %s to recover your password."
msgstr "Bitte besuche %s, um Dein Passwort zurückzusetzen"
#: includes/controller/users_controller.php:394
-msgid "We sent an email containing your password recovery link."
+msgid "We sent you an email containing your password recovery link."
msgstr ""
-"Wir haben eine eMail mit einem Link zum Passwort-zurücksetzen geschickt."
+"Wir haben dir eine eMail mit einem Link zum Passwort-zurücksetzen geschickt."
#: includes/helper/email_helper.php:41
#, php-format
@@ -2769,3 +2769,21 @@ msgstr "Bitte gib ein Passwort an."
msgid "validation.login.required"
msgstr "Bitte gib einen Loginnamen an."
+
+msgid "form.submit"
+msgstr "Absenden"
+
+msgid "validation.email.required"
+msgstr "Bitte gib eine E-Mail-Adresse an."
+
+msgid "validation.email.email"
+msgstr "Die E-Mail-Adresse ist nicht gültig."
+
+msgid "validation.password.min"
+msgstr "Dein angegebenes Passwort ist zu kurz."
+
+msgid "validation.password.confirmed"
+msgstr "Deine Passwörter stimmen nicht überein."
+
+msgid "validation.password_confirmation.required"
+msgstr "Du musst dein Passwort bestätigen."
diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po
index 1ff16c83..a2d56fd1 100644
--- a/resources/lang/en_US/default.po
+++ b/resources/lang/en_US/default.po
@@ -30,3 +30,21 @@ msgstr "The password is required."
msgid "validation.login.required"
msgstr "The login name is required."
+
+msgid "form.submit"
+msgstr "Submit"
+
+msgid "validation.email.required"
+msgstr "The email address is required."
+
+msgid "validation.email.email"
+msgstr "This email address is not valid."
+
+msgid "validation.password.min"
+msgstr "Your password is too short."
+
+msgid "validation.password.confirmed"
+msgstr "Your passwords are not equal."
+
+msgid "validation.password_confirmation.required"
+msgstr "You have to confirm your password."
diff --git a/resources/lang/pt_BR/default.po b/resources/lang/pt_BR/default.po
index b9bf420d..e5e5371e 100644
--- a/resources/lang/pt_BR/default.po
+++ b/resources/lang/pt_BR/default.po
@@ -551,7 +551,7 @@ msgid "Please visit %s to recover your password."
msgstr "Por favor visite %s para recuperar sua senha"
#: includes/controller/users_controller.php:271
-msgid "We sent an email containing your password recovery link."
+msgid "We sent you an email containing your password recovery link."
msgstr "Nós enviamos um email com o link para recuperação da sua senha."
#: includes/helper/email_helper.php:12
diff --git a/resources/views/emails/mail.twig b/resources/views/emails/mail.twig
index ec70f594..e0ad7b64 100644
--- a/resources/views/emails/mail.twig
+++ b/resources/views/emails/mail.twig
@@ -1,6 +1,6 @@
-{{ __('Hi %s,', [username]) }}
+{% block title %}{{ __('Hi %s,', [username]) }}{% endblock %}
-{{ __('here is a message for you from the %s:', [config('app_name')]) }}
-{{ message|raw }}
+{% block introduction %}{{ __('here is a message for you from the %s:', [config('app_name')]) }}{% endblock %}
+{% block message %}{{ message|raw }}{% endblock %}
-{{ __('This email is autogenerated and has not been signed. You got this email because you are registered in the %s.', [config('app_name')]) }}
+{% block footer %}{{ __('This email is autogenerated and has not been signed. You got this email because you are registered in the %s.', [config('app_name')]) }}{% endblock %}
diff --git a/resources/views/emails/password-reset.twig b/resources/views/emails/password-reset.twig
new file mode 100644
index 00000000..30b613b4
--- /dev/null
+++ b/resources/views/emails/password-reset.twig
@@ -0,0 +1,3 @@
+{% extends "emails/mail.twig" %}
+
+{% block message %}{{ __('Please visit %s to recover your password.', [url('/password/reset/') ~ reset.token]) }}{% endblock %}
diff --git a/resources/views/macros/form.twig b/resources/views/macros/form.twig
new file mode 100644
index 00000000..5d41b085
--- /dev/null
+++ b/resources/views/macros/form.twig
@@ -0,0 +1,18 @@
+{% macro input(name, label, type, required) %}
+ <div class="form-group">
+ {% if label %}
+ <label for="{{ name }}">{{ label }}</label>
+ {% endif %}
+ <input type="{{ type|default('text') }}" class="form-control" id="{{ name }}" name="{{ name }}"
+ {%- if required|default(false) %} required="required"{% endif -%}
+ >
+ </div>
+{% endmacro %}
+
+{% macro hidden(name, value) %}
+ <input type="hidden" id="{{ name }}" name="{{ name }}" value="{{ value }}">
+{% endmacro %}
+
+{% macro submit(label) %}
+ <button type="submit" class="btn btn-default">{{ label|default(__('form.submit')) }}</button>
+{% endmacro %}
diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig
index 88326429..34dbd63f 100644
--- a/resources/views/pages/login.twig
+++ b/resources/views/pages/login.twig
@@ -32,6 +32,7 @@
<div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
<div class="panel panel-primary first">
<div class="panel-body">
+ {{ msg() }}
{% for message in errors|default([]) %}
{{ m.alert(__(message), 'danger') }}
{% endfor %}
@@ -61,7 +62,7 @@
</div>
<div class="text-center">
- <a href="{{ url('user-password-recovery') }}" class="">
+ <a href="{{ url('/password/reset') }}" class="">
{{ __('I forgot my password') }}
</a>
</div>
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 %}
+ <div class="col-md-8">
+ <form action="" enctype="multipart/form-data" method="post">
+ {{ csrf() }}
+
+ {{ f.input('password', __('Password'), 'password', true) }}
+ {{ f.input('password_confirmation', __('Confirm password'), 'password', true) }}
+
+ <div class="form-group">
+ {{ f.submit(__('Save')) }}
+ </div>
+ </form>
+ </div>
+{% 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 %}
+ <div class="col-md-12">
+ {% 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 %}
+ </div>
+{% 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 %}
+ <div class="container">
+ <h1>{{ __('Password recovery') }}</h1>
+
+ {% for message in errors|default([]) %}
+ {{ m.alert(__(message), 'danger') }}
+ {% endfor %}
+
+ <div class="row">
+ {% block row_content %}
+ <div class="col-md-8">
+ <form action="" enctype="multipart/form-data" method="post">
+ {{ csrf() }}
+
+ {{ __('We will send you an e-mail with a password recovery link. Please use the email address you used for registration.') }}
+ {{ f.input('email', __('E-Mail'), 'email', true) }}
+
+ <div class="form-group">
+ {{ f.submit(__('Recover')) }}
+ </div>
+ </form>
+ </div>
+ {% endblock %}
+ </div>
+ </div>
+{% 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 @@
+<?php
+
+namespace Engelsystem\Controllers;
+
+use Engelsystem\Http\Exceptions\HttpNotFound;
+use Engelsystem\Http\Request;
+use Engelsystem\Http\Response;
+use Engelsystem\Mail\EngelsystemMailer;
+use Engelsystem\Models\User\PasswordReset;
+use Engelsystem\Models\User\User;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Collection;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+
+class PasswordResetController extends BaseController
+{
+ /** @var LoggerInterface */
+ protected $log;
+
+ /** @var EngelsystemMailer */
+ protected $mail;
+
+ /** @var Response */
+ protected $response;
+
+ /** @var SessionInterface */
+ protected $session;
+
+ /** @var array */
+ protected $permissions = [
+ 'reset' => '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 @@
+<?php
+
+namespace Engelsystem\Http\Exceptions;
+
+use Throwable;
+
+class HttpNotFound extends HttpException
+{
+ /**
+ * @param string $message
+ * @param array $headers
+ * @param int $code
+ * @param Throwable|null $previous
+ */
+ public function __construct(
+ string $message = '',
+ array $headers = [],
+ int $code = 0,
+ Throwable $previous = null
+ ) {
+ parent::__construct(404, $message, $headers, $code, $previous);
+ }
+}
diff --git a/src/Http/Response.php b/src/Http/Response.php
index 1a7c8209..a6b4ab74 100644
--- a/src/Http/Response.php
+++ b/src/Http/Response.php
@@ -3,6 +3,7 @@
namespace Engelsystem\Http;
use Engelsystem\Renderer\Renderer;
+use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
@@ -11,21 +12,21 @@ class Response extends SymfonyResponse implements ResponseInterface
use MessageTrait;
/** @var Renderer */
- protected $view;
+ protected $renderer;
/**
* @param string $content
* @param int $status
* @param array $headers
- * @param Renderer $view
+ * @param Renderer $renderer
*/
public function __construct(
$content = '',
int $status = 200,
array $headers = [],
- Renderer $view = null
+ Renderer $renderer = null
) {
- $this->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 @@
+<?php
+
+namespace Engelsystem\Http\Validation\Rules;
+
+use Respect\Validation\Rules\Between as RespectBetween;
+
+class Between extends RespectBetween
+{
+ use StringInputLength;
+}
diff --git a/src/Http/Validation/Rules/Max.php b/src/Http/Validation/Rules/Max.php
new file mode 100644
index 00000000..b1b2cfa3
--- /dev/null
+++ b/src/Http/Validation/Rules/Max.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Engelsystem\Http\Validation\Rules;
+
+use Respect\Validation\Rules\Max as RespectMax;
+
+class Max extends RespectMax
+{
+ use StringInputLength;
+}
diff --git a/src/Http/Validation/Rules/Min.php b/src/Http/Validation/Rules/Min.php
new file mode 100644
index 00000000..ab8d4e1a
--- /dev/null
+++ b/src/Http/Validation/Rules/Min.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Engelsystem\Http\Validation\Rules;
+
+use Respect\Validation\Rules\Min as RespectMin;
+
+class Min extends RespectMin
+{
+ use StringInputLength;
+}
diff --git a/src/Http/Validation/Rules/StringInputLength.php b/src/Http/Validation/Rules/StringInputLength.php
new file mode 100644
index 00000000..7b5c248b
--- /dev/null
+++ b/src/Http/Validation/Rules/StringInputLength.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Engelsystem\Http\Validation\Rules;
+
+use DateTime;
+use Illuminate\Support\Str;
+use Throwable;
+
+trait StringInputLength
+{
+ /**
+ * Use the input length of a string
+ *
+ * @param mixed $input
+ * @return bool
+ */
+ public function validate($input): bool
+ {
+ if (
+ is_string($input)
+ && !is_numeric($input)
+ && !$this->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Controllers;
+
+use Engelsystem\Config\Config;
+use Engelsystem\Controllers\PasswordResetController;
+use Engelsystem\Helpers\Authenticator;
+use Engelsystem\Http\Exceptions\HttpNotFound;
+use Engelsystem\Http\Exceptions\ValidationException;
+use Engelsystem\Http\Request;
+use Engelsystem\Http\Response;
+use Engelsystem\Http\Validation\Validator;
+use Engelsystem\Mail\EngelsystemMailer;
+use Engelsystem\Models\User\PasswordReset;
+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 Psr\Log\Test\TestLogger;
+use Symfony\Component\HttpFoundation\Session\Session;
+use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
+
+class PasswordResetControllerTest extends TestCase
+{
+ use HasDatabase;
+
+ /** @var array */
+ protected $args = [];
+
+ /**
+ * @covers \Engelsystem\Controllers\PasswordResetController::reset
+ * @covers \Engelsystem\Controllers\PasswordResetController::__construct
+ */
+ public function testReset(): void
+ {
+ $controller = $this->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Exceptions;
+
+use Engelsystem\Http\Exceptions\HttpNotFound;
+use PHPUnit\Framework\TestCase;
+
+class HttpNotFoundTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Exceptions\HttpNotFound::__construct
+ */
+ public function testConstruct()
+ {
+ $exception = new HttpNotFound();
+ $this->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules;
+
+use Engelsystem\Http\Validation\Rules\Between;
+use Engelsystem\Test\Unit\TestCase;
+
+class BetweenTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\Rules\Between
+ */
+ public function testValidate()
+ {
+ $rule = new Between(3, 10);
+ $this->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules;
+
+use Engelsystem\Http\Validation\Rules\Max;
+use Engelsystem\Test\Unit\TestCase;
+
+class MaxTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\Rules\Max
+ */
+ public function testValidate()
+ {
+ $rule = new Max(3);
+ $this->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules;
+
+use Engelsystem\Http\Validation\Rules\Min;
+use Engelsystem\Test\Unit\TestCase;
+
+class MinTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\Rules\Min
+ */
+ public function testValidate()
+ {
+ $rule = new Min(3);
+ $this->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules;
+
+use Engelsystem\Test\Unit\Http\Validation\Rules\Stub\UsesStringInputLength;
+use Engelsystem\Test\Unit\TestCase;
+
+class StringInputLengthTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\Rules\StringInputLength::validate
+ * @covers \Engelsystem\Http\Validation\Rules\StringInputLength::isDateTime
+ * @dataProvider validateProvider
+ * @param mixed $input
+ * @param mixed $expectedInput
+ */
+ public function testValidate($input, $expectedInput)
+ {
+ $rule = new UsesStringInputLength();
+ $rule->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules\Stub;
+
+class ParentClassImplementation
+{
+ /** @var bool */
+ public $validateResult = true;
+
+ /** @var mixed */
+ public $lastInput;
+
+ /**
+ * @param mixed $input
+ * @return bool
+ */
+ public function validate($input): bool
+ {
+ $this->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 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules\Stub;
+
+use Engelsystem\Http\Validation\Rules\StringInputLength;
+
+class UsesStringInputLength extends ParentClassImplementation
+{
+ use StringInputLength;
+}
diff --git a/tests/Unit/Http/Validation/ValidatorTest.php b/tests/Unit/Http/Validation/ValidatorTest.php
index 450e5d4e..124673df 100644
--- a/tests/Unit/Http/Validation/ValidatorTest.php
+++ b/tests/Unit/Http/Validation/ValidatorTest.php
@@ -50,9 +50,10 @@ class ValidatorTest extends TestCase
));
$this->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,22 +31,70 @@ 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
* @covers \Engelsystem\Mail\EngelsystemMailer::setSubjectPrefix
@@ -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__ . '/../../');
+ }
}