Refactor & Complete tests for the authentication log feature

This commit is contained in:
Bubka 2024-04-24 21:46:50 +02:00
parent 76c3b6fe0c
commit 4987e060c4
12 changed files with 484 additions and 250 deletions

View File

@ -18,6 +18,7 @@ use Jenssegers\Agent\Agent;
* @property Carbon|null $logout_at * @property Carbon|null $logout_at
* @property bool $login_successful * @property bool $login_successful
* @property string|null $duration * @property string|null $duration
* @property string|null $login_method
*/ */
class UserAuthentication extends JsonResource class UserAuthentication extends JsonResource
{ {
@ -67,6 +68,7 @@ class UserAuthentication extends JsonResource
'duration' => $this->logout_at 'duration' => $this->logout_at
? Carbon::parse($this->logout_at)->diffForHumans(Carbon::parse($this->login_at), ['syntax' => CarbonInterface::DIFF_ABSOLUTE]) ? Carbon::parse($this->logout_at)->diffForHumans(Carbon::parse($this->login_at), ['syntax' => CarbonInterface::DIFF_ABSOLUTE])
: null, : null,
'login_method' => $this->login_method,
]; ];
} }
} }

View File

@ -57,7 +57,7 @@ class FailedLoginListener extends AbstractAccessListener
'login_method' => $this->loginMethod(), 'login_method' => $this->loginMethod(),
]); ]);
if ($user->preferences['notifyOnFailedLogin']) { if ($user->preferences['notifyOnFailedLogin'] == true) {
$user->notify(new FailedLogin($log)); $user->notify(new FailedLogin($log));
} }
} }

View File

@ -60,7 +60,7 @@ class LoginListener extends AbstractAccessListener
'login_method' => $this->loginMethod(), 'login_method' => $this->loginMethod(),
]); ]);
if (! $known && ! $newUser && $user->preferences['notifyOnNewAuthDevice']) { if (! $known && ! $newUser && $user->preferences['notifyOnNewAuthDevice'] == true) {
$user->notify(new SignedInWithNewDevice($log)); $user->notify(new SignedInWithNewDevice($log));
} }
} }

View File

@ -24,6 +24,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
@ -42,6 +43,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
*/ */
class AuthLog extends Model class AuthLog extends Model
{ {
use HasFactory;
/** /**
* Indicates if the model should be timestamped. * Indicates if the model should be timestamped.
*/ */

View File

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

View File

@ -4,6 +4,7 @@ namespace Tests\Api\v1\Controllers;
use App\Api\v1\Controllers\UserManagerController; use App\Api\v1\Controllers\UserManagerController;
use App\Api\v1\Resources\UserManagerResource; use App\Api\v1\Resources\UserManagerResource;
use App\Models\AuthLog;
use App\Models\User; use App\Models\User;
use App\Policies\UserPolicy; use App\Policies\UserPolicy;
use Database\Factories\UserFactory; use Database\Factories\UserFactory;
@ -22,7 +23,6 @@ use Laravel\Passport\TokenRepository;
use Mockery\MockInterface; use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use Tests\Data\AuthenticationLogData;
use Tests\FeatureTestCase; use Tests\FeatureTestCase;
#[CoversClass(UserManagerController::class)] #[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 AuthLog::factory()->for($this->user, 'authenticatable')->beforeLastYear()->create();
$this->user->authentications()->create(AuthenticationLogData::beforeLastYear()); AuthLog::factory()->for($this->user, 'authenticatable')->duringLastYear()->create();
$this->user->authentications()->create(AuthenticationLogData::duringLastYear()); AuthLog::factory()->for($this->user, 'authenticatable')->duringLastSixMonth()->create();
$this->user->authentications()->create(AuthenticationLogData::duringLastSixMonth()); AuthLog::factory()->for($this->user, 'authenticatable')->duringLastThreeMonth()->create();
$this->user->authentications()->create(AuthenticationLogData::duringLastThreeMonth()); AuthLog::factory()->for($this->user, 'authenticatable')->duringLastMonth()->create();
$this->user->authentications()->create(AuthenticationLogData::duringLastMonth()); AuthLog::factory()->for($this->user, 'authenticatable')->logoutOnly()->create();
$this->user->authentications()->create(AuthenticationLogData::noLogin()); AuthLog::factory()->for($this->user, 'authenticatable')->failedLogin()->create();
$this->user->authentications()->create(AuthenticationLogData::noLogout()); 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 * @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') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
->assertOk() ->assertJsonCount(1);
->assertJsonCount($created);
$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 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') $this->actingAs($this->admin, 'api-guard')
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
@ -574,6 +579,7 @@ class UserManagerControllerTest extends FeatureTestCase
'logout_at', 'logout_at',
'login_successful', 'login_successful',
'duration', 'duration',
'login_method',
], ],
]); ]);
} }
@ -581,12 +587,12 @@ class UserManagerControllerTest extends FeatureTestCase
/** /**
* @test * @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') $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) ->assertJsonCount(1)
->assertJsonFragment([ ->assertJsonFragment([
'login_at' => null, 'login_at' => null,
@ -596,12 +602,12 @@ class UserManagerControllerTest extends FeatureTestCase
/** /**
* @test * @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') $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) ->assertJsonCount(1)
->assertJsonFragment([ ->assertJsonFragment([
'logout_at' => null, 'logout_at' => null,
@ -613,14 +619,15 @@ class UserManagerControllerTest extends FeatureTestCase
*/ */
public function test_authentications_returns_failed_entry() : void public function test_authentications_returns_failed_entry() : void
{ {
$this->user->authentications()->create(AuthenticationLogData::failedLogin()); $this->json('POST', '/user/login', [
$expected = Carbon::parse(AuthenticationLogData::failedLogin()['login_at'])->toDayDateTimeString(); 'email' => $this->user->email,
'password' => 'wrong_password',
]);
$this->actingAs($this->admin, 'api-guard') $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?period=1')
->assertJsonCount(1) ->assertJsonCount(1)
->assertJsonFragment([ ->assertJsonFragment([
'login_at' => $expected,
'login_successful' => false, 'login_successful' => false,
]); ]);
} }
@ -630,15 +637,16 @@ class UserManagerControllerTest extends FeatureTestCase
*/ */
public function test_authentications_returns_last_month_entries() : void public function test_authentications_returns_last_month_entries() : void
{ {
$this->feedAuthenticationLog(); $this->travel(-2)->months();
$expected = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); $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') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
->assertJsonCount(3) ->assertJsonCount(1);
->assertJsonFragment([
'login_at' => $expected, $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 public function test_authentications_returns_last_three_months_entries() : void
{ {
$this->feedAuthenticationLog(); $this->travel(-100)->days();
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); $this->logUserInAndOut();
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString(); $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') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=3')
->assertJsonCount(4) ->assertJsonCount(1);
->assertJsonFragment([
'login_at' => $expectedOneMonth, $this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subDays(80)));
])
->assertJsonFragment([
'login_at' => $expectedThreeMonth,
]);
} }
/** /**
@ -666,23 +673,18 @@ class UserManagerControllerTest extends FeatureTestCase
*/ */
public function test_authentications_returns_last_six_months_entries() : void public function test_authentications_returns_last_six_months_entries() : void
{ {
$this->feedAuthenticationLog(); $this->travel(-7)->months();
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); $this->logUserInAndOut();
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString(); $this->travelBack();
$expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString(); $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') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=6')
->assertJsonCount(5) ->assertJsonCount(1);
->assertJsonFragment([
'login_at' => $expectedOneMonth, $this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subMonths(5)));
])
->assertJsonFragment([
'login_at' => $expectedThreeMonth,
])
->assertJsonFragment([
'login_at' => $expectedSixMonth,
]);
} }
/** /**
@ -690,27 +692,18 @@ class UserManagerControllerTest extends FeatureTestCase
*/ */
public function test_authentications_returns_last_year_entries() : void public function test_authentications_returns_last_year_entries() : void
{ {
$this->feedAuthenticationLog(); $this->travel(-13)->months();
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString(); $this->logUserInAndOut();
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString(); $this->travelBack();
$expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString(); $this->travel(-11)->months();
$expectedYear = Carbon::parse(AuthenticationLogData::duringLastYear()['login_at'])->toDayDateTimeString(); $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') ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=12')
->assertJsonCount(6) ->assertJsonCount(1);
->assertJsonFragment([
'login_at' => $expectedOneMonth, $this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subMonths(11)));
])
->assertJsonFragment([
'login_at' => $expectedThreeMonth,
])
->assertJsonFragment([
'login_at' => $expectedSixMonth,
])
->assertJsonFragment([
'login_at' => $expectedYear,
]);
} }
/** /**
@ -719,7 +712,10 @@ class UserManagerControllerTest extends FeatureTestCase
#[DataProvider('LimitProvider')] #[DataProvider('LimitProvider')]
public function test_authentications_returns_limited_entries($limit) : void 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') $this->actingAs($this->admin, 'api-guard')
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit) ->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() public static function LimitProvider()
{ {
@ -736,6 +732,7 @@ class UserManagerControllerTest extends FeatureTestCase
'limited to 1' => [1], 'limited to 1' => [1],
'limited to 2' => [2], 'limited to 2' => [2],
'limited to 3' => [3], '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 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', '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', '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') $this->actingAs($this->admin, 'api-guard')
@ -771,7 +764,7 @@ class UserManagerControllerTest extends FeatureTestCase
{ {
$this->actingAs($this->admin, 'api-guard') $this->actingAs($this->admin, 'api-guard')
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit) ->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') $this->actingAs($this->admin, 'api-guard')
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=' . $period) ->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=' . $period)
->assertStatus(422); ->assertInvalid(['period']);
} }
/** /**
@ -798,4 +791,33 @@ class UserManagerControllerTest extends FeatureTestCase
'array' => ['[]'], '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();
}
} }

View File

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

View File

@ -7,9 +7,14 @@ use App\Http\Middleware\RejectIfAuthenticated;
use App\Http\Middleware\RejectIfDemoMode; use App\Http\Middleware\RejectIfDemoMode;
use App\Http\Middleware\RejectIfReverseProxy; use App\Http\Middleware\RejectIfReverseProxy;
use App\Http\Middleware\SkipIfAuthenticated; use App\Http\Middleware\SkipIfAuthenticated;
use App\Listeners\Authentication\FailedLoginListener;
use App\Listeners\Authentication\LoginListener;
use App\Models\User; use App\Models\User;
use App\Notifications\FailedLogin;
use App\Notifications\SignedInWithNewDevice;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Notification;
use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversClass;
use Tests\FeatureTestCase; use Tests\FeatureTestCase;
@ -21,6 +26,8 @@ use Tests\FeatureTestCase;
#[CoversClass(RejectIfReverseProxy::class)] #[CoversClass(RejectIfReverseProxy::class)]
#[CoversClass(RejectIfDemoMode::class)] #[CoversClass(RejectIfDemoMode::class)]
#[CoversClass(SkipIfAuthenticated::class)] #[CoversClass(SkipIfAuthenticated::class)]
#[CoversClass(LoginListener::class)]
#[CoversClass(FailedLoginListener::class)]
class LoginTest extends FeatureTestCase 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 * @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 * @test
*/ */

View File

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

View File

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

View File

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

View File

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