mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-02-22 21:30:56 +01:00
Fix & Complete tests
This commit is contained in:
parent
8c89c6f0ab
commit
e238e5121c
@ -18,6 +18,6 @@ class ScanForNewReleaseCalled
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Log::debug('ReleaseRadarActivated event dispatched');
|
||||
Log::debug('ReleaseRadarActivated event dispatched'); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
|
@ -131,12 +131,9 @@ public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehash the user's password if required and supported.
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param array $credentials
|
||||
* @param bool $force
|
||||
* @return void
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false)
|
||||
{
|
||||
|
@ -20,8 +20,8 @@ public function update(UserPatchPwdRequest $request)
|
||||
$user = $request->user();
|
||||
$validated = $request->validated();
|
||||
|
||||
if (config('auth.defaults.guard') === 'reverse-proxy-guard' || $user->oauth_provider) {
|
||||
Log::notice('Password update rejected: reverse-proxy-guard enabled or account from external sso provider');
|
||||
if ($user->oauth_provider) {
|
||||
Log::notice('Password update rejected: external account from a sso provider');
|
||||
|
||||
return response()->json(['message' => __('errors.account_managed_by_external_provider')], 400);
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ class CustomCreateFreshApiToken extends CreateFreshApiToken
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function requestShouldReceiveFreshToken($request)
|
||||
{
|
||||
|
@ -11,6 +11,8 @@ class ForceJsonResponse
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return mixed
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
|
@ -8,6 +8,8 @@ class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* TrustProxies constructor.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -51,7 +51,6 @@ public function handle(mixed $event) : void
|
||||
->whereUserAgent($userAgent)
|
||||
->whereLoginSuccessful(true)
|
||||
->whereGuard($event->guard)
|
||||
->whereLoginMethod($this->loginMethod())
|
||||
->first();
|
||||
$newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now(), true) < 1;
|
||||
|
||||
|
@ -49,7 +49,6 @@ public function handle(mixed $event) : void
|
||||
->whereIpAddress($ip)
|
||||
->whereUserAgent($userAgent)
|
||||
->whereGuard($event->guard)
|
||||
->whereLoginMethod($this->loginMethod())
|
||||
->orderByDesc('login_at')
|
||||
->first();
|
||||
|
||||
|
@ -28,6 +28,12 @@
|
||||
use Illuminate\Auth\Events\OtherDeviceLogout;
|
||||
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
|
||||
{
|
||||
/**
|
||||
@ -48,7 +54,6 @@ public function handle(mixed $event) : void
|
||||
$authLog = $user->authentications()
|
||||
->whereIpAddress($ip)
|
||||
->whereUserAgent($userAgent)
|
||||
->whereLoginMethod($this->loginMethod())
|
||||
->first();
|
||||
$guard = $event->guard;
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
use App\Services\TwoFAccountService;
|
||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Zxing\QrReader;
|
||||
|
||||
class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvider
|
||||
{
|
||||
@ -34,6 +35,10 @@ public function register()
|
||||
$this->app->singleton(ReleaseRadarService::class, function () {
|
||||
return new ReleaseRadarService();
|
||||
});
|
||||
|
||||
$this->app->bind(QrReader::class, function ($app, $parameters) {
|
||||
return new QrReader($parameters['imgSource'], $parameters['sourceType']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,7 +39,11 @@ public static function encode(string $data)
|
||||
*/
|
||||
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();
|
||||
|
||||
if (! $text) {
|
||||
|
@ -4,12 +4,16 @@
|
||||
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -18,8 +22,8 @@ class AuthLogFactory extends Factory
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'ip_address' => self::IP,
|
||||
'user_agent' => self::USER_AGENT,
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'logout_at' => null,
|
||||
@ -28,6 +32,20 @@ public function definition()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
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\RejectIfAuthenticated;
|
||||
use App\Http\Middleware\RejectIfDemoMode;
|
||||
use App\Http\Middleware\RejectIfReverseProxy;
|
||||
use App\Http\Middleware\SkipIfAuthenticated;
|
||||
use App\Listeners\Authentication\FailedLoginListener;
|
||||
use App\Listeners\Authentication\LoginListener;
|
||||
use App\Listeners\Authentication\LogoutListener;
|
||||
use App\Listeners\Authentication\VisitedByProxyUserListener;
|
||||
use App\Listeners\LogNotificationListener;
|
||||
use App\Models\AuthLog;
|
||||
use App\Models\User;
|
||||
@ -37,8 +37,8 @@
|
||||
#[CoversClass(LoginListener::class)]
|
||||
#[CoversClass(LogoutListener::class)]
|
||||
#[CoversClass(FailedLoginListener::class)]
|
||||
#[CoversClass(VisitedByProxyUserListener::class)]
|
||||
#[CoversMethod(CaseInsensitiveEmailExists::class, 'validate')]
|
||||
#[CoversMethod(SkipIfAuthenticated::class, 'handle')]
|
||||
#[CoversMethod(Handler::class, 'register')]
|
||||
#[CoversMethod(KickOutInactiveUser::class, 'handle')]
|
||||
#[CoversMethod(LogUserLastSeen::class, 'handle')]
|
||||
@ -59,9 +59,15 @@ class LoginTest extends FeatureTestCase
|
||||
|
||||
private const WEB_GUARD = 'web-guard';
|
||||
|
||||
private const REVERSE_PROXY_GUARD = 'reverse-proxy-guard';
|
||||
|
||||
private const PASSWORD = 'password';
|
||||
|
||||
private const WRONG_PASSWORD = 'wrong_password';
|
||||
|
||||
private const USER_NAME = 'John';
|
||||
|
||||
private const USER_EMAIL = 'john@example.com';
|
||||
|
||||
public function setUp() : void
|
||||
{
|
||||
@ -115,14 +121,14 @@ public function test_login_send_new_device_notification()
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
], [
|
||||
'HTTP_USER_AGENT' => 'NotSymfony',
|
||||
'HTTP_USER_AGENT' => 'another_useragent_to_be_identified_as_new_device',
|
||||
])->assertOk();
|
||||
|
||||
Notification::assertSentTo($this->user, SignedInWithNewDeviceNotification::class);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
@ -143,7 +149,23 @@ public function test_login_does_not_send_new_device_notification()
|
||||
'email' => $this->user->email,
|
||||
'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();
|
||||
|
||||
Notification::assertNothingSentTo($this->user);
|
||||
@ -361,12 +383,12 @@ public function test_successful_web_logout_is_logged()
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
])->assertOk();
|
||||
|
||||
|
||||
$this->actingAs($this->user, self::WEB_GUARD)
|
||||
->json('GET', '/user/logout')
|
||||
->assertOk();
|
||||
|
||||
$authlog = AuthLog::first();
|
||||
|
||||
$authlog = $this->user->latestAuthentication()->first();
|
||||
|
||||
$this->assertEquals($this->user->id, $authlog->authenticatable_id);
|
||||
$this->assertTrue($authlog->login_successful);
|
||||
@ -390,4 +412,138 @@ public function test_orphan_web_logout_is_logged()
|
||||
$this->assertNull($authlog->login_method);
|
||||
$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 @@
|
||||
|
||||
use App\Http\Controllers\Auth\PasswordController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FeatureTestCase;
|
||||
@ -22,6 +23,12 @@ class PasswordControllerTest extends FeatureTestCase
|
||||
private const PASSWORD = 'password';
|
||||
|
||||
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
|
||||
{
|
||||
@ -71,4 +78,53 @@ public function test_update_passing_invalid_data_return_validation_error()
|
||||
])
|
||||
->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 @@
|
||||
use App\Facades\Settings;
|
||||
use App\Http\Controllers\Auth\SocialiteController;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
@ -78,6 +80,53 @@ public function test_redirect_returns_error_when_registrations_are_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]
|
||||
public function test_callback_authenticates_the_user()
|
||||
{
|
||||
@ -228,6 +277,22 @@ public function test_callback_returns_error_when_email_is_already_used()
|
||||
]);
|
||||
}
|
||||
|
||||
#[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]
|
||||
public function test_callback_redirects_to_error_when_registrations_are_closed()
|
||||
{
|
||||
|
@ -9,7 +9,6 @@
|
||||
use App\Notifications\WebauthnRecoveryNotification;
|
||||
use App\Providers\AuthServiceProvider;
|
||||
use App\Rules\CaseInsensitiveEmailExists;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
@ -19,6 +18,7 @@
|
||||
/**
|
||||
* WebAuthnDeviceLostControllerTest test class
|
||||
*/
|
||||
#[CoversMethod(User::class, 'sendWebauthnRecoveryNotification')]
|
||||
#[CoversClass(WebAuthnDeviceLostController::class)]
|
||||
#[CoversClass(WebauthnRecoveryNotification::class)]
|
||||
#[CoversClass(WebauthnCredentialBroker::class)]
|
||||
|
@ -4,10 +4,13 @@
|
||||
|
||||
use App\Extensions\WebauthnTwoFAuthUserProvider;
|
||||
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\Notifications\SignedInWithNewDeviceNotification;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
|
||||
use Laragear\WebAuthn\Enums\UserVerification;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
@ -21,19 +24,15 @@
|
||||
#[CoversClass(WebAuthnLoginController::class)]
|
||||
#[CoversClass(User::class)]
|
||||
#[CoversClass(WebauthnTwoFAuthUserProvider::class)]
|
||||
#[CoversMethod(SkipIfAuthenticated::class, 'handle')]
|
||||
#[CoversClass(LoginListener::class)]
|
||||
#[CoversClass(FailedLoginListener::class)]
|
||||
class WebAuthnLoginControllerTest extends FeatureTestCase
|
||||
{
|
||||
/**
|
||||
* @var \App\Models\User
|
||||
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @var \App\Models\User
|
||||
*/
|
||||
protected $admin;
|
||||
|
||||
const CREDENTIAL_ID = 's06aG41wsIYh5X1YUhB-SlH8y3F2RzdJZVse8iXRXOCd3oqQdEyCOsBawzxrYBtJRQA2azAMEN_q19TUp6iMgg';
|
||||
|
||||
const CREDENTIAL_ID_ALT = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg';
|
||||
@ -48,6 +47,10 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
||||
|
||||
const EMAIL = 'john.doe@example.com';
|
||||
|
||||
private const GUARD = 'web-guard';
|
||||
|
||||
private const AUTH_METHOD = 'webauthn';
|
||||
|
||||
const ASSERTION_RESPONSE = [
|
||||
'id' => self::CREDENTIAL_ID_ALT,
|
||||
'rawId' => self::CREDENTIAL_ID_ALT_RAW,
|
||||
@ -94,37 +97,19 @@ public function setUp() : void
|
||||
parent::setUp();
|
||||
|
||||
DB::table('users')->delete();
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
$this->mock(AssertionValidator::class)
|
||||
->shouldReceive('send->thenReturn')
|
||||
->andReturn();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function test_webauthn_login_returns_success()
|
||||
{
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
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->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||
$this->addWebauthnChallengeToSession();
|
||||
|
||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||
->assertOk()
|
||||
@ -140,35 +125,74 @@ public function test_webauthn_login_returns_success()
|
||||
]);
|
||||
}
|
||||
|
||||
#[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]
|
||||
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([
|
||||
'id' => self::CREDENTIAL_ID_ALT,
|
||||
'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->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||
$this->addWebauthnChallengeToSession();
|
||||
|
||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||
->assertOk()
|
||||
@ -180,32 +204,8 @@ public function test_webauthn_admin_login_returns_admin_role()
|
||||
#[Test]
|
||||
public function test_webauthn_login_merge_handle_if_missing()
|
||||
{
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
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->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||
$this->addWebauthnChallengeToSession();
|
||||
|
||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_NO_HANDLE)
|
||||
->assertOk()
|
||||
@ -223,10 +223,6 @@ public function test_webauthn_login_merge_handle_if_missing()
|
||||
#[Test]
|
||||
public function test_legacy_login_is_rejected_when_webauthn_only_is_enable()
|
||||
{
|
||||
$this->user = User::factory()->create([
|
||||
'email' => self::EMAIL,
|
||||
]);
|
||||
|
||||
// Set to webauthn only
|
||||
$this->user['preferences->useWebauthnOnly'] = true;
|
||||
$this->user->save();
|
||||
@ -241,32 +237,8 @@ public function test_legacy_login_is_rejected_when_webauthn_only_is_enable()
|
||||
#[Test]
|
||||
public function test_webauthn_login_already_authenticated_is_rejected()
|
||||
{
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
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->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
|
||||
$this->addWebauthnChallengeToSession();
|
||||
|
||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
|
||||
->assertOk();
|
||||
@ -281,8 +253,6 @@ public function test_webauthn_login_already_authenticated_is_rejected()
|
||||
#[Test]
|
||||
public function test_webauthn_login_with_missing_data_returns_validation_error()
|
||||
{
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
$data = [
|
||||
'id' => '',
|
||||
'rawId' => '',
|
||||
@ -310,13 +280,7 @@ public function test_webauthn_login_with_missing_data_returns_validation_error()
|
||||
#[Test]
|
||||
public function test_webauthn_invalid_login_returns_unauthorized()
|
||||
{
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
)]);
|
||||
$this->addWebauthnChallengeToSession();
|
||||
|
||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
|
||||
->assertUnauthorized();
|
||||
@ -328,13 +292,7 @@ public function test_too_many_invalid_login_attempts_returns_too_many_request_er
|
||||
$throttle = 8;
|
||||
Config::set('auth.throttle.login', $throttle);
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
$this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
|
||||
new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
|
||||
60,
|
||||
false,
|
||||
)]);
|
||||
$this->addWebauthnChallengeToSession();
|
||||
|
||||
for ($i = 0; $i < $throttle - 1; $i++) {
|
||||
$this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
|
||||
@ -352,22 +310,7 @@ public function test_get_options_returns_success()
|
||||
{
|
||||
Config::set('webauthn.user_verification', UserVerification::PREFERRED);
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
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(),
|
||||
]);
|
||||
$this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
|
||||
|
||||
$response = $this->json('POST', '/webauthn/login/options', [
|
||||
'email' => $this->user->email,
|
||||
@ -390,22 +333,7 @@ public function test_get_options_for_securelogin_returns_required_userVerificati
|
||||
{
|
||||
Config::set('webauthn.user_verification', UserVerification::REQUIRED);
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
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(),
|
||||
]);
|
||||
$this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
|
||||
|
||||
$response = $this->json('POST', '/webauthn/login/options', [
|
||||
'email' => $this->user->email,
|
||||
@ -430,22 +358,7 @@ public function test_get_options_for_fastlogin_returns_discouraged_userVerificat
|
||||
{
|
||||
Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
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(),
|
||||
]);
|
||||
$this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
|
||||
|
||||
$response = $this->json('POST', '/webauthn/login/options', [
|
||||
'email' => $this->user->email,
|
||||
@ -468,8 +381,6 @@ public function test_get_options_for_fastlogin_returns_discouraged_userVerificat
|
||||
#[Test]
|
||||
public function test_get_options_with_capitalized_email_returns_success()
|
||||
{
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
$this->json('POST', '/webauthn/login/options', [
|
||||
'email' => strtoupper($this->user->email),
|
||||
])
|
||||
@ -511,4 +422,72 @@ public function test_get_options_with_unknown_email_returns_validation_errors()
|
||||
'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\Models\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\Request;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
#[CoversClass(AdminOnly::class)]
|
||||
class AdminOnlyMiddlewareTest extends FeatureTestCase
|
||||
{
|
||||
#[Test]
|
||||
|
@ -2,10 +2,18 @@
|
||||
|
||||
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 PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
#[CoversClass(Authenticate::class)]
|
||||
#[CoversMethod(AuthServiceProvider::class, 'boot')]
|
||||
class AuthenticateMiddlewareTest extends FeatureTestCase
|
||||
{
|
||||
private const USER_NAME = 'John';
|
||||
@ -45,4 +53,27 @@ public function test_it_does_not_authenticate_with_missing_header()
|
||||
$this->json('GET', '/api/v1/groups', [], [])
|
||||
->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\Models\User;
|
||||
use App\Notifications\TestEmailSettingNotification;
|
||||
use App\Services\ReleaseRadarService;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Notifications\Dispatcher;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
@ -138,6 +138,21 @@ public function test_testEmail_returns_forbidden()
|
||||
->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]
|
||||
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 @@ public function test_set_invalid_icon_ends_without_error($res, $ext)
|
||||
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
|
||||
*/
|
||||
|
@ -7,6 +7,7 @@
|
||||
use App\Models\TwoFAccount;
|
||||
use App\Models\User;
|
||||
use App\Observers\UserObserver;
|
||||
use Database\Factories\AuthLogFactory;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@ -187,4 +188,264 @@ public function test_delete_does_not_delete_the_only_admin()
|
||||
|
||||
$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;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
|
||||
#[CoversMethod(RouteServiceProvider::class, 'boot')]
|
||||
#[CoversClass(RouteServiceProvider::class)]
|
||||
class RouteTest extends FeatureTestCase
|
||||
{
|
||||
const API_ROUTE_PREFIX = '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]
|
||||
public function test_exception_handler_with_web_route()
|
||||
{
|
||||
|
@ -162,6 +162,21 @@ public function test_user_can_assign_multiple_accounts()
|
||||
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]
|
||||
public function test_prependTheAllGroup_add_the_group_on_top_of_groups()
|
||||
{
|
||||
|
@ -4,10 +4,16 @@
|
||||
|
||||
use App\Facades\QrCode;
|
||||
use App\Services\QrCodeService;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Classes\LocalFile;
|
||||
use Tests\FeatureTestCase;
|
||||
use Zxing\ChecksumException;
|
||||
use Zxing\FormatException;
|
||||
use Zxing\NotFoundException;
|
||||
use Zxing\QrReader;
|
||||
|
||||
/**
|
||||
* QrCodeServiceTest test class
|
||||
@ -42,10 +48,43 @@ public function test_decode_valid_image_returns_correct_value()
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
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;
|
||||
|
||||
use App\Facades\Settings;
|
||||
use App\Providers\TwoFAuthServiceProvider;
|
||||
use App\Services\ReleaseRadarService as ServicesReleaseRadarService;
|
||||
// use App\Services\ReleaseRadarService;
|
||||
use Facades\App\Services\ReleaseRadarService;
|
||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||
@ -15,7 +17,8 @@
|
||||
/**
|
||||
* ReleaseRadarServiceTest test class
|
||||
*/
|
||||
#[CoversClass(\App\Services\ReleaseRadarService::class)]
|
||||
#[CoversClass(ServicesReleaseRadarService::class)]
|
||||
#[CoversClass(TwoFAuthServiceProvider::class)]
|
||||
class ReleaseRadarServiceTest extends FeatureTestCase
|
||||
{
|
||||
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 @@
|
||||
|
||||
use App\Listeners\Authentication\FailedLoginListener;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Classes\unexpectedEvent;
|
||||
use Tests\TestCase;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* FailedLoginListenerTest test class
|
||||
@ -25,4 +30,30 @@ public function test_FailedLoginListener_listen_to_Failed_event()
|
||||
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 @@
|
||||
|
||||
use App\Listeners\Authentication\LoginListener;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Classes\unexpectedEvent;
|
||||
use Tests\TestCase;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* LoginListenerTest test class
|
||||
@ -25,4 +29,17 @@ public function test_LoginListener_listen_to_Login_event()
|
||||
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 @@
|
||||
|
||||
use App\Listeners\Authentication\LogoutListener;
|
||||
use Illuminate\Auth\Events\Logout;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Classes\unexpectedEvent;
|
||||
use Tests\TestCase;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* LogoutListenerTest test class
|
||||
@ -25,4 +29,16 @@ public function test_LogoutListener_listen_to_Logout_event()
|
||||
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 @@
|
||||
|
||||
use App\Events\VisitedByProxyUser;
|
||||
use App\Listeners\Authentication\VisitedByProxyUserListener;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Classes\unexpectedEvent;
|
||||
use Tests\TestCase;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* VisitedByProxyUserListenerTest test class
|
||||
@ -25,4 +29,16 @@ public function test_VisitedByProxyUserListener_listen_to_VisitedByProxyUser_eve
|
||||
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;
|
||||
|
||||
use App\Listeners\LogNotificationListener;
|
||||
use App\Models\User;
|
||||
use App\Notifications\TestEmailSettingNotification;
|
||||
use Illuminate\Notifications\Events\NotificationSent;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
@ -25,4 +28,15 @@ public function test_LogNotificationTest_listen_to_NotificationSent_event()
|
||||
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…
Reference in New Issue
Block a user