From 4987e060c473d2f40ef7b4b0e2924688497ac57a Mon Sep 17 00:00:00 2001 From: Bubka <858858+Bubka@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:46:50 +0200 Subject: [PATCH] Refactor & Complete tests for the authentication log feature --- app/Api/v1/Resources/UserAuthentication.php | 2 + .../Authentication/FailedLoginListener.php | 2 +- .../Authentication/LoginListener.php | 4 +- app/Models/AuthLog.php | 3 + database/factories/AuthLogFactory.php | 150 +++++++++++++ .../Controllers/UserManagerControllerTest.php | 198 ++++++++++-------- tests/Data/AuthenticationLogData.php | 159 -------------- tests/Feature/Http/Auth/LoginTest.php | 97 +++++++++ .../FailedLoginListenerTest.php | 29 +++ .../Authentication/LoginListenerTest.php | 32 +++ .../Authentication/LogoutListenerTest.php | 29 +++ .../VisitedByProxyUserListenerTest.php | 29 +++ 12 files changed, 484 insertions(+), 250 deletions(-) create mode 100644 database/factories/AuthLogFactory.php delete mode 100644 tests/Data/AuthenticationLogData.php create mode 100644 tests/Unit/Listeners/Authentication/FailedLoginListenerTest.php create mode 100644 tests/Unit/Listeners/Authentication/LoginListenerTest.php create mode 100644 tests/Unit/Listeners/Authentication/LogoutListenerTest.php create mode 100644 tests/Unit/Listeners/Authentication/VisitedByProxyUserListenerTest.php diff --git a/app/Api/v1/Resources/UserAuthentication.php b/app/Api/v1/Resources/UserAuthentication.php index 240cd457..fac3a213 100644 --- a/app/Api/v1/Resources/UserAuthentication.php +++ b/app/Api/v1/Resources/UserAuthentication.php @@ -18,6 +18,7 @@ use Jenssegers\Agent\Agent; * @property Carbon|null $logout_at * @property bool $login_successful * @property string|null $duration + * @property string|null $login_method */ class UserAuthentication extends JsonResource { @@ -67,6 +68,7 @@ class UserAuthentication extends JsonResource 'duration' => $this->logout_at ? Carbon::parse($this->logout_at)->diffForHumans(Carbon::parse($this->login_at), ['syntax' => CarbonInterface::DIFF_ABSOLUTE]) : null, + 'login_method' => $this->login_method, ]; } } diff --git a/app/Listeners/Authentication/FailedLoginListener.php b/app/Listeners/Authentication/FailedLoginListener.php index dc5be3d2..25b3b19c 100644 --- a/app/Listeners/Authentication/FailedLoginListener.php +++ b/app/Listeners/Authentication/FailedLoginListener.php @@ -57,7 +57,7 @@ class FailedLoginListener extends AbstractAccessListener 'login_method' => $this->loginMethod(), ]); - if ($user->preferences['notifyOnFailedLogin']) { + if ($user->preferences['notifyOnFailedLogin'] == true) { $user->notify(new FailedLogin($log)); } } diff --git a/app/Listeners/Authentication/LoginListener.php b/app/Listeners/Authentication/LoginListener.php index 7733184e..d643b644 100644 --- a/app/Listeners/Authentication/LoginListener.php +++ b/app/Listeners/Authentication/LoginListener.php @@ -45,7 +45,7 @@ class LoginListener extends AbstractAccessListener * @var \App\Models\User */ $user = $event->user; - $ip = config('2fauth.proxy_headers.forIp') ?? $this->request->ip(); + $ip = config('2fauth.proxy_headers.forIp') ?? $this->request->ip(); $userAgent = $this->request->userAgent(); $known = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->whereLoginSuccessful(true)->first(); $newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now()) < 1; @@ -60,7 +60,7 @@ class LoginListener extends AbstractAccessListener 'login_method' => $this->loginMethod(), ]); - if (! $known && ! $newUser && $user->preferences['notifyOnNewAuthDevice']) { + if (! $known && ! $newUser && $user->preferences['notifyOnNewAuthDevice'] == true) { $user->notify(new SignedInWithNewDevice($log)); } } diff --git a/app/Models/AuthLog.php b/app/Models/AuthLog.php index 2443e17f..222ad3de 100644 --- a/app/Models/AuthLog.php +++ b/app/Models/AuthLog.php @@ -24,6 +24,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; @@ -42,6 +43,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; */ class AuthLog extends Model { + use HasFactory; + /** * Indicates if the model should be timestamped. */ diff --git a/database/factories/AuthLogFactory.php b/database/factories/AuthLogFactory.php new file mode 100644 index 00000000..3e5bf0c2 --- /dev/null +++ b/database/factories/AuthLogFactory.php @@ -0,0 +1,150 @@ + + */ +class AuthLogFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'login_at' => now(), + 'login_successful' => true, + 'logout_at' => null, + 'guard' => 'web-guard', + 'login_method' => 'password', + ]; + } + + /** + * Indicate that the model is a failed login. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog> + */ + public function failedLogin() + { + return $this->state(function (array $attributes) { + return [ + 'login_successful' => false, + ]; + }); + } + + /** + * Indicate that the model has a logout date only, without login date. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog> + */ + public function logoutOnly() + { + return $this->state(function (array $attributes) { + return [ + 'login_at' => null, + 'login_successful' => false, + 'logout_at' => now(), + ]; + }); + } + + /** + * Indicate that the model has login during last month. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog> + */ + public function duringLastMonth() + { + return $this->state(function (array $attributes) { + $loginDate = now()->subDays(15); + $logoutDate = $loginDate->addHours(1); + + return [ + 'login_at' => $loginDate, + 'logout_at' => $logoutDate, + ]; + }); + } + + /** + * Indicate that the model has login during last 3 months. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog> + */ + public function duringLastThreeMonth() + { + return $this->state(function (array $attributes) { + $loginDate = now()->subMonths(2); + $logoutDate = $loginDate->addHours(1); + + return [ + 'login_at' => $loginDate, + 'logout_at' => $logoutDate, + ]; + }); + } + + /** + * Indicate that the model has login during last 6 month. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog> + */ + public function duringLastSixMonth() + { + return $this->state(function (array $attributes) { + $loginDate = now()->subMonths(4); + $logoutDate = $loginDate->addHours(1); + + return [ + 'login_at' => $loginDate, + 'logout_at' => $logoutDate, + ]; + }); + } + + /** + * Indicate that the model has login during last year. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog> + */ + public function duringLastYear() + { + return $this->state(function (array $attributes) { + $loginDate = now()->subMonths(10); + $logoutDate = $loginDate->addHours(1); + + return [ + 'login_at' => $loginDate, + 'logout_at' => $logoutDate, + ]; + }); + } + + /** + * Indicate that the model has login before last year. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog> + */ + public function beforeLastYear() + { + return $this->state(function (array $attributes) { + $loginDate = now()->subYears(2); + $logoutDate = $loginDate->addHours(1); + + return [ + 'login_at' => $loginDate, + 'logout_at' => $logoutDate, + ]; + }); + } +} \ No newline at end of file diff --git a/tests/Api/v1/Controllers/UserManagerControllerTest.php b/tests/Api/v1/Controllers/UserManagerControllerTest.php index 8043f6cd..6dd9c292 100644 --- a/tests/Api/v1/Controllers/UserManagerControllerTest.php +++ b/tests/Api/v1/Controllers/UserManagerControllerTest.php @@ -4,6 +4,7 @@ namespace Tests\Api\v1\Controllers; use App\Api\v1\Controllers\UserManagerController; use App\Api\v1\Resources\UserManagerResource; +use App\Models\AuthLog; use App\Models\User; use App\Policies\UserPolicy; use Database\Factories\UserFactory; @@ -22,7 +23,6 @@ use Laravel\Passport\TokenRepository; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; -use Tests\Data\AuthenticationLogData; use Tests\FeatureTestCase; #[CoversClass(UserManagerController::class)] @@ -524,33 +524,38 @@ class UserManagerControllerTest extends FeatureTestCase } /** - * Local feeder because Factory cannot be used here + * @test */ - protected function feedAuthenticationLog() : int + public function test_authentications_returns_all_entries() : void { - // Do not change creation order - $this->user->authentications()->create(AuthenticationLogData::beforeLastYear()); - $this->user->authentications()->create(AuthenticationLogData::duringLastYear()); - $this->user->authentications()->create(AuthenticationLogData::duringLastSixMonth()); - $this->user->authentications()->create(AuthenticationLogData::duringLastThreeMonth()); - $this->user->authentications()->create(AuthenticationLogData::duringLastMonth()); - $this->user->authentications()->create(AuthenticationLogData::noLogin()); - $this->user->authentications()->create(AuthenticationLogData::noLogout()); + AuthLog::factory()->for($this->user, 'authenticatable')->beforeLastYear()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastYear()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastSixMonth()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastThreeMonth()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastMonth()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->logoutOnly()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->failedLogin()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->create(); - return 7; + $this->actingAs($this->admin, 'api-guard') + ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications') + ->assertOk() + ->assertJsonCount(8); } /** * @test */ - public function test_authentications_returns_all_entries() : void + public function test_authentications_returns_user_entries_only() : void { - $created = $this->feedAuthenticationLog(); + AuthLog::factory()->for($this->admin, 'authenticatable')->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->create(); - $this->actingAs($this->admin, 'api-guard') + $response = $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications') - ->assertOk() - ->assertJsonCount($created); + ->assertJsonCount(1); + + $this->assertEquals($response->getData()[0]->id, $this->user->id); } /** @@ -558,7 +563,7 @@ class UserManagerControllerTest extends FeatureTestCase */ public function test_authentications_returns_expected_resource() : void { - $this->user->authentications()->create(AuthenticationLogData::duringLastMonth()); + AuthLog::factory()->for($this->user, 'authenticatable')->create(); $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications') @@ -574,6 +579,7 @@ class UserManagerControllerTest extends FeatureTestCase 'logout_at', 'login_successful', 'duration', + 'login_method', ], ]); } @@ -581,12 +587,12 @@ class UserManagerControllerTest extends FeatureTestCase /** * @test */ - public function test_authentications_returns_no_login_entry() : void + public function test_authentications_returns_loginless_entries() : void { - $this->user->authentications()->create(AuthenticationLogData::noLogin()); + $this->logUserOut(); $this->actingAs($this->admin, 'api-guard') - ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1') + ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications') ->assertJsonCount(1) ->assertJsonFragment([ 'login_at' => null, @@ -596,12 +602,12 @@ class UserManagerControllerTest extends FeatureTestCase /** * @test */ - public function test_authentications_returns_no_logout_entry() : void + public function test_authentications_returns_logoutless_entries() : void { - $this->user->authentications()->create(AuthenticationLogData::noLogout()); + $this->logUserIn(); $this->actingAs($this->admin, 'api-guard') - ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1') + ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications') ->assertJsonCount(1) ->assertJsonFragment([ 'logout_at' => null, @@ -613,14 +619,15 @@ class UserManagerControllerTest extends FeatureTestCase */ public function test_authentications_returns_failed_entry() : void { - $this->user->authentications()->create(AuthenticationLogData::failedLogin()); - $expected = Carbon::parse(AuthenticationLogData::failedLogin()['login_at'])->toDayDateTimeString(); + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => 'wrong_password', + ]); $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1') ->assertJsonCount(1) ->assertJsonFragment([ - 'login_at' => $expected, 'login_successful' => false, ]); } @@ -630,15 +637,16 @@ class UserManagerControllerTest extends FeatureTestCase */ public function test_authentications_returns_last_month_entries() : void { - $this->feedAuthenticationLog(); - $expected = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); + $this->travel(-2)->months(); + $this->logUserInAndOut(); + $this->travelBack(); + $this->logUserIn(); - $this->actingAs($this->admin, 'api-guard') + $response = $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1') - ->assertJsonCount(3) - ->assertJsonFragment([ - 'login_at' => $expected, - ]); + ->assertJsonCount(1); + + $this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now())); } /** @@ -646,19 +654,18 @@ class UserManagerControllerTest extends FeatureTestCase */ public function test_authentications_returns_last_three_months_entries() : void { - $this->feedAuthenticationLog(); - $expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); - $expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString(); + $this->travel(-100)->days(); + $this->logUserInAndOut(); + $this->travelBack(); + $this->travel(-80)->days(); + $this->logUserIn(); + $this->travelBack(); - $this->actingAs($this->admin, 'api-guard') + $response = $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=3') - ->assertJsonCount(4) - ->assertJsonFragment([ - 'login_at' => $expectedOneMonth, - ]) - ->assertJsonFragment([ - 'login_at' => $expectedThreeMonth, - ]); + ->assertJsonCount(1); + + $this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subDays(80))); } /** @@ -666,23 +673,18 @@ class UserManagerControllerTest extends FeatureTestCase */ public function test_authentications_returns_last_six_months_entries() : void { - $this->feedAuthenticationLog(); - $expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); - $expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString(); - $expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString(); + $this->travel(-7)->months(); + $this->logUserInAndOut(); + $this->travelBack(); + $this->travel(-5)->months(); + $this->logUserIn(); + $this->travelBack(); - $this->actingAs($this->admin, 'api-guard') + $response = $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=6') - ->assertJsonCount(5) - ->assertJsonFragment([ - 'login_at' => $expectedOneMonth, - ]) - ->assertJsonFragment([ - 'login_at' => $expectedThreeMonth, - ]) - ->assertJsonFragment([ - 'login_at' => $expectedSixMonth, - ]); + ->assertJsonCount(1); + + $this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subMonths(5))); } /** @@ -690,27 +692,18 @@ class UserManagerControllerTest extends FeatureTestCase */ public function test_authentications_returns_last_year_entries() : void { - $this->feedAuthenticationLog(); - $expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); - $expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString(); - $expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString(); - $expectedYear = Carbon::parse(AuthenticationLogData::duringLastYear()['login_at'])->toDayDateTimeString(); + $this->travel(-13)->months(); + $this->logUserInAndOut(); + $this->travelBack(); + $this->travel(-11)->months(); + $this->logUserIn(); + $this->travelBack(); - $this->actingAs($this->admin, 'api-guard') + $response = $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=12') - ->assertJsonCount(6) - ->assertJsonFragment([ - 'login_at' => $expectedOneMonth, - ]) - ->assertJsonFragment([ - 'login_at' => $expectedThreeMonth, - ]) - ->assertJsonFragment([ - 'login_at' => $expectedSixMonth, - ]) - ->assertJsonFragment([ - 'login_at' => $expectedYear, - ]); + ->assertJsonCount(1); + + $this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subMonths(11))); } /** @@ -719,7 +712,10 @@ class UserManagerControllerTest extends FeatureTestCase #[DataProvider('LimitProvider')] public function test_authentications_returns_limited_entries($limit) : void { - $this->feedAuthenticationLog(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastYear()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastSixMonth()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastThreeMonth()->create(); + AuthLog::factory()->for($this->user, 'authenticatable')->duringLastMonth()->create(); $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit) @@ -728,7 +724,7 @@ class UserManagerControllerTest extends FeatureTestCase } /** - * Provide various limit + * Provide various limits */ public static function LimitProvider() { @@ -736,6 +732,7 @@ class UserManagerControllerTest extends FeatureTestCase 'limited to 1' => [1], 'limited to 2' => [2], 'limited to 3' => [3], + 'limited to 4' => [4], ]; } @@ -744,13 +741,9 @@ class UserManagerControllerTest extends FeatureTestCase */ public function test_authentications_returns_expected_ip_and_useragent_chunks() : void { - $this->user->authentications()->create([ + AuthLog::factory()->for($this->user, 'authenticatable')->create([ 'ip_address' => '127.0.0.1', 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', - 'login_at' => now(), - 'login_successful' => true, - 'logout_at' => null, - 'location' => null, ]); $this->actingAs($this->admin, 'api-guard') @@ -771,7 +764,7 @@ class UserManagerControllerTest extends FeatureTestCase { $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit) - ->assertStatus(422); + ->assertInvalid(['limit']); } /** @@ -782,7 +775,7 @@ class UserManagerControllerTest extends FeatureTestCase { $this->actingAs($this->admin, 'api-guard') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=' . $period) - ->assertStatus(422); + ->assertInvalid(['period']); } /** @@ -798,4 +791,33 @@ class UserManagerControllerTest extends FeatureTestCase 'array' => ['[]'], ]; } + + /** + * Makes a request to login the user in + */ + protected function logUserIn() : void + { + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => self::PASSWORD, + ]); + } + + /** + * Makes a request to login the user out + */ + protected function logUserOut() : void + { + $this->actingAs($this->user, 'web-guard') + ->json('GET', '/user/logout'); + } + + /** + * Makes a request to login the user out + */ + protected function logUserInAndOut() : void + { + $this->logUserIn(); + $this->logUserOut(); + } } diff --git a/tests/Data/AuthenticationLogData.php b/tests/Data/AuthenticationLogData.php deleted file mode 100644 index 334cda23..00000000 --- a/tests/Data/AuthenticationLogData.php +++ /dev/null @@ -1,159 +0,0 @@ -subDays(15); - - return [ - 'ip_address' => fake()->ipv4(), - 'user_agent' => fake()->userAgent(), - 'login_at' => $loginDate, - 'login_successful' => false, - 'logout_at' => null, - 'location' => null, - ]; - } - - /** - * Indicate that the model should have no login date - * - * @return array - */ - public static function noLogin() - { - return [ - 'ip_address' => fake()->ipv4(), - 'user_agent' => fake()->userAgent(), - 'login_at' => null, - 'login_successful' => false, - 'logout_at' => now(), - 'location' => null, - ]; - } - - /** - * Indicate that the model should have no logout date - * - * @return array - */ - public static function noLogout() - { - return [ - 'ip_address' => fake()->ipv4(), - 'user_agent' => fake()->userAgent(), - 'login_at' => now(), - 'login_successful' => true, - 'logout_at' => null, - 'location' => null, - ]; - } - - /** - * Indicate that the model should have login during last month - * - * @return array - */ - public static function duringLastMonth() - { - $loginDate = now()->subDays(15); - $logoutDate = $loginDate->addHours(1); - - return [ - 'ip_address' => '127.0.0.1', - 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', - 'login_at' => $loginDate, - 'login_successful' => true, - 'logout_at' => $logoutDate, - 'location' => null, - ]; - } - - /** - * Indicate that the model should have login during last 3 month - * - * @return array - */ - public static function duringLastThreeMonth() - { - $loginDate = now()->subMonths(2); - $logoutDate = $loginDate->addHours(1); - - return [ - 'ip_address' => fake()->ipv4(), - 'user_agent' => fake()->userAgent(), - 'login_at' => $loginDate, - 'login_successful' => true, - 'logout_at' => $logoutDate, - 'location' => null, - ]; - } - - /** - * Indicate that the model should have login during last 6 month - * - * @return array - */ - public static function duringLastSixMonth() - { - $loginDate = now()->subMonths(4); - $logoutDate = $loginDate->addHours(1); - - return [ - 'ip_address' => fake()->ipv4(), - 'user_agent' => fake()->userAgent(), - 'login_at' => $loginDate, - 'login_successful' => true, - 'logout_at' => $logoutDate, - 'location' => null, - ]; - } - - /** - * Indicate that the model should have login during last month - * - * @return array - */ - public static function duringLastYear() - { - $loginDate = now()->subMonths(10); - $logoutDate = $loginDate->addHours(1); - - return [ - 'ip_address' => fake()->ipv4(), - 'user_agent' => fake()->userAgent(), - 'login_at' => $loginDate, - 'login_successful' => true, - 'logout_at' => $logoutDate, - 'location' => null, - ]; - } - - /** - * Indicate that the model should have login during last month - * - * @return array - */ - public static function beforeLastYear() - { - $loginDate = now()->subYears(2); - $logoutDate = $loginDate->addHours(1); - - return [ - 'ip_address' => fake()->ipv4(), - 'user_agent' => fake()->userAgent(), - 'login_at' => $loginDate, - 'login_successful' => true, - 'logout_at' => $logoutDate, - 'location' => null, - ]; - } -} diff --git a/tests/Feature/Http/Auth/LoginTest.php b/tests/Feature/Http/Auth/LoginTest.php index ca85c984..8835a35a 100644 --- a/tests/Feature/Http/Auth/LoginTest.php +++ b/tests/Feature/Http/Auth/LoginTest.php @@ -7,9 +7,14 @@ use App\Http\Middleware\RejectIfAuthenticated; use App\Http\Middleware\RejectIfDemoMode; use App\Http\Middleware\RejectIfReverseProxy; use App\Http\Middleware\SkipIfAuthenticated; +use App\Listeners\Authentication\FailedLoginListener; +use App\Listeners\Authentication\LoginListener; use App\Models\User; +use App\Notifications\FailedLogin; +use App\Notifications\SignedInWithNewDevice; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Notification; use PHPUnit\Framework\Attributes\CoversClass; use Tests\FeatureTestCase; @@ -21,6 +26,8 @@ use Tests\FeatureTestCase; #[CoversClass(RejectIfReverseProxy::class)] #[CoversClass(RejectIfDemoMode::class)] #[CoversClass(SkipIfAuthenticated::class)] +#[CoversClass(LoginListener::class)] +#[CoversClass(FailedLoginListener::class)] class LoginTest extends FeatureTestCase { /** @@ -70,6 +77,63 @@ class LoginTest extends FeatureTestCase ]); } + /** + * @test + */ + public function test_login_send_new_device_notification() + { + Notification::fake(); + + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => self::PASSWORD, + ])->assertOk(); + + $this->actingAs($this->user, 'web-guard') + ->json('GET', '/user/logout'); + + $this->travel(1)->minute(); + + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => self::PASSWORD, + ], [ + 'HTTP_USER_AGENT' => 'NotSymfony' + ])->assertOk(); + + Notification::assertSentTo($this->user, SignedInWithNewDevice::class); + } + + /** + * @test + */ + public function test_login_does_not_send_new_device_notification() + { + Notification::fake(); + + $this->user['preferences->notifyOnNewAuthDevice'] = 0; + $this->user->save(); + + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => self::PASSWORD, + ])->assertOk(); + + $this->actingAs($this->user, 'web-guard') + ->json('GET', '/user/logout'); + + $this->travel(1)->minute(); + + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => self::PASSWORD, + ], [ + 'HTTP_USER_AGENT' => 'NotSymfony' + ])->assertOk(); + + Notification::assertNothingSentTo($this->user); + } + /** * @test */ @@ -164,6 +228,39 @@ class LoginTest extends FeatureTestCase ]); } + /** + * @test + */ + public function test_login_with_invalid_credentials_send_failed_login_notification() + { + Notification::fake(); + + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => self::WRONG_PASSWORD, + ])->assertStatus(401); + + Notification::assertSentTo($this->user, FailedLogin::class); + } + + /** + * @test + */ + public function test_login_with_invalid_credentials_does_not_send_new_device_notification() + { + Notification::fake(); + + $this->user['preferences->notifyOnFailedLogin'] = 0; + $this->user->save(); + + $this->json('POST', '/user/login', [ + 'email' => $this->user->email, + 'password' => self::WRONG_PASSWORD, + ])->assertStatus(401); + + Notification::assertNothingSentTo($this->user); + } + /** * @test */ diff --git a/tests/Unit/Listeners/Authentication/FailedLoginListenerTest.php b/tests/Unit/Listeners/Authentication/FailedLoginListenerTest.php new file mode 100644 index 00000000..9d6df3e2 --- /dev/null +++ b/tests/Unit/Listeners/Authentication/FailedLoginListenerTest.php @@ -0,0 +1,29 @@ +