Password recovery rebuild, correctly translated Mails and some minor fixes #658

This commit is contained in:
msquare 2019-10-13 13:43:08 +02:00
commit c0e97bfe75
49 changed files with 1061 additions and 310 deletions

View File

@ -13,6 +13,12 @@ $route->get('/login', 'AuthController@login');
$route->post('/login', 'AuthController@postLogin'); $route->post('/login', 'AuthController@postLogin');
$route->get('/logout', 'AuthController@logout'); $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 // Stats
$route->get('/metrics', 'Metrics\\Controller@metrics'); $route->get('/metrics', 'Metrics\\Controller@metrics');
$route->get('/stats', 'Metrics\\Controller@stats'); $route->get('/stats', 'Metrics\\Controller@stats');

View File

@ -1,7 +1,6 @@
<?php <?php
use Engelsystem\Database\DB; use Engelsystem\Database\DB;
use Engelsystem\Models\User\PasswordReset;
use Engelsystem\Models\User\State; use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
use Engelsystem\ShiftCalendarRenderer; use Engelsystem\ShiftCalendarRenderer;
@ -311,120 +310,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. * Loads a user from param user_id.
* *

View File

@ -2,7 +2,6 @@
use Carbon\Carbon; use Carbon\Carbon;
use Engelsystem\Database\DB; use Engelsystem\Database\DB;
use Engelsystem\Models\User\PasswordReset;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
use Engelsystem\ValidationResult; use Engelsystem\ValidationResult;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
@ -227,24 +226,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 * @param User $user
* @return float * @return float

View File

@ -242,9 +242,9 @@ function guest_register()
redirect(page_link_to('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')) { if ($message = $config->get('welcome_msg')) {
return User_registration_success_view($message); info((new Parsedown())->text($message));
} }
redirect(page_link_to('/')); redirect(page_link_to('/'));

View File

@ -107,46 +107,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. * Gui for deleting user with password field.
* *
@ -255,13 +215,13 @@ function Users_view(
]; ];
$user_table_headers = [ $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['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); $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['dect'] = Users_table_header_link('dect', __('DECT'), $order_by);
} }
$user_table_headers['arrived'] = Users_table_header_link('arrived', __('Arrived'), $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['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['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['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['arrival_date'] = Users_table_header_link(
$user_table_headers['departure_date'] = Users_table_header_link('planned_departure_date', __('Planned departure'), $order_by); '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['last_login_at'] = Users_table_header_link('last_login_at', __('Last login'), $order_by);
$user_table_headers['actions'] = ''; $user_table_headers['actions'] = '';
@ -791,41 +759,6 @@ function User_view_state_admin($freeloader, $user_source)
return $state; return $state;
} }
/**
* 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 * @param array[] $user_angeltypes
* @return string * @return string

View File

@ -619,9 +619,9 @@ msgid "Please visit %s to recover your password."
msgstr "Bitte besuche %s, um Dein Passwort zurückzusetzen" msgstr "Bitte besuche %s, um Dein Passwort zurückzusetzen"
#: includes/controller/users_controller.php:394 #: 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 "" 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 #: includes/helper/email_helper.php:41
#, php-format #, php-format
@ -2769,3 +2769,21 @@ msgstr "Bitte gib ein Passwort an."
msgid "validation.login.required" msgid "validation.login.required"
msgstr "Bitte gib einen Loginnamen an." 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."

View File

@ -30,3 +30,21 @@ msgstr "The password is required."
msgid "validation.login.required" msgid "validation.login.required"
msgstr "The login name is 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."

View File

@ -551,7 +551,7 @@ msgid "Please visit %s to recover your password."
msgstr "Por favor visite %s para recuperar sua senha" msgstr "Por favor visite %s para recuperar sua senha"
#: includes/controller/users_controller.php:271 #: 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." msgstr "Nós enviamos um email com o link para recuperação da sua senha."
#: includes/helper/email_helper.php:12 #: includes/helper/email_helper.php:12

View File

@ -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')]) }} {% block introduction %}{{ __('here is a message for you from the %s:', [config('app_name')]) }}{% endblock %}
{{ message|raw }} {% 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 %}

View File

@ -0,0 +1,3 @@
{% extends "emails/mail.twig" %}
{% block message %}{{ __('Please visit %s to recover your password.', [url('/password/reset/') ~ reset.token]) }}{% endblock %}

View File

@ -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 %}

View File

@ -32,6 +32,7 @@
<div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4"> <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 panel-primary first">
<div class="panel-body"> <div class="panel-body">
{{ msg() }}
{% for message in errors|default([]) %} {% for message in errors|default([]) %}
{{ m.alert(__(message), 'danger') }} {{ m.alert(__(message), 'danger') }}
{% endfor %} {% endfor %}
@ -61,7 +62,7 @@
</div> </div>
<div class="text-center"> <div class="text-center">
<a href="{{ url('user-password-recovery') }}" class=""> <a href="{{ url('/password/reset') }}" class="">
{{ __('I forgot my password') }} {{ __('I forgot my password') }}
</a> </a>
</div> </div>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -88,7 +88,7 @@ class AuthController extends BaseController
$user = $this->auth->authenticate($data['login'], $data['password']); $user = $this->auth->authenticate($data['login'], $data['password']);
if (!$user instanceof User) { 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(); return $this->showLogin();
} }

View File

@ -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;
}
}

View File

@ -41,8 +41,10 @@ class TranslationServiceProvider extends ServiceProvider
'localeChangeCallback' => [$this, 'setLocale'], 'localeChangeCallback' => [$this, 'setLocale'],
] ]
); );
$this->app->instance(Translator::class, $translator); $this->app->singleton(Translator::class, function () use ($translator) {
$this->app->instance('translator', $translator); return $translator;
});
$this->app->alias(Translator::class, 'translator');
} }
/** /**

View File

@ -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);
}
}

View File

@ -3,6 +3,7 @@
namespace Engelsystem\Http; namespace Engelsystem\Http;
use Engelsystem\Renderer\Renderer; use Engelsystem\Renderer\Renderer;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
@ -11,21 +12,21 @@ class Response extends SymfonyResponse implements ResponseInterface
use MessageTrait; use MessageTrait;
/** @var Renderer */ /** @var Renderer */
protected $view; protected $renderer;
/** /**
* @param string $content * @param string $content
* @param int $status * @param int $status
* @param array $headers * @param array $headers
* @param Renderer $view * @param Renderer $renderer
*/ */
public function __construct( public function __construct(
$content = '', $content = '',
int $status = 200, int $status = 200,
array $headers = [], array $headers = [],
Renderer $view = null Renderer $renderer = null
) { ) {
$this->view = $view; $this->renderer = $renderer;
parent::__construct($content, $status, $headers); parent::__construct($content, $status, $headers);
} }
@ -47,7 +48,7 @@ class Response extends SymfonyResponse implements ResponseInterface
* provided status code; if none is provided, implementations MAY * provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification. * use the defaults as suggested in the HTTP specification.
* @return static * @return static
* @throws \InvalidArgumentException For invalid status code arguments. * @throws InvalidArgumentException For invalid status code arguments.
*/ */
public function withStatus($code, $reasonPhrase = '') public function withStatus($code, $reasonPhrase = '')
{ {
@ -107,12 +108,12 @@ class Response extends SymfonyResponse implements ResponseInterface
*/ */
public function withView($view, $data = [], $status = 200, $headers = []) public function withView($view, $data = [], $status = 200, $headers = [])
{ {
if (!$this->view instanceof Renderer) { if (!$this->renderer instanceof Renderer) {
throw new \InvalidArgumentException('Renderer not defined'); throw new InvalidArgumentException('Renderer not defined');
} }
$new = clone $this; $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)); $new->setStatusCode($status, ($status == $this->getStatusCode() ? $this->statusText : null));
foreach ($headers as $key => $values) { foreach ($headers as $key => $values) {
@ -144,4 +145,14 @@ class Response extends SymfonyResponse implements ResponseInterface
return $response; return $response;
} }
/**
* Set the renderer to use
*
* @param Renderer $renderer
*/
public function setRenderer(Renderer $renderer)
{
$this->renderer = $renderer;
}
} }

View File

@ -0,0 +1,10 @@
<?php
namespace Engelsystem\Http\Validation\Rules;
use Respect\Validation\Rules\Between as RespectBetween;
class Between extends RespectBetween
{
use StringInputLength;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Engelsystem\Http\Validation\Rules;
use Respect\Validation\Rules\Max as RespectMax;
class Max extends RespectMax
{
use StringInputLength;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Engelsystem\Http\Validation\Rules;
use Respect\Validation\Rules\Min as RespectMin;
class Min extends RespectMin
{
use StringInputLength;
}

View File

@ -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;
}
}

View File

@ -2,6 +2,8 @@
namespace Engelsystem\Mail; namespace Engelsystem\Mail;
use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Models\User\User;
use Engelsystem\Renderer\Renderer; use Engelsystem\Renderer\Renderer;
use Swift_Mailer as SwiftMailer; use Swift_Mailer as SwiftMailer;
@ -10,30 +12,75 @@ class EngelsystemMailer extends Mailer
/** @var Renderer|null */ /** @var Renderer|null */
protected $view; protected $view;
/** @var Translator|null */
protected $translation;
/** @var string */ /** @var string */
protected $subjectPrefix = null; protected $subjectPrefix = null;
/** /**
* @param SwiftMailer $mailer * @param SwiftMailer $mailer
* @param Renderer $view * @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); parent::__construct($mailer);
$this->translation = $translation;
$this->view = $view; $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 * Send a template
* *
* @param string $to * @param string|string[] $to
* @param string $subject * @param string $subject
* @param string $template * @param string $template
* @param array $data * @param array $data
* @return int * @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); $body = $this->view->render($template, $data);

View File

@ -26,7 +26,6 @@ class LegacyMiddleware implements MiddlewareInterface
'shifts_json_export', 'shifts_json_export',
'users', 'users',
'user_driver_licenses', 'user_driver_licenses',
'user_password_recovery',
'user_worklog', 'user_worklog',
]; ];
@ -112,11 +111,6 @@ class LegacyMiddleware implements MiddlewareInterface
case 'shifts_json_export': case 'shifts_json_export':
require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php');
shifts_json_export_controller(); 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': case 'public_dashboard':
return public_dashboard_controller(); return public_dashboard_controller();
case 'angeltypes': case 'angeltypes':

View File

@ -32,6 +32,7 @@ class Legacy extends TwigExtension
new TwigFunction('menuUserHints', 'header_render_hints', $isSafeHtml), new TwigFunction('menuUserHints', 'header_render_hints', $isSafeHtml),
new TwigFunction('menuUserSubmenu', 'make_user_submenu', $isSafeHtml), new TwigFunction('menuUserSubmenu', 'make_user_submenu', $isSafeHtml),
new TwigFunction('page', [$this, 'getPage']), new TwigFunction('page', [$this, 'getPage']),
new TwigFunction('msg', 'msg', $isSafeHtml),
]; ];
} }

View File

@ -12,9 +12,8 @@ use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\User\Settings; use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\HasDatabase;
use Illuminate\Support\Collection; use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
@ -66,6 +65,7 @@ class AuthControllerTest extends TestCase
$session = new Session(new MockArraySessionStorage()); $session = new Session(new MockArraySessionStorage());
/** @var Validator|MockObject $validator */ /** @var Validator|MockObject $validator */
$validator = new Validator(); $validator = new Validator();
$session->set('errors', [['bar' => 'some.bar.error']]);
$user = new User([ $user = new User([
'name' => 'foo', 'name' => 'foo',
@ -89,7 +89,7 @@ class AuthControllerTest extends TestCase
$response->expects($this->once()) $response->expects($this->once())
->method('withView') ->method('withView')
->with('pages/login', ['errors' => Collection::make(['auth.not-found'])]) ->with('pages/login', ['errors' => collect(['some.bar.error', 'auth.not-found'])])
->willReturn($response); ->willReturn($response);
$response->expects($this->once()) $response->expects($this->once())
->method('redirectTo') ->method('redirectTo')

View File

@ -155,8 +155,8 @@ class StatsTest extends TestCase
$this->initDatabase(); $this->initDatabase();
$this->addUsers(); $this->addUsers();
(new PasswordReset(['use_id' => 1, 'token' => 'loremIpsum123']))->save(); (new PasswordReset(['user_id' => 1, 'token' => 'loremIpsum123']))->save();
(new PasswordReset(['use_id' => 3, 'token' => '5omeR4nd0mTok3N']))->save(); (new PasswordReset(['user_id' => 3, 'token' => '5omeR4nd0mTok3N']))->save();
$stats = new Stats($this->database); $stats = new Stats($this->database);
$this->assertEquals(2, $stats->passwordResets()); $this->assertEquals(2, $stats->passwordResets());

View File

@ -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;
}
}

View File

@ -2,7 +2,6 @@
namespace Engelsystem\Test\Unit; namespace Engelsystem\Test\Unit;
use Engelsystem\Application;
use Engelsystem\Database\Database; use Engelsystem\Database\Database;
use Engelsystem\Database\Migration\Migrate; use Engelsystem\Database\Migration\Migrate;
use Engelsystem\Database\Migration\MigrationServiceProvider; use Engelsystem\Database\Migration\MigrationServiceProvider;
@ -27,12 +26,11 @@ trait HasDatabase
$connection->getPdo()->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $connection->getPdo()->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->database = new Database($connection); $this->database = new Database($connection);
$app = new Application(); $this->app->instance(Database::class, $this->database);
$app->instance(Database::class, $this->database); $this->app->register(MigrationServiceProvider::class);
$app->register(MigrationServiceProvider::class);
/** @var Migrate $migration */ /** @var Migrate $migration */
$migration = $app->get('db.migration'); $migration = $this->app->get('db.migration');
$migration->initMigration(); $migration->initMigration();
$this->database $this->database

View File

@ -21,7 +21,7 @@ class TranslationServiceProviderTest extends ServiceProviderTest
$locales = ['fo_OO' => 'Foo', 'fo_OO.BAR' => 'Foo (Bar)', 'te_ST.WTF-9' => 'WTF\'s Testing?']; $locales = ['fo_OO' => 'Foo', 'fo_OO.BAR' => 'Foo (Bar)', 'te_ST.WTF-9' => 'WTF\'s Testing?'];
$config = new Config(['locales' => $locales, 'default_locale' => $defaultLocale]); $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 */ /** @var Session|MockObject $session */
$session = $this->createMock(Session::class); $session = $this->createMock(Session::class);
/** @var Translator|MockObject $translator */ /** @var Translator|MockObject $translator */
@ -60,12 +60,16 @@ class TranslationServiceProviderTest extends ServiceProviderTest
) )
->willReturn($translator); ->willReturn($translator);
$app->expects($this->exactly(2)) $app->expects($this->once())
->method('instance') ->method('singleton')
->withConsecutive( ->willReturnCallback(function (string $abstract, callable $callback) use ($translator) {
[Translator::class, $translator], $this->assertEquals(Translator::class, $abstract);
['translator', $translator] $this->assertEquals($translator, $callback());
); });
$app->expects($this->once())
->method('alias')
->with(Translator::class, 'translator');
$serviceProvider->register(); $serviceProvider->register();
} }

View File

@ -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());
}
}

View File

@ -55,6 +55,7 @@ class ResponseTest extends TestCase
/** /**
* @covers \Engelsystem\Http\Response::withView * @covers \Engelsystem\Http\Response::withView
* @covers \Engelsystem\Http\Response::setRenderer
*/ */
public function testWithView() public function testWithView()
{ {
@ -73,6 +74,17 @@ class ResponseTest extends TestCase
$this->assertEquals('Foo ipsum!', $newResponse->getContent()); $this->assertEquals('Foo ipsum!', $newResponse->getContent());
$this->assertEquals(505, $newResponse->getStatusCode()); $this->assertEquals(505, $newResponse->getStatusCode());
$this->assertArraySubset(['test' => ['er']], $newResponse->getHeaders()); $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());
} }
/** /**

View File

@ -4,7 +4,7 @@ namespace Engelsystem\Test\Unit\Http\SessionHandlers;
use Engelsystem\Http\SessionHandlers\DatabaseHandler; use Engelsystem\Http\SessionHandlers\DatabaseHandler;
use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\HasDatabase;
use PHPUnit\Framework\TestCase; use Engelsystem\Test\Unit\TestCase;
class DatabaseHandlerTest extends TestCase class DatabaseHandlerTest extends TestCase
{ {
@ -90,6 +90,7 @@ class DatabaseHandlerTest extends TestCase
*/ */
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp();
$this->initDatabase(); $this->initDatabase();
} }
} }

View File

@ -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'));
}
}

View File

@ -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'));
}
}

View File

@ -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'));
}
}

View File

@ -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'],
];
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -50,9 +50,10 @@ class ValidatorTest extends TestCase
)); ));
$this->assertFalse($val->validate( $this->assertFalse($val->validate(
['lorem' => 2], ['lorem' => 'OMG'],
['lorem' => 'required|min:3|max:10'] ['lorem' => 'required|min:4|max:10']
)); ));
$this->assertEquals(['lorem' => ['validation.lorem.min']], $val->getErrors());
$this->assertFalse($val->validate( $this->assertFalse($val->validate(
['lorem' => 42], ['lorem' => 42],
['lorem' => 'required|min:3|max:10'] ['lorem' => 'required|min:3|max:10']

View File

@ -2,15 +2,22 @@
namespace Engelsystem\Test\Unit\Mail; namespace Engelsystem\Test\Unit\Mail;
use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Mail\EngelsystemMailer; 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\Renderer\Renderer;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Swift_Mailer as SwiftMailer; use Swift_Mailer as SwiftMailer;
use Swift_Message as SwiftMessage; use Swift_Message as SwiftMessage;
class EngelsystemMailerTest extends TestCase class EngelsystemMailerTest extends TestCase
{ {
use HasDatabase;
/** /**
* @covers \Engelsystem\Mail\EngelsystemMailer::__construct * @covers \Engelsystem\Mail\EngelsystemMailer::__construct
* @covers \Engelsystem\Mail\EngelsystemMailer::sendView * @covers \Engelsystem\Mail\EngelsystemMailer::sendView
@ -24,21 +31,69 @@ class EngelsystemMailerTest extends TestCase
/** @var EngelsystemMailer|MockObject $mailer */ /** @var EngelsystemMailer|MockObject $mailer */
$mailer = $this->getMockBuilder(EngelsystemMailer::class) $mailer = $this->getMockBuilder(EngelsystemMailer::class)
->setConstructorArgs(['mailer' => $swiftMailer, 'view' => $view]) ->setConstructorArgs(['mailer' => $swiftMailer, 'view' => $view])
->setMethods(['send']) ->onlyMethods(['send'])
->getMock(); ->getMock();
$mailer->expects($this->once()) $this->setExpects($mailer, 'send', ['foo@bar.baz', 'Lorem dolor', 'Rendered Stuff!'], 1);
->method('send') $this->setExpects($view, 'render', ['test/template.tpl', ['dev' => true]], 'Rendered Stuff!');
->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!');
$return = $mailer->sendView('foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]); $return = $mailer->sendView('foo@bar.baz', 'Lorem dolor', 'test/template.tpl', ['dev' => true]);
$this->equalTo(1, $return); $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::getSubjectPrefix
* @covers \Engelsystem\Mail\EngelsystemMailer::send * @covers \Engelsystem\Mail\EngelsystemMailer::send
@ -50,32 +105,12 @@ class EngelsystemMailerTest extends TestCase
$message = $this->createMock(SwiftMessage::class); $message = $this->createMock(SwiftMessage::class);
/** @var SwiftMailer|MockObject $swiftMailer */ /** @var SwiftMailer|MockObject $swiftMailer */
$swiftMailer = $this->createMock(SwiftMailer::class); $swiftMailer = $this->createMock(SwiftMailer::class);
$swiftMailer->expects($this->once()) $this->setExpects($swiftMailer, 'createMessage', null, $message);
->method('createMessage') $this->setExpects($swiftMailer, 'send', null, 1);
->willReturn($message); $this->setExpects($message, 'setTo', [['to@xam.pel']], $message);
$swiftMailer->expects($this->once()) $this->setExpects($message, 'setFrom', ['foo@bar.baz', 'Lorem Ipsum'], $message);
->method('send') $this->setExpects($message, 'setSubject', ['[Mail test] Foo Bar'], $message);
->willReturn(1); $this->setExpects($message, 'setBody', ['Lorem Ipsum!'], $message);
$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);
$mailer = new EngelsystemMailer($swiftMailer); $mailer = new EngelsystemMailer($swiftMailer);
$mailer->setFromAddress('foo@bar.baz'); $mailer->setFromAddress('foo@bar.baz');

View File

@ -5,7 +5,7 @@ namespace Engelsystem\Test\Unit\Models;
use Carbon\Carbon; use Carbon\Carbon;
use Engelsystem\Models\EventConfig; use Engelsystem\Models\EventConfig;
use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\HasDatabase;
use PHPUnit\Framework\TestCase; use Engelsystem\Test\Unit\TestCase;
class EventConfigTest extends TestCase class EventConfigTest extends TestCase
{ {
@ -102,7 +102,8 @@ class EventConfigTest extends TestCase
*/ */
protected function getEventConfig() protected function getEventConfig()
{ {
return new class extends EventConfig { return new class extends EventConfig
{
/** /**
* @param string $value * @param string $value
* @param string $type * @param string $type
@ -122,6 +123,7 @@ class EventConfigTest extends TestCase
*/ */
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp();
$this->initDatabase(); $this->initDatabase();
} }
} }

View File

@ -4,7 +4,7 @@ namespace Engelsystem\Test\Unit\Models;
use Engelsystem\Models\LogEntry; use Engelsystem\Models\LogEntry;
use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\HasDatabase;
use PHPUnit\Framework\TestCase; use Engelsystem\Test\Unit\TestCase;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
class LogEntryTest extends TestCase class LogEntryTest extends TestCase
@ -38,6 +38,7 @@ class LogEntryTest extends TestCase
*/ */
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp();
$this->initDatabase(); $this->initDatabase();
} }
} }

View File

@ -5,8 +5,8 @@ namespace Engelsystem\Test\Unit\Models;
use Engelsystem\Models\User\HasUserModel; use Engelsystem\Models\User\HasUserModel;
use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\Models\User\Stub\HasUserModelImplementation; use Engelsystem\Test\Unit\Models\User\Stub\HasUserModelImplementation;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use PHPUnit\Framework\TestCase;
class HasUserModelTest extends TestCase class HasUserModelTest extends TestCase
{ {
@ -28,6 +28,7 @@ class HasUserModelTest extends TestCase
*/ */
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp();
$this->initDatabase(); $this->initDatabase();
} }
} }

View File

@ -10,7 +10,7 @@ use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State; use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\HasDatabase;
use PHPUnit\Framework\TestCase; use Engelsystem\Test\Unit\TestCase;
class UserTest extends TestCase class UserTest extends TestCase
{ {
@ -95,6 +95,7 @@ class UserTest extends TestCase
*/ */
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp();
$this->initDatabase(); $this->initDatabase();
} }
} }

View File

@ -26,6 +26,7 @@ class LegacyTest extends ExtensionTest
$this->assertExtensionExists('menuUserHints', 'header_render_hints', $functions, $isSafeHtml); $this->assertExtensionExists('menuUserHints', 'header_render_hints', $functions, $isSafeHtml);
$this->assertExtensionExists('menuUserSubmenu', 'make_user_submenu', $functions, $isSafeHtml); $this->assertExtensionExists('menuUserSubmenu', 'make_user_submenu', $functions, $isSafeHtml);
$this->assertExtensionExists('page', [$extension, 'getPage'], $functions); $this->assertExtensionExists('page', [$extension, 'getPage'], $functions);
$this->assertExtensionExists('msg', 'msg', $functions, $isSafeHtml);
} }
/** /**

View File

@ -2,18 +2,22 @@
namespace Engelsystem\Test\Unit; namespace Engelsystem\Test\Unit;
use PHPUnit\Framework\MockObject\Matcher\InvokedRecorder; use Engelsystem\Application;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use PHPUnit\Framework\TestCase as PHPUnitTestCase; use PHPUnit\Framework\TestCase as PHPUnitTestCase;
abstract class TestCase extends PHPUnitTestCase abstract class TestCase extends PHPUnitTestCase
{ {
/** @var Application */
protected $app;
/** /**
* @param MockObject $object * @param MockObject $object
* @param string $method * @param string $method
* @param array $arguments * @param array $arguments
* @param mixed $return * @param mixed $return
* @param InvokedRecorder $times * @param InvocationOrder $times
*/ */
protected function setExpects($object, $method, $arguments = null, $return = null, $times = null) protected function setExpects($object, $method, $arguments = null, $return = null, $times = null)
{ {
@ -34,4 +38,12 @@ abstract class TestCase extends PHPUnitTestCase
$invocation->willReturn($return); $invocation->willReturn($return);
} }
} }
/**
* Called before each test run
*/
protected function setUp(): void
{
$this->app = new Application(__DIR__ . '/../../');
}
} }