Fix & Complete tests

This commit is contained in:
Bubka 2024-07-03 11:16:08 +02:00
parent 8c89c6f0ab
commit e238e5121c
37 changed files with 1261 additions and 221 deletions

View File

@ -18,6 +18,6 @@ class ScanForNewReleaseCalled
*/
public function __construct()
{
Log::debug('ReleaseRadarActivated event dispatched');
Log::debug('ReleaseRadarActivated event dispatched'); // @codeCoverageIgnore
}
}

View File

@ -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)
{

View File

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

View File

@ -11,6 +11,8 @@ class CustomCreateFreshApiToken extends CreateFreshApiToken
*
* @param \Illuminate\Http\Request $request
* @return bool
*
* @codeCoverageIgnore
*/
protected function requestShouldReceiveFreshToken($request)
{

View File

@ -11,6 +11,8 @@ class ForceJsonResponse
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @codeCoverageIgnore
*/
public function handle($request, Closure $next)
{

View File

@ -8,6 +8,8 @@ class TrustProxies extends Middleware
{
/**
* TrustProxies constructor.
*
* @codeCoverageIgnore
*/
public function __construct()
{

View File

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

View File

@ -49,7 +49,6 @@ public function handle(mixed $event) : void
->whereIpAddress($ip)
->whereUserAgent($userAgent)
->whereGuard($event->guard)
->whereLoginMethod($this->loginMethod())
->orderByDesc('login_at')
->first();

View File

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

View File

@ -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']);
});
}
/**

View File

@ -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) {

View File

@ -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.
*

View File

@ -0,0 +1,8 @@
<?php
namespace Tests\Classes;
class unexpectedEvent
{
//
}

View File

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

View File

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

View File

@ -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()
{

View File

@ -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)]

View File

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

View File

@ -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]

View File

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

View 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());
}
}

View File

@ -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()
{

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

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

View File

@ -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
*/

View File

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

View File

@ -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()
{

View File

@ -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()
{

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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