Browse Source

Refactor & Complete tests for the authentication log feature

Bubka 1 year ago
parent
commit
4987e060c4

+ 2 - 0
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,
         ];
     }
 }

+ 1 - 1
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));
             }
         }

+ 2 - 2
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));
         }
     }

+ 3 - 0
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.
      */

+ 150 - 0
database/factories/AuthLogFactory.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace Database\Factories;
+
+use ParagonIE\ConstantTime\Base32;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
+ */
+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,
+            ];
+        });
+    }
+}

+ 110 - 88
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();
+    }
 }

+ 0 - 159
tests/Data/AuthenticationLogData.php

@@ -1,159 +0,0 @@
-<?php
-
-namespace Tests\Data;
-
-class AuthenticationLogData
-{
-    /**
-     * Indicate that the model should have login date.
-     *
-     * @return array
-     */
-    public static function failedLogin()
-    {
-        $loginDate = now()->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,
-        ];
-    }
-}

+ 97 - 0
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
      */

+ 29 - 0
tests/Unit/Listeners/Authentication/FailedLoginListenerTest.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Tests\Unit\Listeners\Authentication;
+
+use App\Listeners\Authentication\FailedLoginListener;
+use Illuminate\Auth\Events\Failed;
+use Illuminate\Support\Facades\Event;
+use PHPUnit\Framework\Attributes\CoversClass;
+use Tests\TestCase;
+
+/**
+ * FailedLoginListenerTest test class
+ */
+#[CoversClass(FailedLoginListener::class)]
+class FailedLoginListenerTest extends TestCase
+{
+    /**
+     * @test
+     */
+    public function test_FailedLoginListener_listen_to_Failed_event()
+    {
+        Event::fake();
+
+        Event::assertListening(
+            Failed::class,
+            FailedLoginListener::class
+        );
+    }
+}

+ 32 - 0
tests/Unit/Listeners/Authentication/LoginListenerTest.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace Tests\Unit\Listeners\Authentication;
+
+use App\Listeners\Authentication\LoginListener;
+use App\Models\User;
+use Illuminate\Auth\Events\Login;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Event;
+use Mockery;
+use PHPUnit\Framework\Attributes\CoversClass;
+use Tests\TestCase;
+
+/**
+ * LoginListenerTest test class
+ */
+#[CoversClass(LoginListener::class)]
+class LoginListenerTest extends TestCase
+{
+    /**
+     * @test
+     */
+    public function test_LoginListener_listen_to_Login_event()
+    {
+        Event::fake();
+
+        Event::assertListening(
+            Login::class,
+            LoginListener::class
+        );
+    }
+}

+ 29 - 0
tests/Unit/Listeners/Authentication/LogoutListenerTest.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Tests\Unit\Listeners\Authentication;
+
+use App\Listeners\Authentication\LogoutListener;
+use Illuminate\Auth\Events\Logout;
+use Illuminate\Support\Facades\Event;
+use PHPUnit\Framework\Attributes\CoversClass;
+use Tests\TestCase;
+
+/**
+ * LogoutListenerTest test class
+ */
+#[CoversClass(LogoutListener::class)]
+class LogoutListenerTest extends TestCase
+{
+    /**
+     * @test
+     */
+    public function test_LogoutListener_listen_to_Logout_event()
+    {
+        Event::fake();
+
+        Event::assertListening(
+            Logout::class,
+            LogoutListener::class
+        );
+    }
+}

+ 29 - 0
tests/Unit/Listeners/Authentication/VisitedByProxyUserListenerTest.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Tests\Unit\Listeners\Authentication;
+
+use App\Events\VisitedByProxyUser;
+use App\Listeners\Authentication\VisitedByProxyUserListener;
+use Illuminate\Support\Facades\Event;
+use PHPUnit\Framework\Attributes\CoversClass;
+use Tests\TestCase;
+
+/**
+ * VisitedByProxyUserListenerTest test class
+ */
+#[CoversClass(VisitedByProxyUserListener::class)]
+class VisitedByProxyUserListenerTest extends TestCase
+{
+    /**
+     * @test
+     */
+    public function test_VisitedByProxyUserListener_listen_to_VisitedByProxyUser_event()
+    {
+        Event::fake();
+
+        Event::assertListening(
+            VisitedByProxyUser::class,
+            VisitedByProxyUserListener::class
+        );
+    }
+}