summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig16
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--README.md19
-rw-r--r--config/routes.php6
-rw-r--r--contrib/Dockerfile9
-rw-r--r--contrib/docker-compose.yml4
-rw-r--r--includes/controller/shifts_controller.php16
-rw-r--r--includes/controller/users_controller.php115
-rw-r--r--includes/helper/error_helper.php11
-rw-r--r--includes/includes.php1
-rw-r--r--includes/model/User_model.php19
-rw-r--r--includes/pages/guest_login.php4
-rw-r--r--includes/pages/user_atom.php16
-rw-r--r--includes/pages/user_ical.php17
-rw-r--r--includes/view/ShiftCalendarShiftRenderer.php32
-rw-r--r--includes/view/User_view.php93
-rw-r--r--package.json2
-rw-r--r--resources/assets/js/forms.js35
-rw-r--r--resources/assets/js/vendor.js7
-rw-r--r--resources/assets/themes/base.less66
-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/layouts/parts/navbar.twig18
-rw-r--r--resources/views/macros/form.twig18
-rw-r--r--resources/views/pages/credits.twig24
-rw-r--r--resources/views/pages/login.twig11
-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--resources/views/pages/user-shifts.html8
-rw-r--r--src/Controllers/AuthController.php2
-rw-r--r--src/Controllers/PasswordResetController.php167
-rw-r--r--src/Database/DatabaseServiceProvider.php3
-rw-r--r--src/Helpers/Translation/TranslationServiceProvider.php31
-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--storage/.gitignore4
-rw-r--r--storage/cache/.gitignore4
-rw-r--r--tests/Feature/Database/DatabaseServiceProviderTest.php29
-rw-r--r--tests/Feature/Database/DatabaseTest.php2
-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/Database/DatabaseServiceProviderTest.php5
-rw-r--r--tests/Unit/HasDatabase.php8
-rw-r--r--tests/Unit/Helpers/Translation/Assets/ba_RR/default.po3
-rw-r--r--tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php40
-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/RendererServiceProviderTest.php12
-rw-r--r--tests/Unit/Renderer/Twig/Extensions/LegacyTest.php1
-rw-r--r--tests/Unit/Renderer/TwigServiceProviderTest.php2
-rw-r--r--tests/Unit/TestCase.php16
-rw-r--r--yarn.lock2
78 files changed, 1333 insertions, 462 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..60e5c255
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# PHP PSR-2 Coding Standards
+# http://www.php-fig.org/psr/psr-2/
+
+root = true
+
+[*.php]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
diff --git a/.gitignore b/.gitignore
index 77a62c1e..cda5cf92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ _vimrc_local.vim
/public/coverage
/coverage
/unittests.xml
+/resources/lang/*/*.mo
# Composer files
/vendor/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 98ffd902..e14b1841 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,6 +9,7 @@ variables:
MYSQL_PASSWORD: engelsystem
MYSQL_HOST: mariadb
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
+ MYSQL_INITDB_SKIP_TZINFO: "yes"
DOCROOT: /var/www/
stages:
diff --git a/README.md b/README.md
index 02508157..9b4a3aaf 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,11 @@ The following instructions explain how to get, build and run the latest engelsys
```bash
yarn build
```
+ * Optionally (for better performance)
+ * Generate translation files
+ ```bash
+ find resources/lang/ -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \;
+ ```
### Configuration and Setup
* The webserver must have write access to the ```import``` and ```storage``` directories and read access for all other directories
@@ -127,6 +132,20 @@ Import database
docker exec -it engelsystem bin/migrate
```
+#### Local development
+To use the working directory in the container the docker-compose file has to be changed:
+```yaml
+[...]
+ nginx:
+ volumes:
+ - ../public/assets:/var/www/public/assets
+[...]
+ engelsystem:
+ volumes:
+ - ../:/var/www
+[...]
+```
+
#### Scripts
##### bin/deploy.sh
The `bin/deploy.sh` script can be used to deploy the engelsystem. It uses rsync to deploy the application to a server over ssh.
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/contrib/Dockerfile b/contrib/Dockerfile
index a7fd403b..013ccf1d 100644
--- a/contrib/Dockerfile
+++ b/contrib/Dockerfile
@@ -4,7 +4,12 @@ COPY ./ /app/
RUN composer --no-ansi install --no-dev --ignore-platform-reqs
RUN composer --no-ansi dump-autoload --optimize
-# Intermediate container for less layers
+# Intermediate containers for less layers
+FROM alpine as translation
+RUN apk add gettext
+COPY resources/lang/ /data
+RUN find /data -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \;
+
FROM alpine as data
COPY .babelrc .browserslistrc composer.json LICENSE package.json README.md webpack.config.js yarn.lock /app/
COPY bin/ /app/bin
@@ -13,11 +18,11 @@ COPY db/ /app/db
RUN mkdir /app/import/
COPY includes/ /app/includes
COPY public/ /app/public
-COPY resources/lang /app/resources/lang
COPY resources/views /app/resources/views
COPY src/ /app/src
COPY storage/ /app/storage
+COPY --from=translation /data/ /app/resources/lang
COPY --from=composer /app/vendor/ /app/vendor
COPY --from=composer /app/composer.lock /app/
diff --git a/contrib/docker-compose.yml b/contrib/docker-compose.yml
index b1dab793..4624cce4 100644
--- a/contrib/docker-compose.yml
+++ b/contrib/docker-compose.yml
@@ -27,19 +27,19 @@ services:
depends_on:
- database
database:
- image: mariadb:latest
+ image: mariadb:10.2
environment:
MYSQL_DATABASE: engelsystem
MYSQL_USER: engelsystem
MYSQL_PASSWORD: engelsystem
MYSQL_RANDOM_ROOT_PASSWORD: 1
+ MYSQL_INITDB_SKIP_TZINFO: "yes"
volumes:
- db:/var/lib/mysql
networks:
- database
volumes:
db: {}
- static: {}
networks:
internal:
diff --git a/includes/controller/shifts_controller.php b/includes/controller/shifts_controller.php
index a4d44151..15f92a9d 100644
--- a/includes/controller/shifts_controller.php
+++ b/includes/controller/shifts_controller.php
@@ -1,5 +1,6 @@
<?php
+use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\ShiftSignupState;
/**
@@ -349,17 +350,18 @@ function shift_next_controller()
function shifts_json_export_controller()
{
$request = request();
+ $user = auth()->apiUser('key');
- if (!$request->has('key') || !preg_match('/^[\da-f]{32}$/', $request->input('key'))) {
- engelsystem_error('Missing key.');
+ if (
+ !$request->has('key')
+ || !preg_match('/^[\da-f]{32}$/', $request->input('key'))
+ || !$user
+ ) {
+ throw new HttpForbidden('{"error":"Missing or invalid key"}', ['content-type' => 'application/json']);
}
- $user = auth()->apiUser('key');
- if (!$user) {
- engelsystem_error('Key invalid.');
- }
if (!auth()->can('shifts_json_export')) {
- engelsystem_error('No privilege for shifts_json_export.');
+ throw new HttpForbidden('{"error":"Not allowed"}', ['content-type' => 'application/json']);
}
$shifts = load_ical_shifts();
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/helper/error_helper.php b/includes/helper/error_helper.php
deleted file mode 100644
index 9314a57a..00000000
--- a/includes/helper/error_helper.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-/**
- * Displays a fatal message and stops execution.
- *
- * @param string $message
- */
-function engelsystem_error($message)
-{
- raw_output($message);
-}
diff --git a/includes/includes.php b/includes/includes.php
index 855ff359..601a6ca2 100644
--- a/includes/includes.php
+++ b/includes/includes.php
@@ -60,7 +60,6 @@ $includeFiles = [
__DIR__ . '/../includes/helper/graph_helper.php',
__DIR__ . '/../includes/helper/message_helper.php',
- __DIR__ . '/../includes/helper/error_helper.php',
__DIR__ . '/../includes/helper/email_helper.php',
__DIR__ . '/../includes/mailer/shifts_mailer.php',
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/pages/user_atom.php b/includes/pages/user_atom.php
index 8e5b4858..a491fea7 100644
--- a/includes/pages/user_atom.php
+++ b/includes/pages/user_atom.php
@@ -1,6 +1,7 @@
<?php
use Engelsystem\Database\DB;
+use Engelsystem\Http\Exceptions\HttpForbidden;
/**
* Publically available page to feed the news to feed readers
@@ -8,17 +9,18 @@ use Engelsystem\Database\DB;
function user_atom()
{
$request = request();
+ $user = auth()->apiUser('key');
- if (!$request->has('key') || !preg_match('/^[\da-f]{32}$/', $request->input('key'))) {
- engelsystem_error('Missing key.');
+ if (
+ !$request->has('key')
+ || !preg_match('/^[\da-f]{32}$/', $request->input('key'))
+ || empty($user)
+ ) {
+ throw new HttpForbidden('Missing or invalid key', ['content-type' => 'text/text']);
}
- $user = auth()->apiUser('key');
- if (empty($user)) {
- engelsystem_error('Key invalid.');
- }
if (!auth()->can('atom')) {
- engelsystem_error('No privilege for atom.');
+ throw new HttpForbidden('Not allowed', ['content-type' => 'text/text']);
}
$news = DB::select('
diff --git a/includes/pages/user_ical.php b/includes/pages/user_ical.php
index ee3a8340..2f3a7ccc 100644
--- a/includes/pages/user_ical.php
+++ b/includes/pages/user_ical.php
@@ -1,22 +1,25 @@
<?php
+use Engelsystem\Http\Exceptions\HttpForbidden;
+
/**
* Controller for ical output of users own shifts or any user_shifts filter.
*/
function user_ical()
{
$request = request();
+ $user = auth()->apiUser('key');
- if (!$request->has('key') || !preg_match('/^[\da-f]{32}$/', $request->input('key'))) {
- engelsystem_error('Missing key.');
+ if (
+ !$request->has('key')
+ || !preg_match('/^[\da-f]{32}$/', $request->input('key'))
+ || !$user
+ ) {
+ throw new HttpForbidden('Missing or invalid key', ['content-type' => 'text/text']);
}
- $user = auth()->apiUser('key');
- if (!$user) {
- engelsystem_error('Key invalid.');
- }
if (!auth()->can('ical')) {
- engelsystem_error('No privilege for ical.');
+ throw new HttpForbidden('Not allowed', ['content-type' => 'text/text']);
}
$ical_shifts = load_ical_shifts();
diff --git a/includes/view/ShiftCalendarShiftRenderer.php b/includes/view/ShiftCalendarShiftRenderer.php
index 1414c351..4911979f 100644
--- a/includes/view/ShiftCalendarShiftRenderer.php
+++ b/includes/view/ShiftCalendarShiftRenderer.php
@@ -38,23 +38,23 @@ class ShiftCalendarShiftRenderer
return [
$blocks,
- div(
- 'shift panel panel-' . $class . '" '
- . 'style="height: '
+ div('shift-card" style="height: '
. ($blocks * ShiftCalendarRenderer::BLOCK_HEIGHT - ShiftCalendarRenderer::MARGIN)
- . 'px"',
- [
- $this->renderShiftHead($shift, $class),
- div('panel-body', [
- $info_text,
- Room_name_render([
- 'RID' => $shift['RID'],
- 'Name' => $shift['room_name']
- ])
- ]),
- $shifts_row,
- div('shift-spacer')
- ]
+ . 'px;',
+ div(
+ 'shift panel panel-' . $class,
+ [
+ $this->renderShiftHead($shift, $class),
+ div('panel-body', [
+ $info_text,
+ Room_name_render([
+ 'RID' => $shift['RID'],
+ 'Name' => $shift['room_name']
+ ])
+ ]),
+ $shifts_row
+ ]
+ )
)
];
}
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/package.json b/package.json
index 5a8a5a44..8a623a3b 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
"jquery": "^3.3.1",
"jquery-ui": "^1.11.2",
- "moment": "^2.8.2",
+ "moment": "^2.12.0",
"moment-timezone": "^0.4.0",
"select2": "^4.0.6-rc.1",
"select2-bootstrap-theme": "0.1.0-beta.10"
diff --git a/resources/assets/js/forms.js b/resources/assets/js/forms.js
index f5818e97..9970b907 100644
--- a/resources/assets/js/forms.js
+++ b/resources/assets/js/forms.js
@@ -16,7 +16,7 @@ global.checkAll = (id, checked) => {
* Sets the checkboxes according to the given type
*
* @param {string} id The elements ID
- * @param {list} shifts_list A list of numbers
+ * @param {list} shiftsList A list of numbers
*/
global.checkOwnTypes = (id, shiftsList) => {
$('#' + id + ' input[type="checkbox"]').each(function () {
@@ -144,10 +144,10 @@ $(function () {
elem.children('input').on('click', function (ev) {
ev.stopImmediatePropagation();
if (typeof elem.data('DateTimePicker') === 'undefined') {
- elem.datetimepicker(opts);
- elem.data('DateTimePicker').show();
+ elem.datetimepicker(opts);
+ elem.data('DateTimePicker').show();
} else {
- elem.data('DateTimePicker').toggle();
+ elem.data('DateTimePicker').toggle();
}
});
});
@@ -173,3 +173,30 @@ $(function () {
});
});
});
+
+/**
+ * Set the filter selects to latest state
+ *
+ * Uses DOMContentLoaded to prevent flickering
+ */
+window.addEventListener('DOMContentLoaded', () => {
+ const filter = document.getElementById('collapseShiftsFilterSelect');
+ if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden') {
+ return;
+ }
+
+ filter.classList.remove('in');
+});
+$(() => {
+ if (typeof (localStorage) === 'undefined') {
+ return;
+ }
+
+ const onChange = (e) => {
+ localStorage.setItem('collapseShiftsFilterSelect', e.type);
+ };
+
+ $('#collapseShiftsFilterSelect')
+ .on('hidden.bs.collapse', onChange)
+ .on('shown.bs.collapse', onChange);
+});
diff --git a/resources/assets/js/vendor.js b/resources/assets/js/vendor.js
index bf3807f7..b4b6487d 100644
--- a/resources/assets/js/vendor.js
+++ b/resources/assets/js/vendor.js
@@ -10,6 +10,13 @@ require('./forms');
require('./sticky-headers');
require('./moment-countdown');
+moment.updateLocale('en', {
+ week : {
+ dow : 1, // Monday is the first day of the week.
+ doy : 4 // The week that contains Jan 4th is the first week of the year.
+ }
+});
+
$.ajaxSetup({
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')}
});
diff --git a/resources/assets/themes/base.less b/resources/assets/themes/base.less
index b44602f0..74dadc11 100644
--- a/resources/assets/themes/base.less
+++ b/resources/assets/themes/base.less
@@ -11,7 +11,7 @@ body {
}
.first {
- clear: both;
+ clear: both;
margin-top: 30px;
}
@@ -52,7 +52,7 @@ table a > .icon-icon_angel {
}
.table .form-group {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.stats {
@@ -60,20 +60,22 @@ table a > .icon-icon_angel {
.make-xs-column(6);
font-size: 20px;
height: 150px;
- text-align: center;
-
- .number {
- font-size: 80px;
- font-weight: 200;
- }
- @media (max-width: @screen-md) {
- font-size: inherit;
- .number {
- font-size: 40px;
- }
+ text-align: center;
+
+ .number {
+ font-size: 80px;
+ font-weight: 200;
+ }
+
+ @media (max-width: @screen-md) {
+ font-size: inherit;
+ .number {
+ font-size: 40px;
+ }
}
}
+
.stats-danger {
color: @brand-danger;
}
@@ -87,12 +89,12 @@ table a > .icon-icon_angel {
}
.dashboard-panel {
- position: relative;
- font-size: 20px;
+ position: relative;
+ font-size: 20px;
color: @headings-color;
- .panel-link {
- position: absolute;
+ .panel-link {
+ position: absolute;
width: 100%;
height: 100%;
top: 0;
@@ -102,11 +104,11 @@ table a > .icon-icon_angel {
background-color: @brand-primary; /* Fix to make div clickable in IE */
opacity: 0; /* Fix to make div clickable in IE */
filter: alpha(opacity=1); /* Fix to make div clickable in IE */
- }
+ }
- .panel-link:hover {
+ .panel-link:hover {
opacity: 0.3;
- }
+ }
}
.panel-primary .panel-heading a {
@@ -140,7 +142,7 @@ table a > .icon-icon_angel {
}
.selection .checkbox {
- display: block;
+ display: block;
}
.shift-calendar {
@@ -190,9 +192,20 @@ table a > .icon-icon_angel {
flex-shrink: 0;
}
- .shift {
- margin: 0 5px 5px 0;
+ .shift-card {
+ z-index: 0;
overflow: hidden;
+ position: relative;
+ margin: 0 5px 5px 0;
+
+ &:hover {
+ overflow: visible;
+ z-index: 100;
+ }
+
+ .shift {
+ min-height: 100%;
+ }
}
}
@@ -201,13 +214,14 @@ table a > .icon-icon_angel {
}
.column_duration {
- text-align: right;
+ text-align: right;
}
.messages {
&:focus {
outline: none;
}
+
a:focus {
outline: none;
}
@@ -296,7 +310,9 @@ table a > .icon-icon_angel {
float: none;
width: 100%;
- position: relative; left: 0; right: 0;
+ position: relative;
+ left: 0;
+ right: 0;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po
index 3c1e0d15..8dba9b4b 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
@@ -2772,3 +2772,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/layouts/parts/navbar.twig b/resources/views/layouts/parts/navbar.twig
index 61c6a10b..0b2eee63 100644
--- a/resources/views/layouts/parts/navbar.twig
+++ b/resources/views/layouts/parts/navbar.twig
@@ -55,16 +55,14 @@
{{ elements.toolbar_item(user.name, url('users', {'action': 'view'}), 'users', 'icon icon-icon_angel') }}
{% endif %}
- {% if has_permission_to('user_settings') or has_permission_to('logout') %}
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
- <span class="caret"></span>
- </a>
- <ul class="dropdown-menu" role="menu">
- {{ menuUserSubmenu()|join(" ")|raw }}
- </ul>
- </li>
- {% endif %}
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu" role="menu">
+ {{ menuUserSubmenu()|join(" ")|raw }}
+ </ul>
+ </li>
</ul>
{% 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/credits.twig b/resources/views/pages/credits.twig
index 3bb04895..8bd1f694 100644
--- a/resources/views/pages/credits.twig
+++ b/resources/views/pages/credits.twig
@@ -4,28 +4,28 @@
{% block content %}
<div class="container">
- <h1>Credits</h1>
+ <h1>{{ __('Credits') }}</h1>
<div class="row">
{% for title, credit in credits %}
<div class="col-md-4">
- <h2>{{ title }}</h2>
- {{ credit|markdown }}
+ <h2>{{ __(title) }}</h2>
+ {{ __(credit)|markdown }}
</div>
{% endfor %}
<div class="col-md-4">
- <h2>Source code</h2>
- <p>Version: <i>{{ version }}</i></p>
+ <h2>{{ __('Source code') }}</h2>
+ <p>{{ __('Version: _%s_', [version])|markdown }}</i></p>
<p>
- The original engelsystem was written by
- <a href="https://github.com/cookieBerlin/engelsystem">cookie</a>.
- It was then completely rewritten and enhanced by
- <a href="https://notrademark.de">msquare</a> (maintainer) and
- <a href="https://myigel.name">MyIgel</a>.
+ {{ __('The original engelsystem was written by
+[cookie](https://github.com/cookieBerlin/engelsystem).
+It was then completely rewritten and enhanced by [msquare](https://notrademark.de) (maintainer) and
+[MyIgel](https://myigel.name).')|markdown }}
</p>
<p>
- Please look at the <a href="https://github.com/engelsystem/engelsystem/graphs/contributors">
- contributor list on GitHub</a> for a complete list.
+ {{ __('Please have a look at the
+[contributors list on GitHub](https://github.com/engelsystem/engelsystem/graphs/contributors)
+for a complete list.')|markdown }}
</p>
</div>
</div>
diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig
index da6f4fdf..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 %}
@@ -50,7 +51,7 @@
<div class="input-group">
<span class="input-group-addon input-lg">{{ m.glyphicon('lock') }}</span>
<input class="form-control input-lg" id="form_password"
- type="password" name="password" value="" placeholder="{{ __('Password') }}">
+ type="password" name="password" value="" placeholder="{{ __('Password') }}">
</div>
</div>
@@ -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>
@@ -93,8 +94,10 @@
</a>
</div>
</div>
- </div>
- {{ m.glyphicon('info-sign') }} {{ __('Please note: You have to activate cookies!') }}
+ <div class="col-md-12 text-center">
+ {{ m.glyphicon('info-sign') }} {{ __('Please note: You have to activate cookies!') }}
+ </div>
+ </div>
</div>
{% endblock %}
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/resources/views/pages/user-shifts.html b/resources/views/pages/user-shifts.html
index 9ac501da..d5a98f80 100644
--- a/resources/views/pages/user-shifts.html
+++ b/resources/views/pages/user-shifts.html
@@ -55,12 +55,12 @@
<div class="col-md-6">
<button class="btn btn-info btn-sm hidden-print" style="margin-top: 20px; margin-bottom:10px" type="button"
data-toggle="collapse"
- data-target="#collapseRoomSelect" aria-expanded="false"
- aria-controls="collapseRoomSelect"
+ data-target="#collapseShiftsFilterSelect" aria-expanded="true"
+ aria-controls="collapseShiftsFilterSelect"
>
collapse/show filters
</button>
- <div class="collapse in" id="collapseRoomSelect">
+ <div class="collapse in" id="collapseShiftsFilterSelect">
<div class="row">
<div class="col-xs-4 col-xxs-12">%room_select%</div>
<div class="col-xs-4 col-xxs-12">%type_select%</div>
@@ -79,5 +79,5 @@
%shifts_table%
<div class="hidden-print">
-%ical_text%
+ %ical_text%
</div> \ No newline at end of file
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/Database/DatabaseServiceProvider.php b/src/Database/DatabaseServiceProvider.php
index d0068b54..b5225015 100644
--- a/src/Database/DatabaseServiceProvider.php
+++ b/src/Database/DatabaseServiceProvider.php
@@ -15,6 +15,7 @@ class DatabaseServiceProvider extends ServiceProvider
{
$config = $this->app->get('config');
$capsule = $this->app->make(CapsuleManager::class);
+ $now = Carbon::now($config->get('timezone'));
$dbConfig = $config->get('database');
$capsule->addConnection(array_merge([
@@ -25,7 +26,7 @@ class DatabaseServiceProvider extends ServiceProvider
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
- 'timezone' => Carbon::now()->format('P'),
+ 'timezone' => $now->format('P'),
'prefix' => '',
], $dbConfig));
diff --git a/src/Helpers/Translation/TranslationServiceProvider.php b/src/Helpers/Translation/TranslationServiceProvider.php
index 09337dad..6df9b0fe 100644
--- a/src/Helpers/Translation/TranslationServiceProvider.php
+++ b/src/Helpers/Translation/TranslationServiceProvider.php
@@ -5,6 +5,7 @@ namespace Engelsystem\Helpers\Translation;
use Engelsystem\Config\Config;
use Engelsystem\Container\ServiceProvider;
use Gettext\Translations;
+use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Session\Session;
class TranslationServiceProvider extends ServiceProvider
@@ -40,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');
}
/**
@@ -67,14 +70,18 @@ class TranslationServiceProvider extends ServiceProvider
public function getTranslator(string $locale): GettextTranslator
{
if (!isset($this->translators[$locale])) {
- $file = $this->app->get('path.lang') . '/' . $locale . '/default.mo';
+ $file = $this->getFile($locale);
/** @var GettextTranslator $translator */
$translator = $this->app->make(GettextTranslator::class);
/** @var Translations $translations */
$translations = $this->app->make(Translations::class);
- $translations->addFromMoFile($file);
+ if (Str::endsWith($file, '.mo')) {
+ $translations->addFromMoFile($file);
+ } else {
+ $translations->addFromPoFile($file);
+ }
$translator->loadTranslations($translations);
@@ -83,4 +90,20 @@ class TranslationServiceProvider extends ServiceProvider
return $this->translators[$locale];
}
+
+ /**
+ * @param string $locale
+ * @return string
+ */
+ protected function getFile(string $locale): string
+ {
+ $filepath = $file = $this->app->get('path.lang') . '/' . $locale . '/default';
+ $file = $filepath . '.mo';
+
+ if (!file_exists($file)) {
+ $file = $filepath . '.po';
+ }
+
+ return $file;
+ }
}
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/storage/.gitignore b/storage/.gitignore
new file mode 100644
index 00000000..fa776e4d
--- /dev/null
+++ b/storage/.gitignore
@@ -0,0 +1,4 @@
+/*
+!/app
+!/cache
+!.gitignore
diff --git a/storage/cache/.gitignore b/storage/cache/.gitignore
index ea604cb2..b376bb75 100644
--- a/storage/cache/.gitignore
+++ b/storage/cache/.gitignore
@@ -1 +1,3 @@
-/routes.cache.php
+/*
+!/views
+!.gitignore
diff --git a/tests/Feature/Database/DatabaseServiceProviderTest.php b/tests/Feature/Database/DatabaseServiceProviderTest.php
index d66ed25c..aa4dbc7b 100644
--- a/tests/Feature/Database/DatabaseServiceProviderTest.php
+++ b/tests/Feature/Database/DatabaseServiceProviderTest.php
@@ -2,10 +2,9 @@
namespace Engelsystem\Test\Feature\Database;
-use Engelsystem\Application;
use Engelsystem\Config\Config;
+use Engelsystem\Database\Database;
use Engelsystem\Database\DatabaseServiceProvider;
-use PHPUnit\Framework\MockObject\MockObject;
class DatabaseServiceProviderTest extends DatabaseTest
{
@@ -14,27 +13,13 @@ class DatabaseServiceProviderTest extends DatabaseTest
*/
public function testRegister()
{
- /** @var Config|MockObject $config */
- $config = $this->getMockBuilder(Config::class)
- ->getMock();
+ $this->app->instance('config', new Config([
+ 'database' => $this->getDbConfig(),
+ 'timezone' => 'UTC',
+ ]));
- /** @var Application|MockObject $app */
- $app = $this->getMockBuilder(Application::class)
- ->setMethods(['get'])
- ->getMock();
- Application::setInstance($app);
-
- $app->expects($this->once())
- ->method('get')
- ->with('config')
- ->willReturn($config);
-
- $config->expects($this->atLeastOnce())
- ->method('get')
- ->with('database')
- ->willReturn($this->getDbConfig());
-
- $serviceProvider = new DatabaseServiceProvider($app);
+ $serviceProvider = new DatabaseServiceProvider($this->app);
$serviceProvider->register();
+ $this->assertTrue($this->app->has(Database::class));
}
}
diff --git a/tests/Feature/Database/DatabaseTest.php b/tests/Feature/Database/DatabaseTest.php
index 11df6779..0116e526 100644
--- a/tests/Feature/Database/DatabaseTest.php
+++ b/tests/Feature/Database/DatabaseTest.php
@@ -2,7 +2,7 @@
namespace Engelsystem\Test\Feature\Database;
-use PHPUnit\Framework\TestCase;
+use Engelsystem\Test\Unit\TestCase;
abstract class DatabaseTest extends TestCase
{
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/Database/DatabaseServiceProviderTest.php b/tests/Unit/Database/DatabaseServiceProviderTest.php
index c3e4c5d0..241c47b7 100644
--- a/tests/Unit/Database/DatabaseServiceProviderTest.php
+++ b/tests/Unit/Database/DatabaseServiceProviderTest.php
@@ -100,7 +100,10 @@ class DatabaseServiceProviderTest extends ServiceProviderTest
$app = $this->getApp(['get', 'make', 'instance']);
$this->setExpects($app, 'get', ['config'], $config);
- $this->setExpects($config, 'get', ['database'], $dbConfigData, $this->atLeastOnce());
+ $config->expects($this->exactly(2))
+ ->method('get')
+ ->withConsecutive(['timezone'], ['database'])
+ ->willReturnOnConsecutiveCalls('UTC', $dbConfigData);
$app->expects($this->atLeastOnce())
->method('make')
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/Assets/ba_RR/default.po b/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po
new file mode 100644
index 00000000..887e2daa
--- /dev/null
+++ b/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po
@@ -0,0 +1,3 @@
+# Testing content
+msgid "foo.bar"
+msgstr "B Arr!"
diff --git a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
index 91307bdd..e55fdf02 100644
--- a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
+++ b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
@@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Session\Session;
class TranslationServiceProviderTest extends ServiceProviderTest
{
/**
- * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::register()
+ * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::register
*/
public function testRegister(): void
{
@@ -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 */
@@ -30,7 +30,7 @@ class TranslationServiceProviderTest extends ServiceProviderTest
/** @var TranslationServiceProvider|MockObject $serviceProvider */
$serviceProvider = $this->getMockBuilder(TranslationServiceProvider::class)
->setConstructorArgs([$app])
- ->setMethods(['setLocale'])
+ ->onlyMethods(['setLocale'])
->getMock();
$app->expects($this->exactly(2))
@@ -60,18 +60,22 @@ 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();
}
/**
- * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator()
+ * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator
*/
public function testGetTranslator(): void
{
@@ -87,4 +91,20 @@ class TranslationServiceProviderTest extends ServiceProviderTest
// Retry from cache
$serviceProvider->getTranslator('fo_OO');
}
+
+ /**
+ * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator
+ * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getFile
+ */
+ public function testGetTranslatorFromPo(): void
+ {
+ $app = $this->getApp(['get']);
+ $this->setExpects($app, 'get', ['path.lang'], __DIR__ . '/Assets');
+
+ $serviceProvider = new TranslationServiceProvider($app);
+
+ // Get translator using a .po file
+ $translator = $serviceProvider->getTranslator('ba_RR');
+ $this->assertEquals('B Arr!', $translator->gettext('foo.bar'));
+ }
}
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/RendererServiceProviderTest.php b/tests/Unit/Renderer/RendererServiceProviderTest.php
index 224e36d4..e655284d 100644
--- a/tests/Unit/Renderer/RendererServiceProviderTest.php
+++ b/tests/Unit/Renderer/RendererServiceProviderTest.php
@@ -67,15 +67,13 @@ class RendererServiceProviderTest extends ServiceProviderTest
$app = $this->getApp(['get', 'tagged']);
- $engines = [$engine1, $engine2];
-
$this->setExpects($app, 'get', ['renderer'], $renderer);
- $this->setExpects($app, 'tagged', ['renderer.engine'], $engines);
+ $this->setExpects($app, 'tagged', ['renderer.engine'], [$engine1, $engine2]);
- $invocation = $renderer
- ->expects($this->exactly(count($engines)))
- ->method('addRenderer');
- call_user_func_array([$invocation, 'withConsecutive'], $engines);
+ $renderer
+ ->expects($this->exactly(2))
+ ->method('addRenderer')
+ ->withConsecutive([$engine1], [$engine2]);
$serviceProvider = new RendererServiceProvider($app);
$serviceProvider->boot();
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/Renderer/TwigServiceProviderTest.php b/tests/Unit/Renderer/TwigServiceProviderTest.php
index 86dee1de..ee4cd971 100644
--- a/tests/Unit/Renderer/TwigServiceProviderTest.php
+++ b/tests/Unit/Renderer/TwigServiceProviderTest.php
@@ -82,7 +82,7 @@ class TwigServiceProviderTest extends ServiceProviderTest
$twig->expects($this->exactly(2))
->method('addExtension')
- ->withConsecutive($firsExtension, $secondExtension);
+ ->withConsecutive([$firsExtension], [$secondExtension]);
$serviceProvider = new TwigServiceProvider($app);
$serviceProvider->boot();
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__ . '/../../');
+ }
}
diff --git a/yarn.lock b/yarn.lock
index cec94e2e..c49eb596 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3328,7 +3328,7 @@ moment-timezone@^0.4.0:
dependencies:
moment ">= 2.6.0"
-"moment@>= 2.6.0", moment@^2.10, moment@^2.10.2, moment@^2.8.2:
+"moment@>= 2.6.0", moment@^2.10, moment@^2.10.2, moment@^2.12.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==