Enhance test coverage & Fix small issues & Refactoring

This commit is contained in:
Bubka 2024-06-28 16:13:45 +02:00
parent 413e2c4ba9
commit 0f1372e8bd
38 changed files with 1274 additions and 163 deletions

View File

@ -42,10 +42,6 @@ public function index(TwoFAccountIndexRequest $request)
$validated = $request->validated();
// if ($request->has('withOtp')) {
// $request->merge(['at' => time()]);
// }
return Arr::has($validated, 'ids')
? new TwoFAccountCollection($request->user()->twofaccounts()->whereIn('id', Helpers::commaSeparatedToArray($validated['ids']))->get()->sortBy('order_column'))
: new TwoFAccountCollection($request->user()->twofaccounts->sortBy('order_column'));

View File

@ -4,7 +4,7 @@
use App\Api\v1\Requests\UserManagerPromoteRequest;
use App\Api\v1\Requests\UserManagerStoreRequest;
use App\Api\v1\Resources\UserAuthentication;
use App\Api\v1\Resources\UserAuthenticationResource;
use App\Api\v1\Resources\UserManagerResource;
use App\Http\Controllers\Controller;
use App\Models\User;
@ -231,7 +231,7 @@ public function authentications(Request $request, User $user)
$authentications = $request->has('period') ? $user->authenticationsByPeriod($validated['period']) : $user->authentications;
$authentications = $request->has('limit') ? $authentications->take($validated['limit']) : $authentications;
return UserAuthentication::collection($authentications);
return UserAuthenticationResource::collection($authentications);
}
/**

View File

@ -25,7 +25,7 @@ public function authorize()
public function rules()
{
return [
'service' => 'string|regex:/^[^:]+$/i',
'service' => 'string',
];
}
@ -39,7 +39,7 @@ public function rules()
protected function prepareForValidation()
{
$this->merge([
'service' => strip_tags($this->service),
'service' => strip_tags(strval($this->service)),
]);
}
}

View File

@ -29,8 +29,11 @@ public function toArray($request)
$request->merge(['withSecret' => false]);
}
// Here we add a timestamp to the request if OTPs have to be in the response.
// The 'at' parameter is used by the TwoFAccountReadResource class to obtain
// all OTPs at the same timestamps
if ($request->has('withOtp')) {
$request->merge(['at' => time()]);
$request->merge(['at' => now()->timestamp]);
}
return $this->collection;

View File

@ -20,7 +20,7 @@
* @property string|null $duration
* @property string|null $login_method
*/
class UserAuthentication extends JsonResource
class UserAuthenticationResource extends JsonResource
{
/**
* A user agent parser instance.

View File

@ -24,7 +24,7 @@
namespace App\Listeners\Authentication;
use App\Notifications\FailedLogin;
use App\Notifications\FailedLoginNotification;
use Illuminate\Auth\Events\Failed;
class FailedLoginListener extends AbstractAccessListener
@ -56,7 +56,7 @@ public function handle(mixed $event) : void
]);
if ($user->preferences['notifyOnFailedLogin'] == true) {
$user->notify(new FailedLogin($log));
$user->notify(new FailedLoginNotification($log));
}
}
}

View File

@ -24,7 +24,7 @@
namespace App\Listeners\Authentication;
use App\Notifications\SignedInWithNewDevice;
use App\Notifications\SignedInWithNewDeviceNotification;
use Illuminate\Auth\Events\Login;
use Illuminate\Support\Carbon;
@ -59,7 +59,7 @@ public function handle(mixed $event) : void
]);
if (! $known && ! $newUser && $user->preferences['notifyOnNewAuthDevice'] == true) {
$user->notify(new SignedInWithNewDevice($log));
$user->notify(new SignedInWithNewDeviceNotification($log));
}
}
}

View File

@ -4,7 +4,7 @@
use App\Events\VisitedByProxyUser;
use App\Extensions\RemoteUserProvider;
use App\Notifications\SignedInWithNewDevice;
use App\Notifications\SignedInWithNewDeviceNotification;
use Illuminate\Support\Carbon;
class VisitedByProxyUserListener extends AbstractAccessListener
@ -37,7 +37,7 @@ public function handle(mixed $event) : void
]);
if (! $known && ! $newUser && ! str_ends_with($user->email, RemoteUserProvider::FAKE_REMOTE_DOMAIN) && $user->preferences['notifyOnNewAuthDevice']) {
$user->notify(new SignedInWithNewDevice($log));
$user->notify(new SignedInWithNewDeviceNotification($log));
}
}
}

View File

@ -5,7 +5,7 @@
use Illuminate\Notifications\Events\NotificationSent;
use Illuminate\Support\Facades\Log;
class LogNotification
class LogNotificationListener
{
/**
* Create the event listener.

View File

@ -9,7 +9,7 @@
use Illuminate\Notifications\Notification;
use Jenssegers\Agent\Agent;
class FailedLogin extends Notification implements ShouldQueue
class FailedLoginNotification extends Notification implements ShouldQueue
{
use Queueable;
@ -26,7 +26,7 @@ class FailedLogin extends Notification implements ShouldQueue
public AuthLog $authLog;
/**
* Create a new FailedLogin instance
* Create a new FailedLoginNotification instance
*/
public function __construct(AuthLog $authLog)
{

View File

@ -9,7 +9,7 @@
use Illuminate\Notifications\Notification;
use Jenssegers\Agent\Agent;
class SignedInWithNewDevice extends Notification implements ShouldQueue
class SignedInWithNewDeviceNotification extends Notification implements ShouldQueue
{
use Queueable;
@ -26,7 +26,7 @@ class SignedInWithNewDevice extends Notification implements ShouldQueue
protected $agent;
/**
* Create a new SignedInWithNewDevice instance
* Create a new SignedInWithNewDeviceNotification instance
*/
public function __construct(AuthLog $authLog)
{
@ -47,7 +47,7 @@ public function toMail(mixed $notifiable) : MailMessage
{
return (new MailMessage())
->subject(__('notifications.new_device.subject'))
->markdown('emails.SignedInWithNewDevice', [
->markdown('emails.signedInWithNewDevice', [
'account' => $notifiable,
'time' => $this->authLog->login_at,
'ipAddress' => $this->authLog->ip_address,

View File

@ -24,13 +24,16 @@ public function before(User $user, string $ability) : ?bool
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user) : bool
{
return false;
}
// public function viewAny(User $user) : bool
// {
// return false;
// }
/**
* Determine whether the user can view the model.
*
* @codeCoverageIgnore
* Ignored as long as the before() method restrict the access to admins only
*/
public function view(User $user, User $model) : bool
{
@ -45,6 +48,9 @@ public function view(User $user, User $model) : bool
/**
* Determine whether the user can create models.
*
* @codeCoverageIgnore
* Ignored as long as the before() method restrict the access to admins only
*/
public function create(?User $user) : bool
{
@ -53,6 +59,8 @@ public function create(?User $user) : bool
/**
* Determine whether the user can update the model.
*
* Not ignored because the user can update itself
*/
public function update(User $user, User $model) : bool
{
@ -67,6 +75,9 @@ public function update(User $user, User $model) : bool
/**
* Determine whether the user can delete the model.
*
* @codeCoverageIgnore
* Ignored as long as the before() method restrict the access to admins only
*/
public function delete(User $user, User $model) : bool
{
@ -81,6 +92,9 @@ public function delete(User $user, User $model) : bool
/**
* Determine whether the user can promote the model.
*
* @codeCoverageIgnore
* Ignored as long as the before() method restrict the access to admins only
*/
public function promote(User $user) : bool
{

View File

@ -13,7 +13,7 @@
use App\Listeners\Authentication\VisitedByProxyUserListener;
use App\Listeners\CleanIconStorage;
use App\Listeners\DissociateTwofaccountFromGroup;
use App\Listeners\LogNotification;
use App\Listeners\LogNotificationListener;
use App\Listeners\RegisterOpenId;
use App\Listeners\ReleaseRadar;
use App\Listeners\ResetUsersPreference;
@ -55,7 +55,7 @@ class EventServiceProvider extends ServiceProvider
RegisterOpenId::class,
],
NotificationSent::class => [
LogNotification::class,
LogNotificationListener::class,
],
Login::class => [
LoginListener::class,

10
docker/entrypoint.sh vendored
View File

@ -60,9 +60,11 @@ fi
echo "${COMMIT}" > /2fauth/installed
php artisan storage:link --quiet
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Clearing compiled, cache has already been cleared
php artisan clear-compiled
# Clearing and Caching config, events, routes, views
php artisan optimize
supervisord

View File

@ -23,7 +23,7 @@
use Laravel\Passport\Http\Controllers\PersonalAccessTokenController;
// use App\Models\User;
// use App\Notifications\SignedInWithNewDevice;
// use App\Notifications\SignedInWithNewDeviceNotification;
// use App\Models\AuthLog;
/*
@ -109,7 +109,7 @@
// Route::get('/notification', function () {
// $user = User::find(1);
// return (new SignedInWithNewDevice(AuthLog::find(9)))
// return (new SignedInWithNewDeviceNotification(AuthLog::find(9)))
// ->toMail($user);
// });

View File

@ -4,6 +4,7 @@
use App\Api\v1\Controllers\GroupController;
use App\Api\v1\Resources\GroupResource;
use App\Listeners\DissociateTwofaccountFromGroup;
use App\Listeners\ResetUsersPreference;
use App\Models\Group;
use App\Models\TwoFAccount;
@ -18,6 +19,7 @@
#[CoversClass(ResetUsersPreference::class)]
#[CoversClass(GroupPolicy::class)]
#[CoversClass(Group::class)]
#[CoversClass(DissociateTwofaccountFromGroup::class)]
class GroupControllerTest extends FeatureTestCase
{
/**
@ -103,6 +105,27 @@ public function test_index_returns_user_groups_only_with_pseudo_group()
]);
}
#[Test]
public function test_orphan_groups_are_reassign_to_the_only_user()
{
config(['auth.defaults.guard' => 'reverse-proxy-guard']);
$this->anotherUser->delete();
$this->userGroupA->user_id = null;
$this->userGroupA->save();
$this->assertCount(1, User::all());
$this->assertNull($this->userGroupA->user_id);
$this->actingAs($this->user, 'reverse-proxy-guard')
->json('GET', '/api/v1/groups')
->assertOk();
$this->userGroupA->refresh();
$this->assertNotNull($this->userGroupA->user_id);
}
#[Test]
public function test_store_returns_created_group_resource()
{

View File

@ -3,12 +3,14 @@
namespace Tests\Api\v1\Controllers;
use App\Api\v1\Controllers\SettingController;
use App\Api\v1\Requests\SettingUpdateRequest;
use App\Facades\Settings;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
@ -16,6 +18,7 @@
* SettingController test class
*/
#[CoversClass(SettingController::class)]
#[CoversMethod(SettingUpdateRequest::class, 'rules')]
class SettingControllerTest extends FeatureTestCase
{
/**
@ -230,6 +233,16 @@ public function test_update_missing_user_setting_returns_created_setting()
]);
}
#[Test]
public function test_update_restrictList_setting_rejects_invalid_email_list()
{
$response = $this->actingAs($this->admin, 'api-guard')
->json('PUT', '/api/v1/settings/restrictList', [
'value' => 'johndoe@example.com|janedoeexamplecom',
])
->assertJsonValidationErrorFor('value');
}
#[Test]
public function test_destroy_user_setting_returns_success()
{

View File

@ -68,6 +68,8 @@ class TwoFAccountControllerTest extends FeatureTestCase
protected $twofaccountD;
protected $twofaccountE;
private const VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET = [
'id',
'group_id',
@ -102,12 +104,46 @@ class TwoFAccountControllerTest extends FeatureTestCase
'period',
];
private const VALID_EMBEDDED_OTP_RESOURCE_STRUCTURE_FOR_TOTP = [
'generated_at',
'password',
];
private const VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP = [
'otp_type',
'password',
'counter',
];
private const VALID_RESOURCE_STRUCTURE_WITH_OTP = [
'id',
'group_id',
'service',
'account',
'icon',
'otp_type',
'secret',
'digits',
'algorithm',
'period',
'counter',
'otp' => self::VALID_EMBEDDED_OTP_RESOURCE_STRUCTURE_FOR_TOTP
];
private const VALID_COLLECTION_RESOURCE_STRUCTURE_WITH_OTP = [
'id',
'group_id',
'service',
'account',
'icon',
'otp_type',
'digits',
'algorithm',
'period',
'counter',
'otp' => self::VALID_EMBEDDED_OTP_RESOURCE_STRUCTURE_FOR_TOTP
];
private const VALID_EXPORT_STRUTURE = [
'app',
'schema',
@ -204,10 +240,13 @@ public function setUp() : void
$this->twofaccountD = TwoFAccount::factory()->for($this->anotherUser)->create([
'group_id' => $this->anotherUserGroupB->id,
]);
$this->twofaccountE = TwoFAccount::factory()->for($this->anotherUser)->create([
'group_id' => $this->anotherUserGroupB->id,
]);
}
#[Test]
#[DataProvider('indexUrlParameterProvider')]
#[DataProvider('validResourceStructureProvider')]
public function test_index_returns_user_twofaccounts_only($urlParameter, $expected)
{
$response = $this->actingAs($this->user, 'api-guard')
@ -234,7 +273,7 @@ public function test_index_returns_user_twofaccounts_only($urlParameter, $expect
/**
* Provide data for index tests
*/
public static function indexUrlParameterProvider()
public static function validResourceStructureProvider()
{
return [
'VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET' => [
@ -245,9 +284,71 @@ public static function indexUrlParameterProvider()
'?withSecret=1',
self::VALID_RESOURCE_STRUCTURE_WITH_SECRET,
],
'VALID_COLLECTION_RESOURCE_STRUCTURE_WITH_OTP' => [
'?withOtp=1',
self::VALID_COLLECTION_RESOURCE_STRUCTURE_WITH_OTP,
],
];
}
#[Test]
public function test_index_returns_user_accounts_with_given_ids()
{
$response = $this->actingAs($this->anotherUser, 'api-guard')
->json('GET', '/api/v1/twofaccounts?ids=' . $this->twofaccountC->id . ',' . $this->twofaccountE->id)
->assertOk()
->assertJsonCount(2, $key = null)
->assertJsonStructure([
'*' => self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET,
])
->assertJsonFragment([
'id' => $this->twofaccountC->id,
])
->assertJsonFragment([
'id' => $this->twofaccountE->id,
]);
}
#[Test]
public function test_index_returns_only_user_accounts_in_given_ids()
{
$response = $this->actingAs($this->anotherUser, 'api-guard')
->json('GET', '/api/v1/twofaccounts?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountE->id)
->assertOk()
->assertJsonCount(1, $key = null)
->assertJsonStructure([
'*' => self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET,
])
->assertJsonMissing([
'id' => $this->twofaccountA->id,
])
->assertJsonFragment([
'id' => $this->twofaccountE->id,
]);
}
#[Test]
public function test_orphan_accounts_are_reassign_to_the_only_user()
{
config(['auth.defaults.guard' => 'reverse-proxy-guard']);
$this->anotherUser->delete();
$this->twofaccountA->user_id = null;
$this->twofaccountA->save();
$this->assertCount(1, User::all());
$this->assertNull($this->twofaccountA->user_id);
$this->assertCount(1, TwoFAccount::orphans()->get());
$this->actingAs($this->user, 'reverse-proxy-guard')
->json('GET', '/api/v1/twofaccounts')
->assertOk();
$this->twofaccountA->refresh();
$this->assertNotNull($this->twofaccountA->user_id);
}
#[Test]
public function test_show_returns_twofaccount_resource_with_secret()
{
@ -289,6 +390,24 @@ public function test_show_returns_twofaccount_resource_without_secret()
// ]);
// }
#[Test]
public function test_show_returns_twofaccount_resource_with_otp()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id . '?withOtp=1')
->assertOk()
->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_OTP);
}
#[Test]
public function test_show_returns_twofaccount_resource_without_otp()
{
$response = $this->actingAs($this->user, 'api-guard')
->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id . '?withOtp=0')
->assertOk()
->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET);
}
#[Test]
public function test_show_missing_twofaccount_returns_not_found()
{

View File

@ -3,10 +3,12 @@
namespace Tests\Api\v1\Controllers;
use App\Api\v1\Controllers\UserManagerController;
use App\Api\v1\Resources\UserAuthenticationResource;
use App\Api\v1\Resources\UserManagerResource;
use App\Models\AuthLog;
use App\Models\TwoFAccount;
use App\Models\User;
use App\Observers\UserObserver;
use App\Policies\UserPolicy;
use Database\Factories\UserFactory;
use Illuminate\Auth\Notifications\ResetPassword;
@ -29,8 +31,10 @@
#[CoversClass(UserManagerController::class)]
#[CoversClass(UserManagerResource::class)]
#[CoversClass(UserPolicy::class)]
#[CoversClass(UserAuthenticationResource::class)]
#[CoversClass(User::class)]
#[CoversClass(UserObserver::class)]
#[CoversClass(UserPolicy::class)]
class UserManagerControllerTest extends FeatureTestCase
{
/**
@ -147,6 +151,17 @@ public function test_show_returns_the_expected_UserManagerResource() : void
]);
}
#[Test]
public function test_show_returns_forbidden_to_non_admin_user() : void
{
$this->actingAs($this->user, 'api-guard')
->json('GET', '/api/v1/users/' . $this->anotherUser->id)
->assertForbidden()
->assertJsonStructure([
'message',
]);
}
#[Test]
public function test_resetPassword_resets_password_and_sends_password_reset_to_user()
{
@ -299,6 +314,23 @@ public function test_store_returns_UserManagerResource_of_created_admin() : void
$response->assertExactJson($resource->response($request)->getData(true));
}
#[Test]
public function test_store_another_user_returns_forbidden() : void
{
$this->actingAs($this->user, 'api-guard')
->json('POST', '/api/v1/users', [
'name' => self::USERNAME,
'email' => self::EMAIL,
'password' => self::PASSWORD,
'password_confirmation' => self::PASSWORD,
'is_admin' => false,
])
->assertForbidden()
->assertJsonStructure([
'message',
]);
}
#[Test]
public function test_revokePATs_flushes_pats()
{
@ -437,6 +469,17 @@ public function test_destroy_the_only_admin_returns_forbidden()
->assertForbidden();
}
#[Test]
public function test_destroy_another_user_returns_forbidden() : void
{
$this->actingAs($this->user, 'api-guard')
->json('DELETE', '/api/v1/users/' . $this->anotherUser->id)
->assertForbidden()
->assertJsonStructure([
'message',
]);
}
#[Test]
public function test_promote_changes_admin_status() : void
{
@ -468,6 +511,19 @@ public function test_promote_returns_UserManagerResource() : void
$response->assertExactJson($resources->response($request)->getData(true));
}
#[Test]
public function test_promote_another_user_returns_forbidden() : void
{
$this->actingAs($this->user, 'api-guard')
->json('PATCH', '/api/v1/users/' . $this->anotherUser->id . '/promote', [
'is_admin' => true,
])
->assertForbidden()
->assertJsonStructure([
'message',
]);
}
#[Test]
public function test_demote_returns_UserManagerResource() : void
{

View File

@ -0,0 +1,111 @@
<?php declare(strict_types=1);
namespace Tests\Api\v1\Requests\DataProviders;
final class TwoFAccountDataProvider
{
/**
*
*/
public static function validIdsProvider(): array
{
return [
[[
'ids' => '1',
]],
[[
'ids' => '1,2,5',
]],
];
}
/**
*
*/
public static function invalidIdsProvider(): array
{
return [
[[
'ids' => '', // required
]],
[[
'ids' => null, // required
]],
[[
'ids' => true, // string
]],
[[
'ids' => 10, // string
]],
[[
'ids' => 'notaCommaSeparatedList', // regex
]],
[[
'ids' => 'a,b', // regex
]],
[[
'ids' => 'a,1', // regex
]],
[[
'ids' => ',1,2', // regex
]],
[[
'ids' => '1,,2', // regex
]],
[[
'ids' => '1,2,', // regex
]],
[[
'ids' => ',1,2,', // regex
]],
[[
'ids' => '1;2', // regex
]],
];
}
/**
*
*/
public static function validIsAdminProvider(): array
{
return [
[[
'is_admin' => true,
]],
[[
'is_admin' => false,
]],
[[
'is_admin' => 0,
]],
[[
'is_admin' => 1,
]],
];
}
/**
*
*/
public static function invalidIsAdminProvider(): array
{
return [
[[
'is_admin' => [],
]],
[[
'is_admin' => null,
]],
[[
'is_admin' => 'string',
]],
[[
'is_admin' => '',
]],
[[
'is_admin' => 5,
]],
];
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Tests\Api\v1\Requests;
use App\Api\v1\Requests\GroupAssignRequest;
use App\Api\v1\Requests\IconFetchRequest;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* IconFetchRequestTest test class
*/
#[CoversClass(IconFetchRequest::class)]
class IconFetchRequestTest extends TestCase
{
use WithoutMiddleware;
#[Test]
public function test_user_is_authorized()
{
Auth::shouldReceive('check')
->once()
->andReturn(true);
$request = new IconFetchRequest();
$this->assertTrue($request->authorize());
}
#[Test]
#[DataProvider('provideValidData')]
public function test_valid_data(array $data) : void
{
$request = new IconFetchRequest();
$validator = Validator::make($data, $request->rules());
$this->assertFalse($validator->fails());
}
/**
* Provide Valid data for validation test
*/
public static function provideValidData() : array
{
return [
[[
'service' => 'validWord',
]],
[[
'service' => '0',
]],
[[
'service' => '~string.with-sp3ci@l-ch4rs',
]],
];
}
#[Test]
#[DataProvider('provideInvalidData')]
public function test_invalid_data(array $data) : void
{
$request = new IconFetchRequest();
$validator = Validator::make($data, $request->rules());
$this->assertTrue($validator->fails());
}
/**
* Provide invalid data for validation test
*/
public static function provideInvalidData() : array
{
return [
[[
'service' => null,
]],
[[
'service' => 0,
]],
[[
'service' => true,
]],
[[
'service' => [],
]],
];
}
}

View File

@ -2,7 +2,6 @@
namespace Tests\Api\v1\Requests;
use App\Api\v1\Requests\SettingStoreRequest;
use App\Api\v1\Requests\SettingUpdateRequest;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Auth;
@ -15,7 +14,7 @@
/**
* SettingUpdateRequestTest test class
*/
#[CoversClass(SettingStoreRequest::class)]
#[CoversClass(SettingUpdateRequest::class)]
class SettingUpdateRequestTest extends TestCase
{
use WithoutMiddleware;

View File

@ -7,8 +7,9 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Test;
use Tests\Api\v1\Requests\DataProviders\TwoFAccountDataProvider;
use Tests\TestCase;
/**
@ -32,7 +33,7 @@ public function test_user_is_authorized()
}
#[Test]
#[DataProvider('provideValidData')]
#[DataProviderExternal(TwoFAccountDataProvider::class, 'validIdsProvider')]
public function test_valid_data(array $data) : void
{
$request = new TwoFAccountBatchRequest();
@ -41,23 +42,8 @@ public function test_valid_data(array $data) : void
$this->assertFalse($validator->fails());
}
/**
* Provide Valid data for validation test
*/
public static function provideValidData() : array
{
return [
[[
'ids' => '1',
]],
[[
'ids' => '1,2,5',
]],
];
}
#[Test]
#[DataProvider('provideInvalidData')]
#[DataProviderExternal(TwoFAccountDataProvider::class, 'invalidIdsProvider')]
public function test_invalid_data(array $data) : void
{
$request = new TwoFAccountBatchRequest();
@ -65,49 +51,4 @@ public function test_invalid_data(array $data) : void
$this->assertTrue($validator->fails());
}
/**
* Provide invalid data for validation test
*/
public static function provideInvalidData() : array
{
return [
[[
'ids' => '', // required
]],
[[
'ids' => null, // required
]],
[[
'ids' => true, // string
]],
[[
'ids' => 10, // string
]],
[[
'ids' => 'notaCommaSeparatedList', // regex
]],
[[
'ids' => 'a,b', // regex
]],
[[
'ids' => 'a,1', // regex
]],
[[
'ids' => ',1,2', // regex
]],
[[
'ids' => '1,,2', // regex
]],
[[
'ids' => '1,2,', // regex
]],
[[
'ids' => ',1,2,', // regex
]],
[[
'ids' => '1;2', // regex
]],
];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Tests\Api\v1\Requests;
use App\Api\v1\Requests\TwoFAccountIndexRequest;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Test;
use Tests\Api\v1\Requests\DataProviders\TwoFAccountDataProvider;
use Tests\TestCase;
/**
* TwoFAccountIndexRequestTestTest test class
*/
#[CoversClass(TwoFAccountIndexRequest::class)]
class TwoFAccountIndexRequestTest extends TestCase
{
use WithoutMiddleware;
#[Test]
public function test_user_is_authorized()
{
Auth::shouldReceive('check')
->once()
->andReturn(true);
$request = new TwoFAccountIndexRequest();
$this->assertTrue($request->authorize());
}
#[Test]
#[DataProviderExternal(TwoFAccountDataProvider::class, 'validIdsProvider')]
public function test_valid_data(array $data) : void
{
$request = new TwoFAccountIndexRequest();
$validator = Validator::make($data, $request->rules());
$this->assertFalse($validator->fails());
}
#[Test]
#[DataProviderExternal(TwoFAccountDataProvider::class, 'invalidIdsProvider')]
public function test_invalid_data(array $data) : void
{
$request = new TwoFAccountIndexRequest();
$validator = Validator::make($data, $request->rules());
$this->assertTrue($validator->fails());
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Tests\Api\v1\Requests;
use App\Api\v1\Requests\UserManagerPromoteRequest;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* UserManagerPromoteRequestTest test class
*/
#[CoversClass(UserManagerPromoteRequest::class)]
class UserManagerPromoteRequestTest extends TestCase
{
use WithoutMiddleware;
#[Test]
public function test_user_is_authorized()
{
Auth::shouldReceive('user->isAdministrator')
->once()
->andReturn(true);
$request = new UserManagerPromoteRequest();
$this->assertTrue($request->authorize());
}
#[Test]
#[DataProvider('provideValidData')]
public function test_valid_data(array $data) : void
{
$request = new UserManagerPromoteRequest();
$validator = Validator::make($data, $request->rules());
$this->assertFalse($validator->fails());
}
/**
* Provide Valid data for validation test
*/
public static function provideValidData() : array
{
return [
[[
'is_admin' => true,
]],
[[
'is_admin' => false,
]],
[[
'is_admin' => 0,
]],
[[
'is_admin' => 1,
]],
];
}
#[Test]
#[DataProvider('provideInvalidData')]
public function test_invalid_data(array $data) : void
{
$request = new UserManagerPromoteRequest();
$validator = Validator::make($data, $request->rules());
$this->assertTrue($validator->fails());
}
/**
* Provide invalid data for validation test
*/
public static function provideInvalidData() : array
{
return [
[[
'is_admin' => [],
]],
[[
'is_admin' => null,
]],
[[
'is_admin' => 'string',
]],
[[
'is_admin' => '',
]],
[[
'is_admin' => 5,
]],
];
}
}

View File

@ -0,0 +1,181 @@
<?php
namespace Tests\Api\v1\Requests;
use App\Api\v1\Requests\UserManagerStoreRequest;
use App\Models\User;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
/**
* UserManagerStoreRequestTest test class
*/
#[CoversClass(UserManagerStoreRequest::class)]
class UserManagerStoreRequestTest extends FeatureTestCase
{
use WithoutMiddleware;
#[Test]
public function test_admin_is_authorized()
{
Auth::shouldReceive('user->isAdministrator')
->once()
->andReturn(true);
$request = new UserManagerStoreRequest();
$this->assertTrue($request->authorize());
}
#[Test]
#[DataProvider('provideValidData')]
public function test_valid_data(array $data) : void
{
User::factory()->create([
'name' => 'Jane',
'email' => 'jane@example.com',
]);
$request = new UserManagerStoreRequest();
$validator = Validator::make($data, $request->rules());
$this->assertFalse($validator->fails());
}
/**
* Provide Valid data for validation test
*/
public static function provideValidData() : array
{
return [
[[
'name' => 'John',
'email' => 'john@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => 'JOHN@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => 0,
]],
];
}
#[Test]
#[DataProvider('provideInvalidData')]
public function test_invalid_data(array $data) : void
{
User::factory()->create([
'name' => 'John',
'email' => 'john@example.com',
]);
$request = new UserManagerStoreRequest();
$validator = Validator::make($data, $request->rules());
$this->assertTrue($validator->fails());
}
/**
* Provide invalid data for validation test
*/
public static function provideInvalidData() : array
{
return [
[[
'name' => 'John',
'email' => 'john@example.com', // unique
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => true,
]],
[[
'name' => '', // required
'email' => 'john@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => '', // required
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => true,
]],
[[
'name' => 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz', // max:255
'email' => 'john@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz@example.com', // max:255
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => 'johnexample.com', // email
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => 'john@example.com',
'password' => '', // required
'password_confirmation' => '', // required
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => 'john@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'anotherPassword', // confirmed
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => 'john@example.com',
'password' => 'pwd', // min:8
'password_confirmation' => 'pwd',
'is_admin' => true,
]],
[[
'name' => 'John',
'email' => 'john@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => null, // required
]],
[[
'name' => 'John',
'email' => 'john@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => '', // required
]],
[[
'name' => 'John',
'email' => 'john@example.com',
'password' => 'MyPassword',
'password_confirmation' => 'MyPassword',
'is_admin' => 'string', // boolean
]],
];
}
}

23
tests/Feature/AppTest.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace Tests\Feature;
use App\Providers\EventServiceProvider;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
/**
* AppTest class
*/
#[CoversClass(EventServiceProvider::class)]
class AppTest extends FeatureTestCase
{
#[Test]
public function test_events_should_be_explicitly_registered()
{
$eventServiceProvider = app()->getProvider(EventServiceProvider::class);
$this->assertFalse($eventServiceProvider->shouldDiscoverEvents());
}
}

View File

@ -12,9 +12,12 @@
use App\Http\Middleware\SkipIfAuthenticated;
use App\Listeners\Authentication\FailedLoginListener;
use App\Listeners\Authentication\LoginListener;
use App\Listeners\Authentication\LogoutListener;
use App\Listeners\LogNotificationListener;
use App\Models\AuthLog;
use App\Models\User;
use App\Notifications\FailedLogin;
use App\Notifications\SignedInWithNewDevice;
use App\Notifications\FailedLoginNotification;
use App\Notifications\SignedInWithNewDeviceNotification;
use App\Rules\CaseInsensitiveEmailExists;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
@ -32,12 +35,16 @@
#[CoversClass(RejectIfReverseProxy::class)]
#[CoversClass(RejectIfDemoMode::class)]
#[CoversClass(LoginListener::class)]
#[CoversClass(LogoutListener::class)]
#[CoversClass(FailedLoginListener::class)]
#[CoversMethod(CaseInsensitiveEmailExists::class, 'handle')]
#[CoversMethod(CaseInsensitiveEmailExists::class, 'validate')]
#[CoversMethod(SkipIfAuthenticated::class, 'handle')]
#[CoversMethod(Handler::class, 'register')]
#[CoversMethod(KickOutInactiveUser::class, 'handle')]
#[CoversMethod(LogUserLastSeen::class, 'handle')]
#[CoversClass(LogNotificationListener::class)]
#[CoversClass(SignedInWithNewDeviceNotification::class)]
#[CoversClass(FailedLoginNotification::class)]
class LoginTest extends FeatureTestCase
{
/**
@ -50,6 +57,8 @@ class LoginTest extends FeatureTestCase
*/
protected $admin;
private const WEB_GUARD = 'web-guard';
private const PASSWORD = 'password';
private const WRONG_PASSWORD = 'wrong_password';
@ -65,6 +74,8 @@ public function setUp() : void
#[Test]
public function test_user_login_returns_success()
{
Notification::fake();
$response = $this->json('POST', '/user/login', [
'email' => $this->user->email,
'password' => self::PASSWORD,
@ -95,7 +106,7 @@ public function test_login_send_new_device_notification()
'password' => self::PASSWORD,
])->assertOk();
$this->actingAs($this->user, 'web-guard')
$this->actingAs($this->user, self::WEB_GUARD)
->json('GET', '/user/logout');
$this->travel(1)->minute();
@ -107,7 +118,7 @@ public function test_login_send_new_device_notification()
'HTTP_USER_AGENT' => 'NotSymfony',
])->assertOk();
Notification::assertSentTo($this->user, SignedInWithNewDevice::class);
Notification::assertSentTo($this->user, SignedInWithNewDeviceNotification::class);
}
#[Test]
@ -123,7 +134,7 @@ public function test_login_does_not_send_new_device_notification()
'password' => self::PASSWORD,
])->assertOk();
$this->actingAs($this->user, 'web-guard')
$this->actingAs($this->user, self::WEB_GUARD)
->json('GET', '/user/logout');
$this->travel(1)->minute();
@ -170,6 +181,40 @@ public function test_user_login_with_uppercased_email_returns_success()
]);
}
#[Test]
public function test_successful_web_login_with_password_is_logged()
{
$this->json('POST', '/user/login', [
'email' => $this->user->email,
'password' => self::PASSWORD,
])->assertOk();
$this->assertDatabaseHas('auth_logs', [
'authenticatable_id' => $this->user->id,
'login_successful' => true,
'guard' => self::WEB_GUARD,
'login_method' => self::PASSWORD,
'logout_at' => null,
]);
}
#[Test]
public function test_failed_web_login_with_password_is_logged()
{
$this->json('POST', '/user/login', [
'email' => $this->user->email,
'password' => self::WRONG_PASSWORD,
])->assertStatus(401);
$this->assertDatabaseHas('auth_logs', [
'authenticatable_id' => $this->user->id,
'login_successful' => false,
'guard' => self::WEB_GUARD,
'login_method' => self::PASSWORD,
'logout_at' => null,
]);
}
#[Test]
public function test_user_login_already_authenticated_is_rejected()
{
@ -178,7 +223,7 @@ public function test_user_login_already_authenticated_is_rejected()
'password' => self::PASSWORD,
]);
$response = $this->actingAs($this->user, 'web-guard')
$response = $this->actingAs($this->user, self::WEB_GUARD)
->json('POST', '/user/login', [
'email' => $this->user->email,
'password' => self::PASSWORD,
@ -229,7 +274,7 @@ public function test_login_with_invalid_credentials_send_failed_login_notificati
'password' => self::WRONG_PASSWORD,
])->assertStatus(401);
Notification::assertSentTo($this->user, FailedLogin::class);
Notification::assertSentTo($this->user, FailedLoginNotification::class);
}
#[Test]
@ -278,7 +323,7 @@ public function test_user_logout_returns_validation_success()
'password' => self::PASSWORD,
]);
$response = $this->actingAs($this->user, 'web-guard')
$response = $this->actingAs($this->user, self::WEB_GUARD)
->json('GET', '/user/logout')
->assertOk()
->assertExactJson([
@ -308,4 +353,41 @@ public function test_user_logout_after_inactivity_returns_teapot()
->json('GET', '/api/v1/twofaccounts')
->assertStatus(418);
}
#[Test]
public function test_successful_web_logout_is_logged()
{
$this->json('POST', '/user/login', [
'email' => $this->user->email,
'password' => self::PASSWORD,
])->assertOk();
$this->actingAs($this->user, self::WEB_GUARD)
->json('GET', '/user/logout')
->assertOk();
$authlog = AuthLog::first();
$this->assertEquals($this->user->id, $authlog->authenticatable_id);
$this->assertTrue($authlog->login_successful);
$this->assertEquals(self::WEB_GUARD, $authlog->guard);
$this->assertEquals(self::PASSWORD, $authlog->login_method);
$this->assertNotNull($authlog->logout_at);
}
#[Test]
public function test_orphan_web_logout_is_logged()
{
$this->actingAs($this->user, self::WEB_GUARD)
->json('GET', '/user/logout')
->assertOk();
$authlog = AuthLog::first();
$this->assertEquals($this->user->id, $authlog->authenticatable_id);
$this->assertFalse($authlog->login_successful);
$this->assertEquals(self::WEB_GUARD, $authlog->guard);
$this->assertNull($authlog->login_method);
$this->assertNotNull($authlog->logout_at);
}
}

View File

@ -60,42 +60,6 @@ public function test_sendRecoveryEmail_sends_notification_on_success()
]);
}
#[Test]
public function test_WebauthnRecoveryNotification_renders_to_email()
{
$mail = (new WebauthnRecoveryNotification('test_token'))->toMail($this->user)->render();
$this->assertStringContainsString(
'http://localhost/webauthn/recover?token=test_token&amp;email=' . urlencode($this->user->email),
$mail
);
$this->assertStringContainsString(
Lang::get('Recover Account'),
$mail
);
$this->assertStringContainsString(
Lang::get(
'You are receiving this email because we received an account recovery request for your account.'
),
$mail
);
$this->assertStringContainsString(
Lang::get(
'This recovery link will expire in :count minutes.',
['count' => config('auth.passwords.webauthn.expire')]
),
$mail
);
$this->assertStringContainsString(
Lang::get('If you did not request an account recovery, no further action is required.'),
$mail
);
}
#[Test]
public function test_sendRecoveryEmail_does_not_send_anything_to_unknown_email()
{

View File

@ -9,6 +9,7 @@
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversTrait;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
@ -17,7 +18,7 @@
*/
#[CoversClass(WebAuthnManageController::class)]
#[CoversClass(RejectIfReverseProxy::class)]
#[CoversClass(WebAuthnManageCredentials::class)]
#[CoversTrait(WebAuthnManageCredentials::class)]
class WebAuthnManageControllerTest extends FeatureTestCase
{
// use WithoutMiddleware;

View File

@ -6,6 +6,7 @@
use App\Models\User;
use App\Notifications\TestEmailSettingNotification;
use App\Services\ReleaseRadarService;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Notification;
use PHPUnit\Framework\Attributes\CoversClass;
@ -122,17 +123,6 @@ public function test_testEmail_sends_a_notification()
Notification::assertSentTo($this->admin, TestEmailSettingNotification::class);
}
#[Test]
public function test_testEmail_renders_to_email()
{
$mail = (new TestEmailSettingNotification('test_token'))->toMail($this->user)->render();
$this->assertStringContainsString(
Lang::get('notifications.test_email_settings.reason'),
$mail
);
}
#[Test]
public function test_testEmail_returns_unauthorized()
{

View File

@ -6,6 +6,7 @@
use App\Models\Group;
use App\Models\TwoFAccount;
use App\Models\User;
use App\Observers\UserObserver;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
@ -21,6 +22,7 @@
* UserModelTest test class
*/
#[CoversClass(User::class)]
#[CoversClass(UserObserver::class)]
class UserModelTest extends FeatureTestCase
{
#[Test]

View File

@ -0,0 +1,82 @@
<?php
namespace Tests\Feature\Notifications;
use App\Models\AuthLog;
use App\Models\User;
use App\Notifications\FailedLoginNotification;
use Illuminate\Notifications\Messages\MailMessage;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
/**
* FailedLoginTest test class
*/
#[CoversClass(FailedLoginNotification::class)]
class FailedLoginNotificationTest extends FeatureTestCase
{
/**
* @var \App\Models\User
*/
protected $user;
/**
* @var \App\Models\AuthLog
*/
protected $authLog;
/**
* @var \App\Notifications\FailedLoginNotification
*/
protected $failedLogin;
public function setUp() : void
{
parent::setUp();
$this->user = User::factory()->create();
AuthLog::factory()->for($this->user, 'authenticatable')->failedLogin()->create();
$this->authLog = AuthLog::first();
$this->failedLogin = new FailedLoginNotification($this->authLog);
}
#[Test]
public function test_it_renders_to_email()
{
$mail = $this->failedLogin->toMail($this->user);
$this->assertInstanceOf(MailMessage::class, $mail);
}
#[Test]
public function test_rendered_email_contains_expected_data()
{
$mail = $this->failedLogin->toMail($this->user)->render();
$this->assertStringContainsString(
$this->authLog->login_at->toCookieString(),
$mail
);
$this->assertStringContainsString(
$this->authLog->ip_address,
$mail
);
$this->assertStringContainsString(
$this->user->name,
$mail
);
$this->assertStringContainsString(
__('admin.browser_on_platform', ['browser' => 'Firefox', 'platform' => 'Windows']),
$mail
);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Tests\Feature\Notifications;
use App\Models\AuthLog;
use App\Models\User;
use App\Notifications\SignedInWithNewDeviceNotification;
use Illuminate\Notifications\Messages\MailMessage;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
/**
* SignedInWithNewDeviceNotificationTest test class
*/
#[CoversClass(SignedInWithNewDeviceNotification::class)]
class SignedInWithNewDeviceNotificationTest extends FeatureTestCase
{
/**
* @var \App\Models\User
*/
protected $user;
/**
* @var \App\Models\AuthLog
*/
protected $authLog;
/**
* @var \App\Notifications\SignedInWithNewDeviceNotification
*/
protected $signedInWithNewDevice;
public function setUp() : void
{
parent::setUp();
$this->user = User::factory()->create();
AuthLog::factory()->for($this->user, 'authenticatable')->failedLogin()->create();
$this->authLog = AuthLog::first();
$this->signedInWithNewDevice = new SignedInWithNewDeviceNotification($this->authLog);
}
#[Test]
public function test_it_renders_to_email()
{
$mail = $this->signedInWithNewDevice->toMail($this->user);
$this->assertInstanceOf(MailMessage::class, $mail);
}
#[Test]
public function test_rendered_email_contains_expected_data()
{
$mail = $this->signedInWithNewDevice->toMail($this->user)->render();
$this->assertStringContainsString(
$this->authLog->login_at->toCookieString(),
$mail
);
$this->assertStringContainsString(
$this->authLog->ip_address,
$mail
);
$this->assertStringContainsString(
$this->user->name,
$mail
);
$this->assertStringContainsString(
__('admin.browser_on_platform', ['browser' => 'Firefox', 'platform' => 'Windows']),
$mail
);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Tests\Feature\Notifications;
use App\Models\User;
use App\Notifications\TestEmailSettingNotification;
use Illuminate\Notifications\Messages\MailMessage;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
/**
* TestEmailSettingNotificationTest test class
*/
#[CoversClass(TestEmailSettingNotification::class)]
class TestEmailSettingNotificationTest extends FeatureTestCase
{
/**
* @var \App\Models\User
*/
protected $user;
/**
* @var \App\Notifications\TestEmailSettingNotification
*/
protected $testEmailSettingNotification;
public function setUp() : void
{
parent::setUp();
$this->user = User::factory()->create();
$this->testEmailSettingNotification = new TestEmailSettingNotification('test_token');
}
#[Test]
public function test_it_renders_to_email()
{
$mail = $this->testEmailSettingNotification->toMail($this->user);
$this->assertInstanceOf(MailMessage::class, $mail);
}
#[Test]
public function test_rendered_email_contains_expected_data()
{
$mail = $this->testEmailSettingNotification->toMail($this->user)->render();
$this->assertStringContainsString(
__('notifications.test_email_settings.success'),
$mail
);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Tests\Feature\Notifications;
use App\Models\User;
use App\Notifications\WebauthnRecoveryNotification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Lang;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
/**
* WebauthnRecoveryNotificationTest test class
*/
#[CoversClass(WebauthnRecoveryNotification::class)]
class WebauthnRecoveryNotificationTest extends FeatureTestCase
{
/**
* @var \App\Models\User
*/
protected $user;
/**
* @var \App\Notifications\WebauthnRecoveryNotification
*/
protected $webauthnRecoveryNotification;
public function setUp() : void
{
parent::setUp();
$this->user = User::factory()->create();
$this->webauthnRecoveryNotification = new WebauthnRecoveryNotification('test_token');
}
#[Test]
public function test_it_renders_to_email()
{
$mail = $this->webauthnRecoveryNotification->toMail($this->user);
$this->assertInstanceOf(MailMessage::class, $mail);
}
#[Test]
public function test_rendered_email_contains_expected_data()
{
$mail = $this->webauthnRecoveryNotification->toMail($this->user)->render();
$this->assertStringContainsString(
'http://localhost/webauthn/recover?token=test_token&amp;email=' . urlencode($this->user->email),
$mail
);
$this->assertStringContainsString(
Lang::get('Recover Account'),
$mail
);
$this->assertStringContainsString(
Lang::get(
'You are receiving this email because we received an account recovery request for your account.'
),
$mail
);
$this->assertStringContainsString(
Lang::get(
'This recovery link will expire in :count minutes.',
['count' => config('auth.passwords.webauthn.expire')]
),
$mail
);
$this->assertStringContainsString(
Lang::get('If you did not request an account recovery, no further action is required.'),
$mail
);
}
}

View File

@ -2,11 +2,22 @@
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\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\FeatureTestCase;
#[CoversMethod(RouteServiceProvider::class, 'boot')]
class RouteTest extends FeatureTestCase
{
const API_ROUTE_PREFIX = 'api/v1';
const API_MIDDLEWARE = 'api.v1';
#[Test]
public function test_landing_view_is_returned()
{
@ -23,4 +34,40 @@ public function test_exception_handler_with_web_route()
$response->assertStatus(405);
}
#[Test]
public function test_all_api_routes_are_behind_apiv1_middleware()
{
$routes = Route::getRoutes();
foreach ($routes as $route) {
$middlewares = Route::gatherRouteMiddleware($route);
if (Str::startsWith($route->uri(), self::API_ROUTE_PREFIX)) {
$this->assertEquals(self::API_ROUTE_PREFIX, $route->getPrefix());
$this->assertTrue(in_array(self::API_MIDDLEWARE, $middlewares));
}
}
}
#[Test]
#[DataProvider('wherePatternProvider')]
public function test_router_has_expected_global_where_patterns($pattern)
{
$patterns = Route::getPatterns();
$this->assertArrayHasKey($pattern, $patterns);
}
/**
* Provide data for tests
*/
public static function wherePatternProvider()
{
return [
'SETTING_NAME' => ['settingName']
];
}
}

View File

@ -2,7 +2,7 @@
namespace Tests\Unit\Listeners;
use App\Listeners\LogNotification;
use App\Listeners\LogNotificationListener;
use Illuminate\Notifications\Events\NotificationSent;
use Illuminate\Support\Facades\Event;
use PHPUnit\Framework\Attributes\CoversClass;
@ -10,9 +10,9 @@
use Tests\TestCase;
/**
* ResetUsersPreferenceTest test class
* LogNotificationTest test class
*/
#[CoversClass(LogNotification::class)]
#[CoversClass(LogNotificationListener::class)]
class LogNotificationTest extends TestCase
{
#[Test]
@ -22,7 +22,7 @@ public function test_LogNotificationTest_listen_to_NotificationSent_event()
Event::assertListening(
NotificationSent::class,
LogNotification::class
LogNotificationListener::class
);
}
}