From dd0366296893a0e8da8ae0365387dd4823d53451 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 8 Oct 2019 16:17:06 +0200 Subject: [PATCH] Rebuild password reset --- config/routes.php | 6 + includes/controller/users_controller.php | 115 -------- includes/model/User_model.php | 19 -- includes/view/User_view.php | 35 --- resources/lang/de_DE/default.mo | Bin 46206 -> 46693 bytes resources/lang/de_DE/default.po | 22 +- resources/lang/en_US/default.mo | Bin 770 -> 1240 bytes resources/lang/en_US/default.po | 18 ++ resources/lang/pt_BR/default.mo | Bin 41129 -> 41132 bytes resources/lang/pt_BR/default.po | 2 +- resources/views/emails/mail.twig | 8 +- resources/views/emails/password-reset.twig | 3 + resources/views/macros/form.twig | 18 ++ resources/views/pages/login.twig | 2 +- .../views/pages/password/reset-form.twig | 18 ++ .../views/pages/password/reset-success.twig | 12 + resources/views/pages/password/reset.twig | 32 +++ src/Controllers/PasswordResetController.php | 167 +++++++++++ src/Middleware/LegacyMiddleware.php | 6 - .../PasswordResetControllerTest.php | 266 ++++++++++++++++++ 20 files changed, 566 insertions(+), 183 deletions(-) create mode 100644 resources/views/emails/password-reset.twig create mode 100644 resources/views/macros/form.twig create mode 100644 resources/views/pages/password/reset-form.twig create mode 100644 resources/views/pages/password/reset-success.twig create mode 100644 resources/views/pages/password/reset.twig create mode 100644 src/Controllers/PasswordResetController.php create mode 100644 tests/Unit/Controllers/PasswordResetControllerTest.php diff --git a/config/routes.php b/config/routes.php index 02fd3abd..e57d3079 100644 --- a/config/routes.php +++ b/config/routes.php @@ -13,6 +13,12 @@ $route->get('/login', 'AuthController@login'); $route->post('/login', 'AuthController@postLogin'); $route->get('/logout', 'AuthController@logout'); +// Password recovery +$route->get('/password/reset', 'PasswordResetController@reset'); +$route->post('/password/reset', 'PasswordResetController@postReset'); +$route->get('/password/reset/{token:.+}', 'PasswordResetController@resetPassword'); +$route->post('/password/reset/{token:.+}', 'PasswordResetController@postResetPassword'); + // Stats $route->get('/metrics', 'Metrics\\Controller@metrics'); $route->get('/stats', 'Metrics\\Controller@stats'); diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php index 892089e7..3ad2ffd9 100644 --- a/includes/controller/users_controller.php +++ b/includes/controller/users_controller.php @@ -1,7 +1,6 @@ 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. * 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; @@ -227,24 +226,6 @@ function User_reset_api_key($user, $log = true) } } -/** - * Generates a new password recovery token for given user. - * - * @param User $user - * @return string - */ -function User_generate_password_recovery_token($user) -{ - $reset = PasswordReset::findOrNew($user->id); - $reset->user_id = $user->id; - $reset->token = md5($user->name . time() . rand()); - $reset->save(); - - engelsystem_log('Password recovery for ' . User_Nick_render($user, true) . ' started.'); - - return $reset->token; -} - /** * @param User $user * @return float diff --git a/includes/view/User_view.php b/includes/view/User_view.php index b38a5062..95ecb626 100644 --- a/includes/view/User_view.php +++ b/includes/view/User_view.php @@ -759,41 +759,6 @@ function User_view_state_admin($freeloader, $user_source) return $state; } -/** - * View for password recovery step 1: E-Mail - * - * @return string - */ -function User_password_recovery_view() -{ - return page_with_title(user_password_recovery_title(), [ - msg(), - __('We will send you an e-mail with a password recovery link. Please use the email address you used for registration.'), - form([ - form_text('email', __('E-Mail'), ''), - form_submit('submit', __('Recover')) - ]) - ]); -} - -/** - * View for password recovery step 2: New password - * - * @return string - */ -function User_password_set_view() -{ - return page_with_title(user_password_recovery_title(), [ - msg(), - __('Please enter a new password.'), - form([ - form_password('password', __('Password')), - form_password('password2', __('Confirm password')), - form_submit('submit', __('Save')) - ]) - ]); -} - /** * @param array[] $user_angeltypes * @return string diff --git a/resources/lang/de_DE/default.mo b/resources/lang/de_DE/default.mo index fb93d59098f243fc9f3d8f04dc65da27eb7ce8d5..d4b7885bdb60dad5663232db1d20a4ced7afa4b8 100644 GIT binary patch delta 10236 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 delta 9860 zcmXZi33yId9>?+fB9Rq|NFs;`B9TNSNMcWk68jokQDfhtv6kXhgV?v%zO{(m*wPk* z&M>vBmQjkSWm>9Kjb)f>%Y1)1$MbkT=YQ_GXZfFVZc;OMe=hR+ei8T0;9|=h{dkn(fSQ6jEN;nl`a3fa6GuB6_ z{(@2p$8$TiDfm;-0ZU;o^urvijuWi;=uP~U^$Kdh?@$wYgvIeUWd2UkT800!{W2lZ#p(c0^eentw!`m2zcTpMeu5I3r z#c<*_SQZDO7CIFp8Q)noq}cd!f�Z>(YQF+C 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 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.mo b/resources/lang/pt_BR/default.mo index 8b8641565705f92b5dbcf78956dbb887cd62b6bb..d15826e6b0ee3c286dca910b7223e6c2b67156cf 100644 GIT binary patch delta 4947 zcmXZedvuRS9>?*Kgoq?aBIG8bkr0{-(I{~_Zc(M~w``;oO=yW)5|?TpBt#W)EfL$0 z?nR^Br8$d((>8W%MXI(fcBN;P6@NG_OBc-^#lAmg{_&cb-}B6T=R4nd;=@{>k7|7m z%?bAU8)N=F$(Tj>6K3F?$;P~d6&Q*)u{HjJ9WZc;F~Jy*&9Nu?Vjm30Ow_#Nu{UnU zO#BO`;}d)q)2ABG&^9xt8nXhAVrT3%&6rr6gfHV_jK*)U0B_@Kn44|PGk6B$@ds3( zZGL6U8yJi8une{E|4yN}l`jcIMrE@*%xgDtY%iVY-2GKu=)Rt$C(NHAysGVFu71wvDh;O1MXhKc+7?nET znfATbsPPUMf-$I!rr@<^1db}UcC(C0z`>|_^D%_=P0`baG3&8AKkUJ-_ys26EoX<> zcBg}#b5WTqb5^1z{uB1WPf^wX0JX6mb6h4-8F(IBu)Y~iBLGKZ1Wv&iT!`&(BgWx= z)a^KhDy~LUX2M^!MmsyB0_lnhFatGj7V3y+U`KotJym@<4PA>vsQY^cBk>9B5cBmy6m!J}Q$1n2PIA&mTc;=x?ZbFQEdu?$OZBe#QU{ z;r6pqWFRVILr^;&f%>C06I`usbScgHYqyco-L;7Vh+h-FZB=rau&Qy(XY`T7U|y5Eal0 zR7N(THo6N#bpL-#Lsfjl{csX>1a;U7{T5l(`;4`#b8n%cz}SM?KeS zsXg;h45Z(|_2V&=em7?(DpO-T8trM!LKVw$)K1@aZb3~{g__`xsGZlM0yyu+FFSuk z-Igb)dD<GJc=pUh)Qi3-%$k?g?cU# zbz6p@CZ3K8G#|CoLR3+{gMDxha#WuAhK3gWCn^(nP&@EnVN)H0+DQVc==xwJPRA%L zLQT9C|E6IMPQZ^b70uf=Q)#FwpMkw_6Q<$k7=w3lweEjw{&^R-IAd1XqB?-FjGsp> zcpLRcsOM^XJ+o00m7zYpAE7?kXHn1HK^i7loLs9RKqs)x>x*onGc2T%bXK?Qim_0OX+bPYA%ebn=T@7N7Q zqH4?QO+$aZCZb-Lg*t*FRODMQ01u!)e&RfiE$N?f{d!a;zeQ#8iu=9Mjo)(pCJf;B zW*eRswPylpXyP%bvz>wpWU;dpU!Y%s1=xt`IFTzOOHsvo0*B)x?2kh?+WsmWMgKH* z!O%_iKQetVSNFewhEn(?Dic>wFWka-?7P`6kc(u|RALTxE3+Sz667kIlc=-L++r7) zifQyWqiW_eR1r3yYUO+MXMNK|L#Z@d?F0d+j3l4}NJgFccvP*-K?SNN=Im@!w`M-4;DXoJ^xTj>J%0f{L&d_2NEMaUI7%tVIQQ4u{}HT!j(a z`CE_WI06MEWi!_gm(YJ1wb5@;NA(~401I}Le_gN8a{IwaLtU3~I2ZGfPlGv+ zBXPHzv|QhdFr9^~ZU8?H`w=*q0wppsvS# z%)lq8>dyF$P4zNV3QwS(Yj8HAj^sA#s2-pKdW10;_P)(P8ZM?&=b`3Xg!+9M`e}xBG*o== zqN={k^$%>%j{B_H!J5d>y?t0QZxIN HtU><=wBNr< delta 4944 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 + {% if label %} + + {% endif %} + + +{% endmacro %} + +{% macro hidden(name, value) %} + +{% endmacro %} + +{% macro submit(label) %} + +{% endmacro %} diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig index 6160508f..34dbd63f 100644 --- a/resources/views/pages/login.twig +++ b/resources/views/pages/login.twig @@ -62,7 +62,7 @@ diff --git a/resources/views/pages/password/reset-form.twig b/resources/views/pages/password/reset-form.twig new file mode 100644 index 00000000..60eb2499 --- /dev/null +++ b/resources/views/pages/password/reset-form.twig @@ -0,0 +1,18 @@ +{% extends "pages/password/reset.twig" %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block row_content %} +
+
+ {{ csrf() }} + + {{ f.input('password', __('Password'), 'password', true) }} + {{ f.input('password_confirmation', __('Confirm password'), 'password', true) }} + +
+ {{ f.submit(__('Save')) }} +
+
+
+{% endblock %} diff --git a/resources/views/pages/password/reset-success.twig b/resources/views/pages/password/reset-success.twig new file mode 100644 index 00000000..19b8a93e --- /dev/null +++ b/resources/views/pages/password/reset-success.twig @@ -0,0 +1,12 @@ +{% extends "pages/password/reset.twig" %} +{% import 'macros/base.twig' as m %} + +{% block row_content %} +
+ {% if type == 'email' %} + {{ m.alert(__('We sent you an email containing your password recovery link.'), 'info') }} + {% elseif type == 'reset' %} + {{ m.alert(__('Password saved.'), 'success') }} + {% endif %} +
+{% endblock %} diff --git a/resources/views/pages/password/reset.twig b/resources/views/pages/password/reset.twig new file mode 100644 index 00000000..289152ea --- /dev/null +++ b/resources/views/pages/password/reset.twig @@ -0,0 +1,32 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('Password recovery') }}{% endblock %} + +{% block content %} +
+

{{ __('Password recovery') }}

+ + {% for message in errors|default([]) %} + {{ m.alert(__(message), 'danger') }} + {% endfor %} + +
+ {% block row_content %} +
+
+ {{ 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) }} + +
+ {{ f.submit(__('Recover')) }} +
+
+
+ {% endblock %} +
+
+{% endblock %} diff --git a/src/Controllers/PasswordResetController.php b/src/Controllers/PasswordResetController.php new file mode 100644 index 00000000..505ed8eb --- /dev/null +++ b/src/Controllers/PasswordResetController.php @@ -0,0 +1,167 @@ + 'login', + 'postReset' => 'login', + 'resetPassword' => 'login', + 'postResetPassword' => 'login', + ]; + + /** + * @param Response $response + * @param SessionInterface $session + * @param EngelsystemMailer $mail + * @param LoggerInterface $log + */ + public function __construct( + Response $response, + SessionInterface $session, + EngelsystemMailer $mail, + LoggerInterface $log + ) { + $this->log = $log; + $this->mail = $mail; + $this->response = $response; + $this->session = $session; + } + + /** + * @return Response + */ + public function reset(): Response + { + return $this->showView('pages/password/reset'); + } + + /** + * @param Request $request + * @return Response + */ + public function postReset(Request $request): Response + { + $data = $this->validate($request, [ + 'email' => 'required|email', + ]); + + /** @var User $user */ + $user = User::whereEmail($data['email'])->first(); + if ($user) { + $reset = PasswordReset::findOrNew($user->id); + $reset->user_id = $user->id; + $reset->token = md5(random_bytes(64)); + $reset->save(); + + $this->log->info( + sprintf('Password recovery for %s (%u)', $user->name, $user->id), + ['user' => $user->toJson()] + ); + + $this->mail->sendViewTranslated( + $user, + 'Password recovery', + 'emails/password-reset', + ['username' => $user->name, 'reset' => $reset] + ); + } + + return $this->showView('pages/password/reset-success', ['type' => 'email']); + } + + /** + * @param Request $request + * @return Response + */ + public function resetPassword(Request $request): Response + { + $this->requireToken($request); + + return $this->showView('pages/password/reset-form'); + } + + /** + * @param Request $request + * @return Response + */ + public function postResetPassword(Request $request): Response + { + $reset = $this->requireToken($request); + + $data = $this->validate($request, [ + 'password' => 'required|min:' . config('min_password_length'), + 'password_confirmation' => 'required', + ]); + + if ($data['password'] !== $data['password_confirmation']) { + $this->session->set('errors', + array_merge($this->session->get('errors', []), ['validation.password.confirmed'])); + + return $this->showView('pages/password/reset-form'); + } + + auth()->setPassword($reset->user, $data['password']); + $reset->delete(); + + return $this->showView('pages/password/reset-success', ['type' => 'reset']); + } + + /** + * @param string $view + * @param array $data + * @return Response + */ + protected function showView($view = 'pages/password/reset', $data = []): Response + { + $errors = Collection::make(Arr::flatten($this->session->get('errors', []))); + $this->session->remove('errors'); + + return $this->response->withView( + $view, + array_merge_recursive(['errors' => $errors], $data) + ); + } + + /** + * @param Request $request + * @return PasswordReset + */ + protected function requireToken(Request $request): PasswordReset + { + $token = $request->getAttribute('token'); + /** @var PasswordReset|null $reset */ + $reset = PasswordReset::whereToken($token)->first(); + + if (!$reset) { + throw new HttpNotFound(); + } + + return $reset; + } +} diff --git a/src/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/tests/Unit/Controllers/PasswordResetControllerTest.php b/tests/Unit/Controllers/PasswordResetControllerTest.php new file mode 100644 index 00000000..54046cef --- /dev/null +++ b/tests/Unit/Controllers/PasswordResetControllerTest.php @@ -0,0 +1,266 @@ +getController('pages/password/reset'); + $response = $controller->reset(); + + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postReset + */ + public function testPostReset(): void + { + $this->initDatabase(); + $request = new Request([], ['email' => 'foo@bar.batz']); + $user = $this->createUser(); + + $controller = $this->getController( + 'pages/password/reset-success', + ['type' => 'email', 'errors' => collect()] + ); + /** @var TestLogger $log */ + $log = $this->args['log']; + /** @var EngelsystemMailer|MockObject $mailer */ + $mailer = $this->args['mailer']; + $this->setExpects($mailer, 'sendViewTranslated'); + + $controller->postReset($request); + + $this->assertNotEmpty(PasswordReset::find($user->id)->first()); + $this->assertTrue($log->hasInfoThatContains($user->name)); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postReset + */ + public function testPostResetInvalidRequest(): void + { + $request = new Request(); + + $controller = $this->getController(); + + $this->expectException(ValidationException::class); + $controller->postReset($request); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postReset + */ + public function testPostResetNoUser(): void + { + $this->initDatabase(); + $request = new Request([], ['email' => 'foo@bar.batz']); + + $controller = $this->getController( + 'pages/password/reset-success', + ['type' => 'email', 'errors' => collect()] + ); + + $controller->postReset($request); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::resetPassword + * @covers \Engelsystem\Controllers\PasswordResetController::requireToken + */ + public function testResetPassword(): void + { + $this->initDatabase(); + + $user = $this->createUser(); + $token = $this->createToken($user); + $request = new Request([], [], ['token' => $token->token]); + + $controller = $this->getController('pages/password/reset-form'); + + $controller->resetPassword($request); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::resetPassword + * @covers \Engelsystem\Controllers\PasswordResetController::requireToken + */ + public function testResetPasswordNoToken(): void + { + $this->initDatabase(); + $controller = $this->getController(); + + $this->expectException(HttpNotFound::class); + $controller->resetPassword(new Request()); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postResetPassword + */ + public function testPostResetPassword(): void + { + $this->initDatabase(); + + $this->app->instance('config', new Config(['min_password_length' => 3])); + $user = $this->createUser(); + $token = $this->createToken($user); + $password = 'SomeRandomPasswordForAmazingSecurity'; + $request = new Request( + [], + ['password' => $password, 'password_confirmation' => $password], + ['token' => $token->token] + ); + + $controller = $this->getController( + 'pages/password/reset-success', + ['type' => 'reset', 'errors' => collect()] + ); + + $auth = new Authenticator($request, $this->args['session'], $user); + $this->app->instance('authenticator', $auth); + + $response = $controller->postResetPassword($request); + $this->assertEquals(200, $response->getStatusCode()); + + $this->assertEmpty(PasswordReset::find($user->id)); + $this->assertNotNull(auth()->authenticate($user->name, $password)); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postResetPassword + * @covers \Engelsystem\Controllers\PasswordResetController::showView + */ + public function testPostResetPasswordNotMatching(): void + { + $this->initDatabase(); + + $this->app->instance('config', new Config(['min_password_length' => 3])); + $user = $this->createUser(); + $token = $this->createToken($user); + $password = 'SomeRandomPasswordForAmazingSecurity'; + $request = new Request( + [], + ['password' => $password, 'password_confirmation' => $password . 'OrNot'], + ['token' => $token->token] + ); + + $controller = $this->getController( + 'pages/password/reset-form', + ['errors' => collect(['some.other.error', 'validation.password.confirmed'])] + ); + /** @var Session $session */ + $session = $this->args['session']; + $session->set('errors', ['foo' => ['bar' => 'some.other.error']]); + + $controller->postResetPassword($request); + $this->assertEmpty($session->get('errors')); + } + + /** + * @return array + */ + protected function getControllerArgs(): array + { + $response = new Response(); + $session = new Session(new MockArraySessionStorage()); + /** @var EngelsystemMailer|MockObject $mailer */ + $mailer = $this->createMock(EngelsystemMailer::class); + $log = new TestLogger(); + $renderer = $this->createMock(Renderer::class); + $response->setRenderer($renderer); + + return $this->args = [ + 'response' => $response, + 'session' => $session, + 'mailer' => $mailer, + 'log' => $log, + 'renderer' => $renderer + ]; + } + + /** + * @param string $view + * @param array $data + * @return PasswordResetController + */ + protected function getController(?string $view = null, ?array $data = null): PasswordResetController + { + /** @var Response $response */ + /** @var Session $session */ + /** @var EngelsystemMailer|MockObject $mailer */ + /** @var TestLogger $log */ + /** @var Renderer|MockObject $renderer */ + list($response, $session, $mailer, $log, $renderer) = array_values($this->getControllerArgs()); + $controller = new PasswordResetController($response, $session, $mailer, $log); + $controller->setValidator(new Validator()); + + if ($view) { + $args = [$view]; + if ($data) { + $args[] = $data; + } + + $this->setExpects($renderer, 'render', $args, 'Foo'); + } + + return $controller; + } + + /** + * @return User + */ + protected function createUser(): User + { + $user = new User([ + 'name' => 'foo', + 'password' => '', + 'email' => 'foo@bar.batz', + 'api_key' => '', + ]); + $user->save(); + + return $user; + } + + /** + * @param User $user + * @return PasswordReset + */ + protected function createToken(User $user): PasswordReset + { + $reset = new PasswordReset(['user_id' => $user->id, 'token' => 'SomeTestToken123']); + $reset->save(); + + return $reset; + } +}