mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-20 19:57:44 +02:00
Fix & Complete tests
This commit is contained in:
parent
8c89c6f0ab
commit
e238e5121c
@ -18,6 +18,6 @@ class ScanForNewReleaseCalled
|
|||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
Log::debug('ReleaseRadarActivated event dispatched');
|
Log::debug('ReleaseRadarActivated event dispatched'); // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,12 +131,9 @@ class RemoteUserProvider implements UserProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rehash the user's password if required and supported.
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
* @codeCoverageIgnore
|
||||||
* @param array $credentials
|
|
||||||
* @param bool $force
|
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false)
|
public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false)
|
||||||
{
|
{
|
||||||
|
@ -20,8 +20,8 @@ class PasswordController extends Controller
|
|||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
if (config('auth.defaults.guard') === 'reverse-proxy-guard' || $user->oauth_provider) {
|
if ($user->oauth_provider) {
|
||||||
Log::notice('Password update rejected: reverse-proxy-guard enabled or account from external sso provider');
|
Log::notice('Password update rejected: external account from a sso provider');
|
||||||
|
|
||||||
return response()->json(['message' => __('errors.account_managed_by_external_provider')], 400);
|
return response()->json(['message' => __('errors.account_managed_by_external_provider')], 400);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ class CustomCreateFreshApiToken extends CreateFreshApiToken
|
|||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return bool
|
* @return bool
|
||||||
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
*/
|
*/
|
||||||
protected function requestShouldReceiveFreshToken($request)
|
protected function requestShouldReceiveFreshToken($request)
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,8 @@ class ForceJsonResponse
|
|||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return mixed
|
* @return mixed
|
||||||
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,8 @@ class TrustProxies extends Middleware
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* TrustProxies constructor.
|
* TrustProxies constructor.
|
||||||
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
@ -51,7 +51,6 @@ class LoginListener extends AbstractAccessListener
|
|||||||
->whereUserAgent($userAgent)
|
->whereUserAgent($userAgent)
|
||||||
->whereLoginSuccessful(true)
|
->whereLoginSuccessful(true)
|
||||||
->whereGuard($event->guard)
|
->whereGuard($event->guard)
|
||||||
->whereLoginMethod($this->loginMethod())
|
|
||||||
->first();
|
->first();
|
||||||
$newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now(), true) < 1;
|
$newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now(), true) < 1;
|
||||||
|
|
||||||
|
@ -49,7 +49,6 @@ class LogoutListener extends AbstractAccessListener
|
|||||||
->whereIpAddress($ip)
|
->whereIpAddress($ip)
|
||||||
->whereUserAgent($userAgent)
|
->whereUserAgent($userAgent)
|
||||||
->whereGuard($event->guard)
|
->whereGuard($event->guard)
|
||||||
->whereLoginMethod($this->loginMethod())
|
|
||||||
->orderByDesc('login_at')
|
->orderByDesc('login_at')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
@ -28,6 +28,12 @@ use App\Models\AuthLog;
|
|||||||
use Illuminate\Auth\Events\OtherDeviceLogout;
|
use Illuminate\Auth\Events\OtherDeviceLogout;
|
||||||
use TypeError;
|
use TypeError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*
|
||||||
|
* Excluded from test coverage as long as 2FAuth does not offer a logout Other Devices feature
|
||||||
|
* See \Illuminate\Auth\SessionGuard::logoutOtherDevices when the time comes
|
||||||
|
*/
|
||||||
class OtherDeviceLogoutListener extends AbstractAccessListener
|
class OtherDeviceLogoutListener extends AbstractAccessListener
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@ -48,7 +54,6 @@ class OtherDeviceLogoutListener extends AbstractAccessListener
|
|||||||
$authLog = $user->authentications()
|
$authLog = $user->authentications()
|
||||||
->whereIpAddress($ip)
|
->whereIpAddress($ip)
|
||||||
->whereUserAgent($userAgent)
|
->whereUserAgent($userAgent)
|
||||||
->whereLoginMethod($this->loginMethod())
|
|
||||||
->first();
|
->first();
|
||||||
$guard = $event->guard;
|
$guard = $event->guard;
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ use App\Services\SettingService;
|
|||||||
use App\Services\TwoFAccountService;
|
use App\Services\TwoFAccountService;
|
||||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Zxing\QrReader;
|
||||||
|
|
||||||
class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvider
|
class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvider
|
||||||
{
|
{
|
||||||
@ -34,6 +35,10 @@ class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvi
|
|||||||
$this->app->singleton(ReleaseRadarService::class, function () {
|
$this->app->singleton(ReleaseRadarService::class, function () {
|
||||||
return new ReleaseRadarService();
|
return new ReleaseRadarService();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->app->bind(QrReader::class, function ($app, $parameters) {
|
||||||
|
return new QrReader($parameters['imgSource'], $parameters['sourceType']);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,7 +39,11 @@ class QrCodeService
|
|||||||
*/
|
*/
|
||||||
public static function decode(\Illuminate\Http\UploadedFile $file)
|
public static function decode(\Illuminate\Http\UploadedFile $file)
|
||||||
{
|
{
|
||||||
$qrcode = new QrReader($file->get(), QrReader::SOURCE_TYPE_BLOB);
|
$qrcode = app()->make(QrReader::class, [
|
||||||
|
'imgSource' => $file->get(),
|
||||||
|
'sourceType' => QrReader::SOURCE_TYPE_BLOB
|
||||||
|
]);
|
||||||
|
|
||||||
$text = $qrcode->text();
|
$text = $qrcode->text();
|
||||||
|
|
||||||
if (! $text) {
|
if (! $text) {
|
||||||
|
@ -4,12 +4,16 @@ namespace Database\Factories;
|
|||||||
|
|
||||||
use ParagonIE\ConstantTime\Base32;
|
use ParagonIE\ConstantTime\Base32;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||||
*/
|
*/
|
||||||
class AuthLogFactory extends Factory
|
class AuthLogFactory extends Factory
|
||||||
{
|
{
|
||||||
|
public const IP = '127.0.0.1';
|
||||||
|
public const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the model's default state.
|
* Define the model's default state.
|
||||||
*
|
*
|
||||||
@ -18,8 +22,8 @@ class AuthLogFactory extends Factory
|
|||||||
public function definition()
|
public function definition()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'ip_address' => '127.0.0.1',
|
'ip_address' => self::IP,
|
||||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
'user_agent' => self::USER_AGENT,
|
||||||
'login_at' => now(),
|
'login_at' => now(),
|
||||||
'login_successful' => true,
|
'login_successful' => true,
|
||||||
'logout_at' => null,
|
'logout_at' => null,
|
||||||
@ -28,6 +32,20 @@ class AuthLogFactory extends Factory
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model has login at the specified date.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||||
|
*/
|
||||||
|
public function at(Carbon $at)
|
||||||
|
{
|
||||||
|
return $this->state(function (array $attributes) use ($at) {
|
||||||
|
return [
|
||||||
|
'login_at' => $at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate that the model has login before last year.
|
* Indicate that the model has login before last year.
|
||||||
*
|
*
|
||||||
|
8
tests/Classes/UnexpectedEvent.php
Normal file
8
tests/Classes/UnexpectedEvent.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Classes;
|
||||||
|
|
||||||
|
class unexpectedEvent
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
@ -9,10 +9,10 @@ use App\Http\Middleware\LogUserLastSeen;
|
|||||||
use App\Http\Middleware\RejectIfAuthenticated;
|
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\Listeners\Authentication\FailedLoginListener;
|
use App\Listeners\Authentication\FailedLoginListener;
|
||||||
use App\Listeners\Authentication\LoginListener;
|
use App\Listeners\Authentication\LoginListener;
|
||||||
use App\Listeners\Authentication\LogoutListener;
|
use App\Listeners\Authentication\LogoutListener;
|
||||||
|
use App\Listeners\Authentication\VisitedByProxyUserListener;
|
||||||
use App\Listeners\LogNotificationListener;
|
use App\Listeners\LogNotificationListener;
|
||||||
use App\Models\AuthLog;
|
use App\Models\AuthLog;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@ -37,8 +37,8 @@ use Tests\FeatureTestCase;
|
|||||||
#[CoversClass(LoginListener::class)]
|
#[CoversClass(LoginListener::class)]
|
||||||
#[CoversClass(LogoutListener::class)]
|
#[CoversClass(LogoutListener::class)]
|
||||||
#[CoversClass(FailedLoginListener::class)]
|
#[CoversClass(FailedLoginListener::class)]
|
||||||
|
#[CoversClass(VisitedByProxyUserListener::class)]
|
||||||
#[CoversMethod(CaseInsensitiveEmailExists::class, 'validate')]
|
#[CoversMethod(CaseInsensitiveEmailExists::class, 'validate')]
|
||||||
#[CoversMethod(SkipIfAuthenticated::class, 'handle')]
|
|
||||||
#[CoversMethod(Handler::class, 'register')]
|
#[CoversMethod(Handler::class, 'register')]
|
||||||
#[CoversMethod(KickOutInactiveUser::class, 'handle')]
|
#[CoversMethod(KickOutInactiveUser::class, 'handle')]
|
||||||
#[CoversMethod(LogUserLastSeen::class, 'handle')]
|
#[CoversMethod(LogUserLastSeen::class, 'handle')]
|
||||||
@ -59,10 +59,16 @@ class LoginTest extends FeatureTestCase
|
|||||||
|
|
||||||
private const WEB_GUARD = 'web-guard';
|
private const WEB_GUARD = 'web-guard';
|
||||||
|
|
||||||
|
private const REVERSE_PROXY_GUARD = 'reverse-proxy-guard';
|
||||||
|
|
||||||
private const PASSWORD = 'password';
|
private const PASSWORD = 'password';
|
||||||
|
|
||||||
private const WRONG_PASSWORD = 'wrong_password';
|
private const WRONG_PASSWORD = 'wrong_password';
|
||||||
|
|
||||||
|
private const USER_NAME = 'John';
|
||||||
|
|
||||||
|
private const USER_EMAIL = 'john@example.com';
|
||||||
|
|
||||||
public function setUp() : void
|
public function setUp() : void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@ -115,14 +121,14 @@ class LoginTest extends FeatureTestCase
|
|||||||
'email' => $this->user->email,
|
'email' => $this->user->email,
|
||||||
'password' => self::PASSWORD,
|
'password' => self::PASSWORD,
|
||||||
], [
|
], [
|
||||||
'HTTP_USER_AGENT' => 'NotSymfony',
|
'HTTP_USER_AGENT' => 'another_useragent_to_be_identified_as_new_device',
|
||||||
])->assertOk();
|
])->assertOk();
|
||||||
|
|
||||||
Notification::assertSentTo($this->user, SignedInWithNewDeviceNotification::class);
|
Notification::assertSentTo($this->user, SignedInWithNewDeviceNotification::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_login_does_not_send_new_device_notification()
|
public function test_login_does_not_send_new_device_notification_if_user_disabled_it()
|
||||||
{
|
{
|
||||||
Notification::fake();
|
Notification::fake();
|
||||||
|
|
||||||
@ -143,7 +149,23 @@ class LoginTest extends FeatureTestCase
|
|||||||
'email' => $this->user->email,
|
'email' => $this->user->email,
|
||||||
'password' => self::PASSWORD,
|
'password' => self::PASSWORD,
|
||||||
], [
|
], [
|
||||||
'HTTP_USER_AGENT' => 'NotSymfony',
|
'HTTP_USER_AGENT' => 'another_useragent_to_be_identified_as_new_device',
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
Notification::assertNothingSentTo($this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_login_does_not_send_new_device_notification_if_user_is_considered_new()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->user['preferences->notifyOnNewAuthDevice'] = 1;
|
||||||
|
$this->user->save();
|
||||||
|
|
||||||
|
$this->json('POST', '/user/login', [
|
||||||
|
'email' => $this->user->email,
|
||||||
|
'password' => self::PASSWORD,
|
||||||
])->assertOk();
|
])->assertOk();
|
||||||
|
|
||||||
Notification::assertNothingSentTo($this->user);
|
Notification::assertNothingSentTo($this->user);
|
||||||
@ -366,7 +388,7 @@ class LoginTest extends FeatureTestCase
|
|||||||
->json('GET', '/user/logout')
|
->json('GET', '/user/logout')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
$authlog = AuthLog::first();
|
$authlog = $this->user->latestAuthentication()->first();
|
||||||
|
|
||||||
$this->assertEquals($this->user->id, $authlog->authenticatable_id);
|
$this->assertEquals($this->user->id, $authlog->authenticatable_id);
|
||||||
$this->assertTrue($authlog->login_successful);
|
$this->assertTrue($authlog->login_successful);
|
||||||
@ -390,4 +412,138 @@ class LoginTest extends FeatureTestCase
|
|||||||
$this->assertNull($authlog->login_method);
|
$this->assertNull($authlog->login_method);
|
||||||
$this->assertNotNull($authlog->logout_at);
|
$this->assertNotNull($authlog->logout_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_reverse_proxy_access_is_logged()
|
||||||
|
{
|
||||||
|
Config::set('auth.auth_proxy_headers.user', 'HTTP_REMOTE_USER');
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => strtolower(self::USER_NAME) . '@remote',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->app['auth']->shouldUse(self::REVERSE_PROXY_GUARD);
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('auth_logs', [
|
||||||
|
'authenticatable_id' => $user->id,
|
||||||
|
'login_successful' => true,
|
||||||
|
'guard' => self::REVERSE_PROXY_GUARD,
|
||||||
|
'login_method' => null,
|
||||||
|
'logout_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_reverse_proxy_access_is_logged_only_once_during_a_quarter()
|
||||||
|
{
|
||||||
|
Config::set('auth.auth_proxy_headers.user', 'HTTP_REMOTE_USER');
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => strtolower(self::USER_NAME) . '@remote',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->app['auth']->shouldUse(self::REVERSE_PROXY_GUARD);
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('auth_logs', 1);
|
||||||
|
|
||||||
|
$this->travel(16)->minutes();
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('auth_logs', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_reverse_proxy_access_sends_new_device_notification()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
Config::set('auth.auth_proxy_headers.user', 'HTTP_REMOTE_USER');
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => strtolower(self::USER_NAME) . '@remote',
|
||||||
|
]);
|
||||||
|
$user['preferences->notifyOnNewAuthDevice'] = true;
|
||||||
|
$user->save();
|
||||||
|
$user->refresh();
|
||||||
|
|
||||||
|
$this->app['auth']->shouldUse(self::REVERSE_PROXY_GUARD);
|
||||||
|
|
||||||
|
// We travel back for 2 minutes to avoid the user being considered as a new user
|
||||||
|
$this->travelTo(Carbon::now()->subMinutes(2));
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Notification::assertSentTo($user, SignedInWithNewDeviceNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_reverse_proxy_access_does_not_send_new_device_notification_if_user_disabled_it()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
Config::set('auth.auth_proxy_headers.user', 'HTTP_REMOTE_USER');
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => strtolower(self::USER_NAME) . '@remote',
|
||||||
|
]);
|
||||||
|
$user['preferences->notifyOnNewAuthDevice'] = false;
|
||||||
|
$user->save();
|
||||||
|
$user->refresh();
|
||||||
|
|
||||||
|
$this->app['auth']->shouldUse(self::REVERSE_PROXY_GUARD);
|
||||||
|
|
||||||
|
// We travel back for 2 minutes to avoid the user being considered as a new user
|
||||||
|
$this->travelTo(Carbon::now()->subMinutes(2));
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Notification::assertNothingSentTo($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_reverse_proxy_does_not_send_new_device_notification_if_user_is_considered_new()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
Config::set('auth.auth_proxy_headers.user', 'HTTP_REMOTE_USER');
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => strtolower(self::USER_NAME) . '@remote',
|
||||||
|
]);
|
||||||
|
$user['preferences->notifyOnNewAuthDevice'] = true;
|
||||||
|
$user->save();
|
||||||
|
$user->refresh();
|
||||||
|
|
||||||
|
$this->app['auth']->shouldUse(self::REVERSE_PROXY_GUARD);
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Notification::assertNothingSentTo($user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ namespace Tests\Feature\Http\Auth;
|
|||||||
|
|
||||||
use App\Http\Controllers\Auth\PasswordController;
|
use App\Http\Controllers\Auth\PasswordController;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\FeatureTestCase;
|
use Tests\FeatureTestCase;
|
||||||
@ -23,6 +24,12 @@ class PasswordControllerTest extends FeatureTestCase
|
|||||||
|
|
||||||
private const NEW_PASSWORD = 'newPassword';
|
private const NEW_PASSWORD = 'newPassword';
|
||||||
|
|
||||||
|
private const USER_NAME = 'John';
|
||||||
|
|
||||||
|
private const USER_EMAIL = 'john@example.com';
|
||||||
|
|
||||||
|
private const REVERSE_PROXY_GUARD = 'reverse-proxy-guard';
|
||||||
|
|
||||||
public function setUp() : void
|
public function setUp() : void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@ -71,4 +78,53 @@ class PasswordControllerTest extends FeatureTestCase
|
|||||||
])
|
])
|
||||||
->assertStatus(422);
|
->assertStatus(422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_update_pwd_of_reverse_proxy_user_return_bad_request()
|
||||||
|
{
|
||||||
|
Config::set('auth.auth_proxy_headers.user', 'HTTP_REMOTE_USER');
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => self::USER_EMAIL,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->app['auth']->shouldUse(self::REVERSE_PROXY_GUARD);
|
||||||
|
|
||||||
|
$response = $this->json('PATCH', '/user/password', [
|
||||||
|
'currentPassword' => self::NEW_PASSWORD,
|
||||||
|
'password' => self::NEW_PASSWORD,
|
||||||
|
'password_confirmation' => self::NEW_PASSWORD,
|
||||||
|
], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
])
|
||||||
|
->assertStatus(405)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'message',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_update_pwd_of_oauth_user_return_bad_request()
|
||||||
|
{
|
||||||
|
$this->user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => self::USER_EMAIL,
|
||||||
|
'password' => 'password',
|
||||||
|
'is_admin' => 1,
|
||||||
|
'oauth_id' => '12345',
|
||||||
|
'oauth_provider' => 'github',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($this->user, 'web-guard')
|
||||||
|
->json('PATCH', '/user/password', [
|
||||||
|
'currentPassword' => self::NEW_PASSWORD,
|
||||||
|
'password' => self::NEW_PASSWORD,
|
||||||
|
'password_confirmation' => self::NEW_PASSWORD,
|
||||||
|
])
|
||||||
|
->assertStatus(400)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'message',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,11 @@ namespace Tests\Feature\Http\Auth;
|
|||||||
use App\Facades\Settings;
|
use App\Facades\Settings;
|
||||||
use App\Http\Controllers\Auth\SocialiteController;
|
use App\Http\Controllers\Auth\SocialiteController;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Laravel\Socialite\Facades\Socialite;
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\FeatureTestCase;
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
@ -78,6 +80,53 @@ class SocialiteControllerTest extends FeatureTestCase
|
|||||||
$response->assertRedirect('/error?err=sso_disabled');
|
$response->assertRedirect('/error?err=sso_disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_redirect_returns_error_when_sso_provider_client_id_is_missing()
|
||||||
|
{
|
||||||
|
//Settings::set('enableSso', true);
|
||||||
|
config(['services.github.client_id' => null], true);
|
||||||
|
|
||||||
|
$response = $this->get('/socialite/redirect/github');
|
||||||
|
|
||||||
|
$response->assertRedirect('/error?err=sso_bad_provider_setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_redirect_returns_error_when_sso_provider_client_secret_is_missing()
|
||||||
|
{
|
||||||
|
config(['services.github.client_secret' => null]);
|
||||||
|
|
||||||
|
$response = $this->get('/socialite/redirect/github');
|
||||||
|
|
||||||
|
$response->assertRedirect('/error?err=sso_bad_provider_setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('ssoConfigVarProvider')]
|
||||||
|
public function test_redirect_returns_error_when_openid_provider_client_secret_is_missing($ssoConfigVar)
|
||||||
|
{
|
||||||
|
config(['services.openid.' . $ssoConfigVar => null]);
|
||||||
|
|
||||||
|
$response = $this->get('/socialite/redirect/openid');
|
||||||
|
|
||||||
|
$response->assertRedirect('/error?err=sso_bad_provider_setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function ssoConfigVarProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'TOKEN_URL' => [
|
||||||
|
'token_url'
|
||||||
|
],
|
||||||
|
'AUTHORIZE_URL' => [
|
||||||
|
'authorize_url'
|
||||||
|
],
|
||||||
|
'USERINFO_URL' => [
|
||||||
|
'userinfo_url'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_callback_authenticates_the_user()
|
public function test_callback_authenticates_the_user()
|
||||||
{
|
{
|
||||||
@ -228,6 +277,22 @@ class SocialiteControllerTest extends FeatureTestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_callback_redirects_to_error_when_sso_provider_reject_auth()
|
||||||
|
{
|
||||||
|
$newSocialiteUser = new \Laravel\Socialite\Two\User;
|
||||||
|
$newSocialiteUser->id = 'rejected_id';
|
||||||
|
$newSocialiteUser->name = 'jane';
|
||||||
|
$newSocialiteUser->email = 'jane@provider.com';
|
||||||
|
|
||||||
|
Socialite::shouldReceive('driver->user')
|
||||||
|
->andThrow(new Exception());
|
||||||
|
|
||||||
|
$response = $this->get('/socialite/callback/github', ['driver' => 'github']);
|
||||||
|
|
||||||
|
$response->assertRedirect('/error?err=sso_failed');
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_callback_redirects_to_error_when_registrations_are_closed()
|
public function test_callback_redirects_to_error_when_registrations_are_closed()
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,6 @@ use App\Models\User;
|
|||||||
use App\Notifications\WebauthnRecoveryNotification;
|
use App\Notifications\WebauthnRecoveryNotification;
|
||||||
use App\Providers\AuthServiceProvider;
|
use App\Providers\AuthServiceProvider;
|
||||||
use App\Rules\CaseInsensitiveEmailExists;
|
use App\Rules\CaseInsensitiveEmailExists;
|
||||||
use Illuminate\Support\Facades\Lang;
|
|
||||||
use Illuminate\Support\Facades\Notification;
|
use Illuminate\Support\Facades\Notification;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||||
@ -19,6 +18,7 @@ use Tests\FeatureTestCase;
|
|||||||
/**
|
/**
|
||||||
* WebAuthnDeviceLostControllerTest test class
|
* WebAuthnDeviceLostControllerTest test class
|
||||||
*/
|
*/
|
||||||
|
#[CoversMethod(User::class, 'sendWebauthnRecoveryNotification')]
|
||||||
#[CoversClass(WebAuthnDeviceLostController::class)]
|
#[CoversClass(WebAuthnDeviceLostController::class)]
|
||||||
#[CoversClass(WebauthnRecoveryNotification::class)]
|
#[CoversClass(WebauthnRecoveryNotification::class)]
|
||||||
#[CoversClass(WebauthnCredentialBroker::class)]
|
#[CoversClass(WebauthnCredentialBroker::class)]
|
||||||
|
@ -4,10 +4,13 @@ namespace Tests\Feature\Http\Auth;
|
|||||||
|
|
||||||
use App\Extensions\WebauthnTwoFAuthUserProvider;
|
use App\Extensions\WebauthnTwoFAuthUserProvider;
|
||||||
use App\Http\Controllers\Auth\WebAuthnLoginController;
|
use App\Http\Controllers\Auth\WebAuthnLoginController;
|
||||||
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\SignedInWithNewDeviceNotification;
|
||||||
use Illuminate\Support\Facades\Config;
|
use Illuminate\Support\Facades\Config;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
|
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
|
||||||
use Laragear\WebAuthn\Enums\UserVerification;
|
use Laragear\WebAuthn\Enums\UserVerification;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
@ -21,19 +24,15 @@ use Tests\FeatureTestCase;
|
|||||||
#[CoversClass(WebAuthnLoginController::class)]
|
#[CoversClass(WebAuthnLoginController::class)]
|
||||||
#[CoversClass(User::class)]
|
#[CoversClass(User::class)]
|
||||||
#[CoversClass(WebauthnTwoFAuthUserProvider::class)]
|
#[CoversClass(WebauthnTwoFAuthUserProvider::class)]
|
||||||
#[CoversMethod(SkipIfAuthenticated::class, 'handle')]
|
#[CoversClass(LoginListener::class)]
|
||||||
|
#[CoversClass(FailedLoginListener::class)]
|
||||||
class WebAuthnLoginControllerTest extends FeatureTestCase
|
class WebAuthnLoginControllerTest extends FeatureTestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var \App\Models\User
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
*/
|
*/
|
||||||
protected $user;
|
protected $user;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \App\Models\User
|
|
||||||
*/
|
|
||||||
protected $admin;
|
|
||||||
|
|
||||||
const CREDENTIAL_ID = 's06aG41wsIYh5X1YUhB-SlH8y3F2RzdJZVse8iXRXOCd3oqQdEyCOsBawzxrYBtJRQA2azAMEN_q19TUp6iMgg';
|
const CREDENTIAL_ID = 's06aG41wsIYh5X1YUhB-SlH8y3F2RzdJZVse8iXRXOCd3oqQdEyCOsBawzxrYBtJRQA2azAMEN_q19TUp6iMgg';
|
||||||
|
|
||||||
const CREDENTIAL_ID_ALT = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg';
|
const CREDENTIAL_ID_ALT = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg';
|
||||||
@ -48,6 +47,10 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
|
|
||||||
const EMAIL = 'john.doe@example.com';
|
const EMAIL = 'john.doe@example.com';
|
||||||
|
|
||||||
|
private const GUARD = 'web-guard';
|
||||||
|
|
||||||
|
private const AUTH_METHOD = 'webauthn';
|
||||||
|
|
||||||
const ASSERTION_RESPONSE = [
|
const ASSERTION_RESPONSE = [
|
||||||
'id' => self::CREDENTIAL_ID_ALT,
|
'id' => self::CREDENTIAL_ID_ALT,
|
||||||
'rawId' => self::CREDENTIAL_ID_ALT_RAW,
|
'rawId' => self::CREDENTIAL_ID_ALT_RAW,
|
||||||
@ -94,37 +97,19 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
DB::table('users')->delete();
|
DB::table('users')->delete();
|
||||||
|
|
||||||
|
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||||
|
|
||||||
|
$this->mock(AssertionValidator::class)
|
||||||
|
->shouldReceive('send->thenReturn')
|
||||||
|
->andReturn();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_webauthn_login_returns_success()
|
public function test_webauthn_login_returns_success()
|
||||||
{
|
{
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
DB::table('webauthn_credentials')->insert([
|
|
||||||
'id' => self::CREDENTIAL_ID_ALT,
|
|
||||||
'authenticatable_type' => \App\Models\User::class,
|
|
||||||
'authenticatable_id' => $this->user->id,
|
|
||||||
'user_id' => self::USER_ID_ALT,
|
|
||||||
'counter' => 0,
|
|
||||||
'rp_id' => 'http://localhost',
|
|
||||||
'origin' => 'http://localhost',
|
|
||||||
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
|
||||||
'attestation_format' => 'none',
|
|
||||||
'public_key' => self::PUBLIC_KEY,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
|
||||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
|
||||||
60,
|
|
||||||
false,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
$this->mock(AssertionValidator::class)
|
|
||||||
->expects('send->thenReturn')
|
|
||||||
->andReturn();
|
|
||||||
|
|
||||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||||
->assertOk()
|
->assertOk()
|
||||||
@ -140,35 +125,74 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_webauthn_login_sends_new_device_notification_to_existing_user()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->user['preferences->notifyOnNewAuthDevice'] = 1;
|
||||||
|
$this->user->save();
|
||||||
|
|
||||||
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
|
|
||||||
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->actingAs($this->user, self::GUARD)
|
||||||
|
->json('GET', '/user/logout');
|
||||||
|
|
||||||
|
$this->travel(1)->minute();
|
||||||
|
|
||||||
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE, [
|
||||||
|
'HTTP_USER_AGENT' => 'another_useragent_to_be_identified_as_new_device',
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
Notification::assertSentTo($this->user, SignedInWithNewDeviceNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_webauthn_login_does_not_send_new_device_notification_to_new_user()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->user['preferences->notifyOnNewAuthDevice'] = 1;
|
||||||
|
$this->user->save();
|
||||||
|
|
||||||
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
|
|
||||||
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
Notification::assertNothingSentTo($this->user);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_webauthn_login_does_not_send_new_device_notification_if_user_disabled_it()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->user['preferences->notifyOnNewAuthDevice'] =01;
|
||||||
|
$this->user->save();
|
||||||
|
|
||||||
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
|
|
||||||
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
Notification::assertNothingSentTo($this->user);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_webauthn_admin_login_returns_admin_role()
|
public function test_webauthn_admin_login_returns_admin_role()
|
||||||
{
|
{
|
||||||
$this->admin = User::factory()->administrator()->create(['email' => self::EMAIL]);
|
DB::table('users')->delete();
|
||||||
|
$this->user = User::factory()->administrator()->create(['email' => self::EMAIL]);
|
||||||
|
|
||||||
DB::table('webauthn_credentials')->insert([
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
'id' => self::CREDENTIAL_ID_ALT,
|
$this->addWebauthnChallengeToSession();
|
||||||
'authenticatable_type' => \App\Models\User::class,
|
|
||||||
'authenticatable_id' => $this->admin->id,
|
|
||||||
'user_id' => self::USER_ID_ALT,
|
|
||||||
'counter' => 0,
|
|
||||||
'rp_id' => 'http://localhost',
|
|
||||||
'origin' => 'http://localhost',
|
|
||||||
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
|
||||||
'attestation_format' => 'none',
|
|
||||||
'public_key' => self::PUBLIC_KEY,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
|
||||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
|
||||||
60,
|
|
||||||
false,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
$this->mock(AssertionValidator::class)
|
|
||||||
->expects('send->thenReturn')
|
|
||||||
->andReturn();
|
|
||||||
|
|
||||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||||
->assertOk()
|
->assertOk()
|
||||||
@ -180,32 +204,8 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
#[Test]
|
#[Test]
|
||||||
public function test_webauthn_login_merge_handle_if_missing()
|
public function test_webauthn_login_merge_handle_if_missing()
|
||||||
{
|
{
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
DB::table('webauthn_credentials')->insert([
|
|
||||||
'id' => self::CREDENTIAL_ID_ALT,
|
|
||||||
'authenticatable_type' => \App\Models\User::class,
|
|
||||||
'authenticatable_id' => $this->user->id,
|
|
||||||
'user_id' => self::USER_ID_ALT,
|
|
||||||
'counter' => 0,
|
|
||||||
'rp_id' => 'http://localhost',
|
|
||||||
'origin' => 'http://localhost',
|
|
||||||
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
|
||||||
'attestation_format' => 'none',
|
|
||||||
'public_key' => self::PUBLIC_KEY,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
|
||||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
|
||||||
60,
|
|
||||||
false,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
$this->mock(AssertionValidator::class)
|
|
||||||
->expects('send->thenReturn')
|
|
||||||
->andReturn();
|
|
||||||
|
|
||||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_NO_HANDLE)
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_NO_HANDLE)
|
||||||
->assertOk()
|
->assertOk()
|
||||||
@ -223,10 +223,6 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
#[Test]
|
#[Test]
|
||||||
public function test_legacy_login_is_rejected_when_webauthn_only_is_enable()
|
public function test_legacy_login_is_rejected_when_webauthn_only_is_enable()
|
||||||
{
|
{
|
||||||
$this->user = User::factory()->create([
|
|
||||||
'email' => self::EMAIL,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Set to webauthn only
|
// Set to webauthn only
|
||||||
$this->user['preferences->useWebauthnOnly'] = true;
|
$this->user['preferences->useWebauthnOnly'] = true;
|
||||||
$this->user->save();
|
$this->user->save();
|
||||||
@ -241,32 +237,8 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
#[Test]
|
#[Test]
|
||||||
public function test_webauthn_login_already_authenticated_is_rejected()
|
public function test_webauthn_login_already_authenticated_is_rejected()
|
||||||
{
|
{
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
DB::table('webauthn_credentials')->insert([
|
|
||||||
'id' => self::CREDENTIAL_ID_ALT,
|
|
||||||
'authenticatable_type' => \App\Models\User::class,
|
|
||||||
'authenticatable_id' => $this->user->id,
|
|
||||||
'user_id' => self::USER_ID_ALT,
|
|
||||||
'counter' => 0,
|
|
||||||
'rp_id' => 'http://localhost',
|
|
||||||
'origin' => 'http://localhost',
|
|
||||||
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
|
||||||
'attestation_format' => 'none',
|
|
||||||
'public_key' => self::PUBLIC_KEY,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
|
||||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
|
||||||
60,
|
|
||||||
false,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
$this->mock(AssertionValidator::class)
|
|
||||||
->expects('send->thenReturn')
|
|
||||||
->andReturn();
|
|
||||||
|
|
||||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||||
->assertOk();
|
->assertOk();
|
||||||
@ -281,8 +253,6 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
#[Test]
|
#[Test]
|
||||||
public function test_webauthn_login_with_missing_data_returns_validation_error()
|
public function test_webauthn_login_with_missing_data_returns_validation_error()
|
||||||
{
|
{
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'id' => '',
|
'id' => '',
|
||||||
'rawId' => '',
|
'rawId' => '',
|
||||||
@ -310,13 +280,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
#[Test]
|
#[Test]
|
||||||
public function test_webauthn_invalid_login_returns_unauthorized()
|
public function test_webauthn_invalid_login_returns_unauthorized()
|
||||||
{
|
{
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->addWebauthnChallengeToSession();
|
||||||
|
|
||||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
|
||||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
|
||||||
60,
|
|
||||||
false,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
|
||||||
->assertUnauthorized();
|
->assertUnauthorized();
|
||||||
@ -328,13 +292,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
$throttle = 8;
|
$throttle = 8;
|
||||||
Config::set('auth.throttle.login', $throttle);
|
Config::set('auth.throttle.login', $throttle);
|
||||||
|
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->addWebauthnChallengeToSession();
|
||||||
|
|
||||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
|
||||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
|
||||||
60,
|
|
||||||
false,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
for ($i = 0; $i < $throttle - 1; $i++) {
|
for ($i = 0; $i < $throttle - 1; $i++) {
|
||||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
|
||||||
@ -352,22 +310,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
{
|
{
|
||||||
Config::set('webauthn.user_verification', UserVerification::PREFERRED);
|
Config::set('webauthn.user_verification', UserVerification::PREFERRED);
|
||||||
|
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
|
||||||
|
|
||||||
DB::table('webauthn_credentials')->insert([
|
|
||||||
'id' => self::CREDENTIAL_ID,
|
|
||||||
'authenticatable_type' => \App\Models\User::class,
|
|
||||||
'authenticatable_id' => $this->user->id,
|
|
||||||
'user_id' => self::USER_ID,
|
|
||||||
'counter' => 0,
|
|
||||||
'rp_id' => 'http://localhost',
|
|
||||||
'origin' => 'http://localhost',
|
|
||||||
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
|
||||||
'attestation_format' => 'none',
|
|
||||||
'public_key' => self::PUBLIC_KEY,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->json('POST', '/webauthn/login/options', [
|
$response = $this->json('POST', '/webauthn/login/options', [
|
||||||
'email' => $this->user->email,
|
'email' => $this->user->email,
|
||||||
@ -390,22 +333,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
{
|
{
|
||||||
Config::set('webauthn.user_verification', UserVerification::REQUIRED);
|
Config::set('webauthn.user_verification', UserVerification::REQUIRED);
|
||||||
|
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
|
||||||
|
|
||||||
DB::table('webauthn_credentials')->insert([
|
|
||||||
'id' => self::CREDENTIAL_ID,
|
|
||||||
'authenticatable_type' => \App\Models\User::class,
|
|
||||||
'authenticatable_id' => $this->user->id,
|
|
||||||
'user_id' => self::USER_ID,
|
|
||||||
'counter' => 0,
|
|
||||||
'rp_id' => 'http://localhost',
|
|
||||||
'origin' => 'http://localhost',
|
|
||||||
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
|
||||||
'attestation_format' => 'none',
|
|
||||||
'public_key' => self::PUBLIC_KEY,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->json('POST', '/webauthn/login/options', [
|
$response = $this->json('POST', '/webauthn/login/options', [
|
||||||
'email' => $this->user->email,
|
'email' => $this->user->email,
|
||||||
@ -430,22 +358,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
{
|
{
|
||||||
Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
|
Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
|
||||||
|
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
$this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
|
||||||
|
|
||||||
DB::table('webauthn_credentials')->insert([
|
|
||||||
'id' => self::CREDENTIAL_ID,
|
|
||||||
'authenticatable_type' => \App\Models\User::class,
|
|
||||||
'authenticatable_id' => $this->user->id,
|
|
||||||
'user_id' => self::USER_ID,
|
|
||||||
'counter' => 0,
|
|
||||||
'rp_id' => 'http://localhost',
|
|
||||||
'origin' => 'http://localhost',
|
|
||||||
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
|
||||||
'attestation_format' => 'none',
|
|
||||||
'public_key' => self::PUBLIC_KEY,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->json('POST', '/webauthn/login/options', [
|
$response = $this->json('POST', '/webauthn/login/options', [
|
||||||
'email' => $this->user->email,
|
'email' => $this->user->email,
|
||||||
@ -468,8 +381,6 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
#[Test]
|
#[Test]
|
||||||
public function test_get_options_with_capitalized_email_returns_success()
|
public function test_get_options_with_capitalized_email_returns_success()
|
||||||
{
|
{
|
||||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
|
||||||
|
|
||||||
$this->json('POST', '/webauthn/login/options', [
|
$this->json('POST', '/webauthn/login/options', [
|
||||||
'email' => strtoupper($this->user->email),
|
'email' => strtoupper($this->user->email),
|
||||||
])
|
])
|
||||||
@ -511,4 +422,72 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||||||
'email',
|
'email',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_successful_webauthn_login_is_logged()
|
||||||
|
{
|
||||||
|
$this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
|
|
||||||
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('auth_logs', [
|
||||||
|
'authenticatable_id' => $this->user->id,
|
||||||
|
'login_successful' => true,
|
||||||
|
'guard' => self::GUARD,
|
||||||
|
'login_method' => self::AUTH_METHOD,
|
||||||
|
'logout_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_failed_webauthn_login_is_not_logged()
|
||||||
|
{
|
||||||
|
$this->addWebauthnChallengeToSession();
|
||||||
|
|
||||||
|
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
|
||||||
|
->assertUnauthorized();
|
||||||
|
|
||||||
|
// When webauthn fails, the fireFailedEvent() of the sessionGuard returns
|
||||||
|
// a null $user so nothing should be logged
|
||||||
|
$this->assertDatabaseMissing('auth_logs', [
|
||||||
|
'authenticatable_id' => $this->user->id,
|
||||||
|
'guard' => self::GUARD,
|
||||||
|
'login_method' => self::AUTH_METHOD,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a session
|
||||||
|
*/
|
||||||
|
protected function addWebauthnChallengeToSession() : void
|
||||||
|
{
|
||||||
|
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
||||||
|
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
||||||
|
60,
|
||||||
|
false,
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a webauthn credential in database
|
||||||
|
*/
|
||||||
|
protected function createWebauthnCredential(string $credentialId, int $authenticatableId, string $userId) : void
|
||||||
|
{
|
||||||
|
DB::table('webauthn_credentials')->insert([
|
||||||
|
'id' => $credentialId,
|
||||||
|
'authenticatable_type' => \App\Models\User::class,
|
||||||
|
'authenticatable_id' => $authenticatableId,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'counter' => 0,
|
||||||
|
'rp_id' => 'http://localhost',
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
||||||
|
'attestation_format' => 'none',
|
||||||
|
'public_key' => self::PUBLIC_KEY,
|
||||||
|
'updated_at' => now(),
|
||||||
|
'created_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,11 @@ use App\Http\Middleware\AdminOnly;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\FeatureTestCase;
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
#[CoversClass(AdminOnly::class)]
|
||||||
class AdminOnlyMiddlewareTest extends FeatureTestCase
|
class AdminOnlyMiddlewareTest extends FeatureTestCase
|
||||||
{
|
{
|
||||||
#[Test]
|
#[Test]
|
||||||
|
@ -2,10 +2,18 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Http\Middlewares;
|
namespace Tests\Feature\Http\Middlewares;
|
||||||
|
|
||||||
|
use App\Http\Middleware\Authenticate;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Providers\AuthServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\Config;
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\FeatureTestCase;
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
#[CoversClass(Authenticate::class)]
|
||||||
|
#[CoversMethod(AuthServiceProvider::class, 'boot')]
|
||||||
class AuthenticateMiddlewareTest extends FeatureTestCase
|
class AuthenticateMiddlewareTest extends FeatureTestCase
|
||||||
{
|
{
|
||||||
private const USER_NAME = 'John';
|
private const USER_NAME = 'John';
|
||||||
@ -45,4 +53,27 @@ class AuthenticateMiddlewareTest extends FeatureTestCase
|
|||||||
$this->json('GET', '/api/v1/groups', [], [])
|
$this->json('GET', '/api/v1/groups', [], [])
|
||||||
->assertStatus(407);
|
->assertStatus(407);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_overrides_locale_when_auth_is_successful()
|
||||||
|
{
|
||||||
|
Config::set('auth.auth_proxy_headers.user', 'HTTP_REMOTE_USER');
|
||||||
|
Config::set('auth.auth_proxy_headers.email', 'HTTP_REMOTE_EMAIL');
|
||||||
|
|
||||||
|
$this->app['auth']->shouldUse('reverse-proxy-guard');
|
||||||
|
|
||||||
|
$lang = 'fr';
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => self::USER_NAME,
|
||||||
|
'email' => self::USER_EMAIL,
|
||||||
|
]);
|
||||||
|
$user['preferences->lang'] = $lang;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$this->json('GET', '/api/v1/groups', [], [
|
||||||
|
'HTTP_REMOTE_USER' => self::USER_NAME,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($lang, App::getLocale());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
155
tests/Feature/Http/Middlewares/SetLanguageMiddlewareTest.php
Normal file
155
tests/Feature/Http/Middlewares/SetLanguageMiddlewareTest.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Http\Middlewares;
|
||||||
|
|
||||||
|
use App\Http\Middleware\SetLanguage;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
#[CoversClass(SetLanguage::class)]
|
||||||
|
class SetLanguageMiddlewareTest extends TestCase
|
||||||
|
{
|
||||||
|
const IS_FR = 'fr';
|
||||||
|
|
||||||
|
const IS_FR_WITH_VARIANT = 'fr-CA';
|
||||||
|
|
||||||
|
const IS_DE = 'de';
|
||||||
|
|
||||||
|
const UNSUPPORTED_LANGUAGE = 'yy';
|
||||||
|
|
||||||
|
const UNSUPPORTED_LANGUAGES = 'yy, ww';
|
||||||
|
|
||||||
|
const ACCEPTED_LANGUAGES_DE_FR = 'de, fr';
|
||||||
|
|
||||||
|
const ACCEPTED_LANGUAGES_UNSUPPORTED_DE = 'yy, de';
|
||||||
|
|
||||||
|
const ACCEPTED_LANGUAGES_WEIGHTED_DE_FR = 'fr;q=0.4, de;q=0.7';
|
||||||
|
|
||||||
|
const ACCEPTED_LANGUAGES_WEIGHTED_FR_VARIANTS = 'fr;q=0.4, fr-CA;q=0.7, de;q=0.5';
|
||||||
|
|
||||||
|
const ACCEPTED_LANGUAGES_WEIGHTED_ALL_DE_FR = 'fr;q=0.4, de;q=0.7, *;q=0.9';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
|
*/
|
||||||
|
protected $user;
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_fallback_locale()
|
||||||
|
{
|
||||||
|
Config::set('app.fallback_locale', self::IS_FR);
|
||||||
|
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => null]);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_FR, App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_fallback_locale_if_header_ask_for_unsupported()
|
||||||
|
{
|
||||||
|
Config::set('app.fallback_locale', self::IS_FR);
|
||||||
|
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => self::UNSUPPORTED_LANGUAGE]);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_FR, App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_fallback_locale_if_header_ask_for_several_unsupported()
|
||||||
|
{
|
||||||
|
Config::set('app.fallback_locale', self::IS_FR);
|
||||||
|
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => self::UNSUPPORTED_LANGUAGES]);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_FR, App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_fallback_locale_if_header_ask_for_wildcard()
|
||||||
|
{
|
||||||
|
Config::set('app.fallback_locale', self::IS_FR);
|
||||||
|
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => '*']);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_FR, App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_accepted_language_from_header()
|
||||||
|
{
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => self::IS_FR]);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_FR, App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_first_accepted_language_from_header()
|
||||||
|
{
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => self::ACCEPTED_LANGUAGES_DE_FR]);
|
||||||
|
|
||||||
|
$this->assertEquals('de', App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_heaviest_language_from_header()
|
||||||
|
{
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => self::ACCEPTED_LANGUAGES_WEIGHTED_DE_FR]);
|
||||||
|
|
||||||
|
$this->assertEquals('de', App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_applies_heaviest_language_with_variant_from_header()
|
||||||
|
{
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => self::ACCEPTED_LANGUAGES_WEIGHTED_FR_VARIANTS]);
|
||||||
|
|
||||||
|
$this->assertEquals('fr', App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_it_ignores_unsupported_language_from_header()
|
||||||
|
{
|
||||||
|
$this->json('GET', '/', [], ['Accept-Language' => self::ACCEPTED_LANGUAGES_UNSUPPORTED_DE]);
|
||||||
|
|
||||||
|
$this->assertEquals('de', App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_user_preference_overrides_header()
|
||||||
|
{
|
||||||
|
$this->user = new User;
|
||||||
|
$this->user['preferences->lang'] = self::IS_FR;
|
||||||
|
|
||||||
|
$this->actingAs($this->user)->json('GET', '/', [], ['Accept-Language' => self::IS_DE]);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_FR, App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_user_preference_applies_header()
|
||||||
|
{
|
||||||
|
$this->user = new User;
|
||||||
|
$this->user['preferences->lang'] = 'browser';
|
||||||
|
|
||||||
|
$this->actingAs($this->user)->json('GET', '/', [], ['Accept-Language' => self::IS_DE]);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_DE, App::getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_user_preference_overrides_fallback()
|
||||||
|
{
|
||||||
|
Config::set('app.fallback_locale', self::IS_DE);
|
||||||
|
|
||||||
|
$this->user = new User;
|
||||||
|
$this->user['preferences->lang'] = self::IS_FR;
|
||||||
|
|
||||||
|
$this->actingAs($this->user)->json('GET', '/', [], ['Accept-Language' => null]);
|
||||||
|
|
||||||
|
$this->assertEquals(self::IS_FR, App::getLocale());
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,8 @@ use App\Http\Controllers\SystemController;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\TestEmailSettingNotification;
|
use App\Notifications\TestEmailSettingNotification;
|
||||||
use App\Services\ReleaseRadarService;
|
use App\Services\ReleaseRadarService;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Lang;
|
use Illuminate\Contracts\Notifications\Dispatcher;
|
||||||
use Illuminate\Support\Facades\Notification;
|
use Illuminate\Support\Facades\Notification;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
@ -138,6 +138,21 @@ class SystemControllerTest extends FeatureTestCase
|
|||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_testEmail_returns_success_even_if_sending_fails()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->mock(Dispatcher::class)->shouldReceive('send')->andThrow(new Exception());
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->admin, 'web-guard')
|
||||||
|
->json('POST', '/system/test-email', []);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
Notification::assertNothingSentTo($this->admin);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_clearCache_returns_success()
|
public function test_clearCache_returns_success()
|
||||||
{
|
{
|
||||||
|
25
tests/Feature/Models/AuthLogModelTest.php
Normal file
25
tests/Feature/Models/AuthLogModelTest.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Models;
|
||||||
|
|
||||||
|
use App\Models\AuthLog;
|
||||||
|
use App\Models\User;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserModelTest test class
|
||||||
|
*/
|
||||||
|
#[CoversClass(AuthLog::class)]
|
||||||
|
class AuthLogModelTest extends FeatureTestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function test_equals_returns_true()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$lastAuthLog = AuthLog::factory()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$this->assertTrue($lastAuthLog->equals($lastAuthLog));
|
||||||
|
}
|
||||||
|
}
|
26
tests/Feature/Models/GroupModelTest.php
Normal file
26
tests/Feature/Models/GroupModelTest.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Models;
|
||||||
|
|
||||||
|
use App\Models\Group;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GroupModelTest test class
|
||||||
|
*/
|
||||||
|
#[CoversClass(Group::class)]
|
||||||
|
class GroupModelTest extends FeatureTestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function test_scopeOrphans_retreives_accounts_without_owner()
|
||||||
|
{
|
||||||
|
$orphan = Group::factory()->create();
|
||||||
|
|
||||||
|
$orphans = Group::orphans()->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $orphans);
|
||||||
|
$this->assertEquals($orphan->id, $orphans[0]->id);
|
||||||
|
}
|
||||||
|
}
|
@ -731,6 +731,19 @@ class TwoFAccountModelTest extends FeatureTestCase
|
|||||||
Storage::disk('imagesLink')->assertMissing($this->customTotpTwofaccount->icon);
|
Storage::disk('imagesLink')->assertMissing($this->customTotpTwofaccount->icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_scopeOrphans_retreives_accounts_without_owner()
|
||||||
|
{
|
||||||
|
$orphan = new TwoFAccount;
|
||||||
|
$orphan->fillWithURI(OtpTestData::HOTP_FULL_CUSTOM_URI);
|
||||||
|
$orphan->save();
|
||||||
|
|
||||||
|
$orphans = TwoFAccount::orphans()->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $orphans);
|
||||||
|
$this->assertEquals($orphan->id, $orphans[0]->id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide data for Icon store tests
|
* Provide data for Icon store tests
|
||||||
*/
|
*/
|
||||||
|
@ -7,6 +7,7 @@ use App\Models\Group;
|
|||||||
use App\Models\TwoFAccount;
|
use App\Models\TwoFAccount;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Observers\UserObserver;
|
use App\Observers\UserObserver;
|
||||||
|
use Database\Factories\AuthLogFactory;
|
||||||
use Illuminate\Auth\Events\PasswordReset;
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@ -187,4 +188,264 @@ class UserModelTest extends FeatureTestCase
|
|||||||
|
|
||||||
$this->assertFalse($isDeleted);
|
$this->assertFalse($isDeleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_getFromCredentialId_retreives_the_user()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
DB::table('webauthn_credentials')->insert([
|
||||||
|
'id' => '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg',
|
||||||
|
'authenticatable_type' => \App\Models\User::class,
|
||||||
|
'authenticatable_id' => $user->id,
|
||||||
|
'user_id' => 'e8af6f703f8042aa91c30cf72289aa07',
|
||||||
|
'counter' => 0,
|
||||||
|
'rp_id' => 'http://localhost',
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'aaguid' => '00000000-0000-0000-0000-000000000000',
|
||||||
|
'attestation_format' => 'none',
|
||||||
|
'public_key' => 'eyJpdiI6Imp0U0NVeFNNbW45KzEvMXpad2p2SUE9PSIsInZhbHVlIjoic0VxZ2I1WnlHM2lJakhkWHVkK2kzMWtibk1IN2ZlaExGT01qOElXMDdRTjhnVlR0TDgwOHk1S0xQUy9BQ1JCWHRLNzRtenNsMml1dVQydWtERjFEU0h0bkJGT2RwUXE1M1JCcVpablE2Y2VGV2YvVEE2RGFIRUE5L0x1K0JIQXhLVE1aNVNmN3AxeHdjRUo2V0hwREZSRTJYaThNNnB1VnozMlVXZEVPajhBL3d3ODlkTVN3bW54RTEwSG0ybzRQZFFNNEFrVytUYThub2IvMFRtUlBZamoyZElWKzR1bStZQ1IwU3FXbkYvSm1FU2FlMTFXYUo0SG9kc1BDME9CNUNKeE9IelE5d2dmNFNJRXBKNUdlVzJ3VHUrQWJZRFluK0hib0xvVTdWQ0ZISjZmOWF3by83aVJES1dxbU9Zd1lhRTlLVmhZSUdlWmlBOUFtcTM2ZVBaRWNKNEFSQUhENk5EaC9hN3REdnVFbm16WkRxekRWOXd4cVcvZFdKa2tlWWJqZWlmZnZLS0F1VEVCZEZQcXJkTExiNWRyQmxsZWtaSDRlT3VVS0ZBSXFBRG1JMjRUMnBKRXZxOUFUa2xxMjg2TEplUzdscVo2UytoVU5SdXk1OE1lcFN6aU05ZkVXTkdIM2tKM3Q5bmx1TGtYb1F5bGxxQVR3K3BVUVlia1VybDFKRm9lZDViNzYraGJRdmtUb2FNTEVGZmZYZ3lYRDRiOUVjRnJpcTVvWVExOHJHSTJpMnVBZ3E0TmljbUlKUUtXY2lSWDh1dE5MVDNRUzVRSkQrTjVJUU8rSGhpeFhRRjJvSEdQYjBoVT0iLCJtYWMiOiI5MTdmNWRkZGE5OTEwNzQ3MjhkYWVhYjRlNjk0MWZlMmI5OTQ4YzlmZWI1M2I4OGVkMjE1MjMxNjUwOWRmZTU2IiwidGFnIjoiIn0=',
|
||||||
|
'updated_at' => now(),
|
||||||
|
'created_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$searched = User::getFromCredentialId('-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg');
|
||||||
|
|
||||||
|
$this->assertEquals($user->id, $searched->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_authentications_returns_user_auth_logs_sorted_by_latest_id()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$tenDaysAgoAuthLog = AuthLog::factory()->daysAgo(10)->for($user, 'authenticatable')->create();
|
||||||
|
$fiveDaysAgoAuthLog = AuthLog::factory()->daysAgo(5)->for($user, 'authenticatable')->create();
|
||||||
|
$lastAuthLog = AuthLog::factory()->daysAgo(1)->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$authentications = $user->authentications()->get();
|
||||||
|
|
||||||
|
$this->assertCount(3, $authentications);
|
||||||
|
$this->assertEquals($lastAuthLog->id, $authentications[0]->id);
|
||||||
|
$this->assertEquals($fiveDaysAgoAuthLog->id, $authentications[1]->id);
|
||||||
|
$this->assertEquals($tenDaysAgoAuthLog->id, $authentications[2]->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_authentications_returns_user_auth_logs_only()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$anotherUser = User::factory()->create();
|
||||||
|
|
||||||
|
$userAuthLog = AuthLog::factory()->daysAgo(10)->for($user, 'authenticatable')->create();
|
||||||
|
AuthLog::factory()->daysAgo(5)->for($anotherUser, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$authentications = $user->authentications()->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $authentications);
|
||||||
|
$this->assertEquals($userAuthLog->id, $authentications[0]->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_authenticationsByPeriod_returns_last_month_auth_logs()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$twoMonthsAgoAuthLog = AuthLog::factory()->duringLastThreeMonth()->for($user, 'authenticatable')->create();
|
||||||
|
$duringLastMonthAuthLog = AuthLog::factory()->duringLastMonth()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$authentications = $user->authenticationsByPeriod(1);
|
||||||
|
|
||||||
|
$this->assertCount(1, $authentications);
|
||||||
|
$this->assertEquals($duringLastMonthAuthLog->id, $authentications[0]->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_authenticationsByPeriod_returns_last_three_months_auth_logs()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$sixMonthsAgoAuthLog = AuthLog::factory()->duringLastSixMonth()->for($user, 'authenticatable')->create();
|
||||||
|
$threeMonthsAgoAuthLog = AuthLog::factory()->duringLastThreeMonth()->for($user, 'authenticatable')->create();
|
||||||
|
$duringLastMonthAuthLog = AuthLog::factory()->duringLastMonth()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$authentications = $user->authenticationsByPeriod(3);
|
||||||
|
|
||||||
|
$this->assertCount(2, $authentications);
|
||||||
|
$this->assertEquals($duringLastMonthAuthLog->id, $authentications[0]->id);
|
||||||
|
$this->assertEquals($threeMonthsAgoAuthLog->id, $authentications[1]->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_latestAuthentication_returns_user_latest_auth_logs()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$twoMonthsAgoAuthLog = AuthLog::factory()->duringLastThreeMonth()->for($user, 'authenticatable')->create();
|
||||||
|
$duringLastMonthAuthLog = AuthLog::factory()->duringLastMonth()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$authentications = $user->latestAuthentication()->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $authentications);
|
||||||
|
$this->assertEquals($duringLastMonthAuthLog->id, $authentications[0]->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_latestAuthentication_returns_user_latest_auth_logs_only()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$anotherUser = User::factory()->create();
|
||||||
|
|
||||||
|
$userAuthLog = AuthLog::factory()->duringLastThreeMonth()->for($user, 'authenticatable')->create();
|
||||||
|
$anotherUserAuthLog = AuthLog::factory()->duringLastMonth()->for($anotherUser, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$authentications = $user->latestAuthentication()->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $authentications);
|
||||||
|
$this->assertEquals($userAuthLog->id, $authentications[0]->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastLoginAt_returns_user_last_auth_date()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
$tenDaysAgoAuthLog = AuthLog::factory()->daysAgo(10)->for($user, 'authenticatable')->create();
|
||||||
|
$fiveDaysAgoAuthLog = AuthLog::factory()->daysAgo(5)->for($user, 'authenticatable')->create();
|
||||||
|
$lastAuthLog = AuthLog::factory()->at($now)->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$lastLoginAt = $user->lastLoginAt();
|
||||||
|
|
||||||
|
$this->assertEquals($lastLoginAt->startOfSecond(), $now->startOfSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastLoginAt_returns_null_if_user_has_no_login()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
AuthLog::factory()->logoutOnly()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$lastLoginAt = $user->lastLoginAt();
|
||||||
|
|
||||||
|
$this->assertNull($lastLoginAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastSuccessfulLoginAt_returns_user_last_successful_login_date()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$now = now();
|
||||||
|
AuthLog::factory()->at($now)->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$lastSuccessfulLoginAt = $user->lastSuccessfulLoginAt();
|
||||||
|
|
||||||
|
$this->assertEquals($lastSuccessfulLoginAt->startOfSecond(), $now->startOfSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastSuccessfulLoginAt_returns_null_if_user_has_no_successful_login()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$now = now();
|
||||||
|
AuthLog::factory()->at($now)->failedLogin()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$lastSuccessfulLoginAt = $user->lastSuccessfulLoginAt();
|
||||||
|
|
||||||
|
$this->assertNull($lastSuccessfulLoginAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastLoginIp_returns_user_last_login_ip()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
AuthLog::factory()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$lastLoginIp = $user->lastLoginIp();
|
||||||
|
|
||||||
|
$this->assertEquals(AuthLogFactory::IP, $lastLoginIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastLoginIp_returns_null_if_user_has_no_auth_log()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$lastLoginIp = $user->lastLoginIp();
|
||||||
|
|
||||||
|
$this->assertNull($lastLoginIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastSuccessfulLoginIp_returns_user_last_successful_login_ip()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
AuthLog::factory()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$lastSuccessfulLoginIp = $user->lastSuccessfulLoginIp();
|
||||||
|
|
||||||
|
$this->assertEquals(AuthLogFactory::IP, $lastSuccessfulLoginIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lastSuccessfulLoginIp_returns_null_if_user_has_no_successful_login()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
AuthLog::factory()->failedLogin()->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$lastSuccessfulLoginIp = $user->lastSuccessfulLoginIp();
|
||||||
|
|
||||||
|
$this->assertNull($lastSuccessfulLoginIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_previousLoginAt_returns_user_last_auth_date()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$now = now();
|
||||||
|
$yesterday = now()->subDay();
|
||||||
|
|
||||||
|
$yesterdayAuthLog = AuthLog::factory()->at($yesterday)->for($user, 'authenticatable')->create();
|
||||||
|
$lastAuthLog = AuthLog::factory()->at($now)->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$previousLoginAt = $user->previousLoginAt();
|
||||||
|
|
||||||
|
$this->assertEquals($previousLoginAt->startOfSecond(), $yesterday->startOfSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_previousLoginAt_returns_null_if_user_has_no_auth_log()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$previousLoginAt = $user->previousLoginAt();
|
||||||
|
|
||||||
|
$this->assertNull($previousLoginAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_previousLoginIp_returns_user_last_auth_ip()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$yesterday = now()->subDay();
|
||||||
|
|
||||||
|
AuthLog::factory()->for($user, 'authenticatable')->create();
|
||||||
|
AuthLog::factory()->at($yesterday)->for($user, 'authenticatable')->create();
|
||||||
|
|
||||||
|
$previousLoginIp = $user->previousLoginIp();
|
||||||
|
|
||||||
|
$this->assertEquals(AuthLogFactory::IP, $previousLoginIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_previousLoginIp_returns_null_if_user_has_no_auth_log()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$previousLoginIp = $user->previousLoginIp();
|
||||||
|
|
||||||
|
$this->assertNull($previousLoginIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,30 +3,19 @@
|
|||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Providers\RouteServiceProvider;
|
use App\Providers\RouteServiceProvider;
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\FeatureTestCase;
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
#[CoversClass(RouteServiceProvider::class)]
|
||||||
#[CoversMethod(RouteServiceProvider::class, 'boot')]
|
|
||||||
class RouteTest extends FeatureTestCase
|
class RouteTest extends FeatureTestCase
|
||||||
{
|
{
|
||||||
const API_ROUTE_PREFIX = 'api/v1';
|
const API_ROUTE_PREFIX = 'api/v1';
|
||||||
const API_MIDDLEWARE = 'api.v1';
|
const API_MIDDLEWARE = 'api.v1';
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function test_landing_view_is_returned()
|
|
||||||
{
|
|
||||||
$response = $this->get(route('landing', ['any' => '/']));
|
|
||||||
|
|
||||||
$response->assertSuccessful()
|
|
||||||
->assertViewIs('landing');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_exception_handler_with_web_route()
|
public function test_exception_handler_with_web_route()
|
||||||
{
|
{
|
||||||
|
@ -162,6 +162,21 @@ class GroupServiceTest extends FeatureTestCase
|
|||||||
Groups::assign([$this->twofaccountOne->id, $this->twofaccountThree->id], $this->user, $this->user->groups()->first());
|
Groups::assign([$this->twofaccountOne->id, $this->twofaccountThree->id], $this->user, $this->user->groups()->first());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_setUser_sets_twfaccounts_user()
|
||||||
|
{
|
||||||
|
$this->groupOne = Group::factory()->create();
|
||||||
|
$this->groupTwo = Group::factory()->create();
|
||||||
|
|
||||||
|
$this->assertEquals(null, $this->groupOne->user_id);
|
||||||
|
$this->assertEquals(null, $this->groupTwo->user_id);
|
||||||
|
|
||||||
|
Groups::setUser(Group::all(), $this->user);
|
||||||
|
|
||||||
|
$this->assertEquals($this->user->id, $this->groupOne->refresh()->user_id);
|
||||||
|
$this->assertEquals($this->user->id, $this->groupTwo->refresh()->user_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_prependTheAllGroup_add_the_group_on_top_of_groups()
|
public function test_prependTheAllGroup_add_the_group_on_top_of_groups()
|
||||||
{
|
{
|
||||||
|
@ -4,10 +4,16 @@ namespace Tests\Feature\Services;
|
|||||||
|
|
||||||
use App\Facades\QrCode;
|
use App\Facades\QrCode;
|
||||||
use App\Services\QrCodeService;
|
use App\Services\QrCodeService;
|
||||||
|
use Exception;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\Classes\LocalFile;
|
use Tests\Classes\LocalFile;
|
||||||
use Tests\FeatureTestCase;
|
use Tests\FeatureTestCase;
|
||||||
|
use Zxing\ChecksumException;
|
||||||
|
use Zxing\FormatException;
|
||||||
|
use Zxing\NotFoundException;
|
||||||
|
use Zxing\QrReader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* QrCodeServiceTest test class
|
* QrCodeServiceTest test class
|
||||||
@ -42,10 +48,43 @@ class QrCodeServiceTest extends FeatureTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_decode_invalid_image_returns_correct_value()
|
public function test_decode_invalid_image_throws_an_exception()
|
||||||
{
|
{
|
||||||
$this->expectException(\App\Exceptions\InvalidQrCodeException::class);
|
$this->expectException(\App\Exceptions\InvalidQrCodeException::class);
|
||||||
|
|
||||||
QrCode::decode(LocalFile::fake()->invalidQrcode());
|
QrCode::decode(LocalFile::fake()->invalidQrcode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('QrReaderExceptionProvider')]
|
||||||
|
public function test_decodee_throws_an_exception($exception)
|
||||||
|
{
|
||||||
|
$this->expectException(\App\Exceptions\InvalidQrCodeException::class);
|
||||||
|
|
||||||
|
// QrReader is a final class, so we need to mock it here with a new object instance
|
||||||
|
// to then bind it to the container
|
||||||
|
$fileContent = LocalFile::fake()->validQrcode()->get();
|
||||||
|
$qrReader = \Mockery::mock(new QrReader($fileContent, QrReader::SOURCE_TYPE_BLOB))->makePartial();
|
||||||
|
$qrReader->shouldReceive('text')->andReturn('');
|
||||||
|
$qrReader->shouldReceive('getError')->andReturn($exception);
|
||||||
|
|
||||||
|
$this->app->bind(QrReader::class, function() use($qrReader) {
|
||||||
|
return $qrReader;
|
||||||
|
});
|
||||||
|
|
||||||
|
QrCode::decode(LocalFile::fake()->validQrcode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public static function QrReaderExceptionProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'NotFoundException' => [new NotFoundException()],
|
||||||
|
'FormatException' => [new FormatException()],
|
||||||
|
'ChecksumException' => [new ChecksumException()],
|
||||||
|
'default' => [new Exception()],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
namespace Tests\Feature\Services;
|
namespace Tests\Feature\Services;
|
||||||
|
|
||||||
use App\Facades\Settings;
|
use App\Facades\Settings;
|
||||||
|
use App\Providers\TwoFAuthServiceProvider;
|
||||||
|
use App\Services\ReleaseRadarService as ServicesReleaseRadarService;
|
||||||
// use App\Services\ReleaseRadarService;
|
// use App\Services\ReleaseRadarService;
|
||||||
use Facades\App\Services\ReleaseRadarService;
|
use Facades\App\Services\ReleaseRadarService;
|
||||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||||
@ -15,7 +17,8 @@ use Tests\FeatureTestCase;
|
|||||||
/**
|
/**
|
||||||
* ReleaseRadarServiceTest test class
|
* ReleaseRadarServiceTest test class
|
||||||
*/
|
*/
|
||||||
#[CoversClass(\App\Services\ReleaseRadarService::class)]
|
#[CoversClass(ServicesReleaseRadarService::class)]
|
||||||
|
#[CoversClass(TwoFAuthServiceProvider::class)]
|
||||||
class ReleaseRadarServiceTest extends FeatureTestCase
|
class ReleaseRadarServiceTest extends FeatureTestCase
|
||||||
{
|
{
|
||||||
use WithoutMiddleware;
|
use WithoutMiddleware;
|
||||||
|
50
tests/Feature/ViewTest.php
Normal file
50
tests/Feature/ViewTest.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Events\ScanForNewReleaseCalled;
|
||||||
|
use App\Http\Controllers\SinglePageController;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\FeatureTestCase;
|
||||||
|
|
||||||
|
#[CoversClass(SinglePageController::class)]
|
||||||
|
class ViewTest extends FeatureTestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function test_landing_view_is_returned()
|
||||||
|
{
|
||||||
|
$response = $this->get('/');
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertViewIs('landing');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_landing_view_has_data()
|
||||||
|
{
|
||||||
|
$response = $this->get('/');
|
||||||
|
|
||||||
|
$response->assertViewHas('appSettings');
|
||||||
|
$response->assertViewHas('appConfig');
|
||||||
|
$response->assertViewHas('defaultPreferences');
|
||||||
|
$response->assertViewHas('subdirectory');
|
||||||
|
$response->assertViewHas('isDemoApp');
|
||||||
|
$response->assertViewHas('isTestingApp');
|
||||||
|
$response->assertViewHas('lang');
|
||||||
|
$response->assertViewHas('locales');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_calling_index_fires_ScanForNewReleaseCalled_event()
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$this->get('/');
|
||||||
|
|
||||||
|
Event::assertDispatched(ScanForNewReleaseCalled::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
25
tests/Unit/Events/VisitedByProxyUserTest.php
Normal file
25
tests/Unit/Events/VisitedByProxyUserTest.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Events;
|
||||||
|
|
||||||
|
use App\Events\VisitedByProxyUser;
|
||||||
|
use App\Models\User;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VisitedByProxyUserTest test class
|
||||||
|
*/
|
||||||
|
#[CoversClass(VisitedByProxyUser::class)]
|
||||||
|
class VisitedByProxyUserTest extends TestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function test_event_constructor()
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$event = new VisitedByProxyUser($user);
|
||||||
|
|
||||||
|
$this->assertSame($user, $event->user);
|
||||||
|
}
|
||||||
|
}
|
@ -4,10 +4,15 @@ namespace Tests\Unit\Listeners\Authentication;
|
|||||||
|
|
||||||
use App\Listeners\Authentication\FailedLoginListener;
|
use App\Listeners\Authentication\FailedLoginListener;
|
||||||
use Illuminate\Auth\Events\Failed;
|
use Illuminate\Auth\Events\Failed;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
use Mockery;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\Classes\unexpectedEvent;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
use TypeError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FailedLoginListenerTest test class
|
* FailedLoginListenerTest test class
|
||||||
@ -25,4 +30,30 @@ class FailedLoginListenerTest extends TestCase
|
|||||||
FailedLoginListener::class
|
FailedLoginListener::class
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_handle_throws_exception_with_unexpected_event_type()
|
||||||
|
{
|
||||||
|
$this->expectException(TypeError::class);
|
||||||
|
|
||||||
|
$request = Mockery::mock(Request::class);
|
||||||
|
$event = Mockery::mock(unexpectedEvent::class);
|
||||||
|
$listener = new FailedLoginListener($request);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_handle_send_nothing_if_user_is_null()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$request = Mockery::mock(Request::class);
|
||||||
|
$event = Mockery::mock(Failed::class);
|
||||||
|
|
||||||
|
(new FailedLoginListener($request))->handle($event);
|
||||||
|
|
||||||
|
Notification::assertNothingSent();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,14 @@ namespace Tests\Unit\Listeners\Authentication;
|
|||||||
|
|
||||||
use App\Listeners\Authentication\LoginListener;
|
use App\Listeners\Authentication\LoginListener;
|
||||||
use Illuminate\Auth\Events\Login;
|
use Illuminate\Auth\Events\Login;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Mockery;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\Classes\unexpectedEvent;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
use TypeError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LoginListenerTest test class
|
* LoginListenerTest test class
|
||||||
@ -25,4 +29,17 @@ class LoginListenerTest extends TestCase
|
|||||||
LoginListener::class
|
LoginListener::class
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_handle_throws_exception_with_unexpected_event_type()
|
||||||
|
{
|
||||||
|
$this->expectException(TypeError::class);
|
||||||
|
|
||||||
|
$request = Mockery::mock(Request::class);
|
||||||
|
$event = Mockery::mock(unexpectedEvent::class);
|
||||||
|
$listener = new LoginListener($request);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,14 @@ namespace Tests\Unit\Listeners\Authentication;
|
|||||||
|
|
||||||
use App\Listeners\Authentication\LogoutListener;
|
use App\Listeners\Authentication\LogoutListener;
|
||||||
use Illuminate\Auth\Events\Logout;
|
use Illuminate\Auth\Events\Logout;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Mockery;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\Classes\unexpectedEvent;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
use TypeError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LogoutListenerTest test class
|
* LogoutListenerTest test class
|
||||||
@ -25,4 +29,16 @@ class LogoutListenerTest extends TestCase
|
|||||||
LogoutListener::class
|
LogoutListener::class
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_handle_throws_exception_with_unexpected_event_type()
|
||||||
|
{
|
||||||
|
$this->expectException(TypeError::class);
|
||||||
|
|
||||||
|
$request = Mockery::mock(Request::class);
|
||||||
|
$event = Mockery::mock(unexpectedEvent::class);
|
||||||
|
$listener = new LogoutListener($request);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,14 @@ namespace Tests\Unit\Listeners\Authentication;
|
|||||||
|
|
||||||
use App\Events\VisitedByProxyUser;
|
use App\Events\VisitedByProxyUser;
|
||||||
use App\Listeners\Authentication\VisitedByProxyUserListener;
|
use App\Listeners\Authentication\VisitedByProxyUserListener;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Mockery;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\Classes\unexpectedEvent;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
use TypeError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VisitedByProxyUserListenerTest test class
|
* VisitedByProxyUserListenerTest test class
|
||||||
@ -25,4 +29,16 @@ class VisitedByProxyUserListenerTest extends TestCase
|
|||||||
VisitedByProxyUserListener::class
|
VisitedByProxyUserListener::class
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_handle_throws_exception_with_unexpected_event_type()
|
||||||
|
{
|
||||||
|
$this->expectException(TypeError::class);
|
||||||
|
|
||||||
|
$request = Mockery::mock(Request::class);
|
||||||
|
$event = Mockery::mock(unexpectedEvent::class);
|
||||||
|
$listener = new VisitedByProxyUserListener($request);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
namespace Tests\Unit\Listeners;
|
namespace Tests\Unit\Listeners;
|
||||||
|
|
||||||
use App\Listeners\LogNotificationListener;
|
use App\Listeners\LogNotificationListener;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Notifications\TestEmailSettingNotification;
|
||||||
use Illuminate\Notifications\Events\NotificationSent;
|
use Illuminate\Notifications\Events\NotificationSent;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
@ -25,4 +28,15 @@ class LogNotificationTest extends TestCase
|
|||||||
LogNotificationListener::class
|
LogNotificationListener::class
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_handle_logs_notification_sending()
|
||||||
|
{
|
||||||
|
$event = new NotificationSent((new User()), (new TestEmailSettingNotification()), 'channel');
|
||||||
|
$listener = new LogNotificationListener();
|
||||||
|
|
||||||
|
Log::shouldReceive('info')->once();
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user