feat: L'Ami Fiduciaire V1.0.0 — full codebase with Story 0.1 complete
Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12, Vue 3, Inertia.js 2, and Tailwind CSS 4. Story 0.1 (rename folders to declarations in database) is implemented and code-reviewed: migration, rollback, and 6 Pest tests all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
84
tests/Feature/Auth/AuthenticationTest.php
Normal file
84
tests/Feature/Auth/AuthenticationTest.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
test('login screen can be rendered', function () {
|
||||
$response = $this->get(route('login'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('users can authenticate using the login screen', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
});
|
||||
|
||||
test('users with two factor enabled are redirected to two factor challenge', function () {
|
||||
if (! Features::canManageTwoFactorAuthentication()) {
|
||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
||||
}
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->forceFill([
|
||||
'two_factor_secret' => encrypt('test-secret'),
|
||||
'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$response = $this->post(route('login'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('two-factor.login'));
|
||||
$response->assertSessionHas('login.id', $user->id);
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('users can not authenticate with invalid password', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('users can logout', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post(route('logout'));
|
||||
|
||||
$this->assertGuest();
|
||||
$response->assertRedirect(route('home'));
|
||||
});
|
||||
|
||||
test('users are rate limited', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
RateLimiter::increment(md5('login'.implode('|', [$user->email, '127.0.0.1'])), amount: 5);
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertTooManyRequests();
|
||||
});
|
||||
95
tests/Feature/Auth/EmailVerificationTest.php
Normal file
95
tests/Feature/Auth/EmailVerificationTest.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
test('email verification screen can be rendered', function () {
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('email can be verified', function () {
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$response = $this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
|
||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
});
|
||||
|
||||
test('email is not verified with invalid hash', function () {
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
|
||||
});
|
||||
|
||||
test('email is not verified with invalid user id', function () {
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => 123, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
|
||||
});
|
||||
|
||||
test('verified user is redirected to dashboard from verification prompt', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
});
|
||||
|
||||
test('already verified user visiting verification link is redirected without firing event again', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl)
|
||||
->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
|
||||
});
|
||||
22
tests/Feature/Auth/PasswordConfirmationTest.php
Normal file
22
tests/Feature/Auth/PasswordConfirmationTest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
test('confirm password screen can be rendered', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('password.confirm'));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/ConfirmPassword'),
|
||||
);
|
||||
});
|
||||
|
||||
test('password confirmation requires authentication', function () {
|
||||
$response = $this->get(route('password.confirm'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
73
tests/Feature/Auth/PasswordResetTest.php
Normal file
73
tests/Feature/Auth/PasswordResetTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
test('reset password link screen can be rendered', function () {
|
||||
$response = $this->get(route('password.request'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('reset password link can be requested', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
});
|
||||
|
||||
test('reset password screen can be rendered', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get(route('password.reset', $notification->token));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('password can be reset with valid token', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$response = $this->post(route('password.update'), [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('password cannot be reset with invalid token', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post(route('password.update'), [
|
||||
'token' => 'invalid-token',
|
||||
'email' => $user->email,
|
||||
'password' => 'newpassword123',
|
||||
'password_confirmation' => 'newpassword123',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
19
tests/Feature/Auth/RegistrationTest.php
Normal file
19
tests/Feature/Auth/RegistrationTest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
test('registration screen can be rendered', function () {
|
||||
$response = $this->get(route('register'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('new users can register', function () {
|
||||
$response = $this->post(route('register.store'), [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
});
|
||||
45
tests/Feature/Auth/TwoFactorChallengeTest.php
Normal file
45
tests/Feature/Auth/TwoFactorChallengeTest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
test('two factor challenge redirects to login when not authenticated', function () {
|
||||
if (! Features::canManageTwoFactorAuthentication()) {
|
||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
||||
}
|
||||
|
||||
$response = $this->get(route('two-factor.login'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('two factor challenge can be rendered', function () {
|
||||
if (! Features::canManageTwoFactorAuthentication()) {
|
||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
||||
}
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->forceFill([
|
||||
'two_factor_secret' => encrypt('test-secret'),
|
||||
'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$this->post(route('login'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->get(route('two-factor.login'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/TwoFactorChallenge'),
|
||||
);
|
||||
});
|
||||
29
tests/Feature/Auth/VerificationNotificationTest.php
Normal file
29
tests/Feature/Auth/VerificationNotificationTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
test('sends verification notification', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('verification.send'))
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('does not send verification notification if email is verified', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('verification.send'))
|
||||
->assertRedirect(route('dashboard', absolute: false));
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
152
tests/Feature/Client/ClientContactTest.php
Normal file
152
tests/Feature/Client/ClientContactTest.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
function createWorkspaceWithUser(): array
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
|
||||
return [$user, $workspace];
|
||||
}
|
||||
|
||||
test('can store a client with a single contact', function () {
|
||||
[$user, $workspace] = createWorkspaceWithUser();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('clients.store'), [
|
||||
'company_name' => 'Test SARL',
|
||||
'legal_form' => 'sarl',
|
||||
'contacts' => [
|
||||
[
|
||||
'full_name' => 'Ahmed Bennani',
|
||||
'job_title' => 'Gérant',
|
||||
'email' => 'ahmed@test.com',
|
||||
'phone' => '0612345678',
|
||||
'is_principal' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$client = Client::query()->where('company_name', 'Test SARL')->first();
|
||||
expect($client)->not->toBeNull();
|
||||
expect($client->contacts)->toHaveCount(1);
|
||||
expect($client->contacts->first()->full_name)->toBe('Ahmed Bennani');
|
||||
expect($client->contacts->first()->is_principal)->toBeTrue();
|
||||
});
|
||||
|
||||
test('can store a client with multiple contacts', function () {
|
||||
[$user, $workspace] = createWorkspaceWithUser();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('clients.store'), [
|
||||
'company_name' => 'Multi Contact SARL',
|
||||
'legal_form' => 'sarl',
|
||||
'contacts' => [
|
||||
[
|
||||
'full_name' => 'Contact A',
|
||||
'email' => 'a@test.com',
|
||||
'is_principal' => false,
|
||||
],
|
||||
[
|
||||
'full_name' => 'Contact B',
|
||||
'email' => 'b@test.com',
|
||||
'is_principal' => true,
|
||||
],
|
||||
[
|
||||
'full_name' => 'Contact C',
|
||||
'email' => 'c@test.com',
|
||||
'is_principal' => false,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$client = Client::query()->where('company_name', 'Multi Contact SARL')->first();
|
||||
expect($client->contacts)->toHaveCount(3);
|
||||
|
||||
$principal = $client->contacts->firstWhere('is_principal', true);
|
||||
expect($principal->full_name)->toBe('Contact B');
|
||||
});
|
||||
|
||||
test('store validation fails with no contacts', function () {
|
||||
[$user, $workspace] = createWorkspaceWithUser();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('clients.store'), [
|
||||
'company_name' => 'No Contact SARL',
|
||||
'legal_form' => 'sarl',
|
||||
'contacts' => [],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('contacts');
|
||||
});
|
||||
|
||||
test('store validation fails with no principal contact', function () {
|
||||
[$user, $workspace] = createWorkspaceWithUser();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('clients.store'), [
|
||||
'company_name' => 'No Principal SARL',
|
||||
'legal_form' => 'sarl',
|
||||
'contacts' => [
|
||||
['full_name' => 'Test', 'email' => 't@t.com', 'is_principal' => false],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('contacts');
|
||||
});
|
||||
|
||||
test('store validation fails with multiple principal contacts', function () {
|
||||
[$user, $workspace] = createWorkspaceWithUser();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('clients.store'), [
|
||||
'company_name' => 'Multi Principal SARL',
|
||||
'legal_form' => 'sarl',
|
||||
'contacts' => [
|
||||
['full_name' => 'A', 'email' => 'a@t.com', 'is_principal' => true],
|
||||
['full_name' => 'B', 'email' => 'b@t.com', 'is_principal' => true],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('contacts');
|
||||
});
|
||||
|
||||
test('update can add and remove contacts', function () {
|
||||
[$user, $workspace] = createWorkspaceWithUser();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$existingContact = $client->contacts->first();
|
||||
|
||||
$response = $this->actingAs($user)->put(route('clients.update', $client), [
|
||||
'company_name' => $client->company_name,
|
||||
'legal_form' => $client->legal_form->value,
|
||||
'contacts' => [
|
||||
[
|
||||
'full_name' => 'New Contact',
|
||||
'email' => 'new@test.com',
|
||||
'is_principal' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$client->refresh();
|
||||
expect($client->contacts)->toHaveCount(1);
|
||||
expect($client->contacts->first()->full_name)->toBe('New Contact');
|
||||
expect(ClientContact::find($existingContact->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('primary contact email accessor returns principal contact email', function () {
|
||||
$client = Client::factory()->create();
|
||||
$principal = $client->contacts->firstWhere('is_principal', true);
|
||||
|
||||
expect($client->primary_contact_email)->toBe($principal->email);
|
||||
});
|
||||
34
tests/Feature/Client/ClientEmailTest.php
Normal file
34
tests/Feature/Client/ClientEmailTest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
|
||||
test('primary_contact_email returns principal contact email', function () {
|
||||
$client = Client::factory()->create();
|
||||
$principal = $client->contacts->firstWhere('is_principal', true);
|
||||
|
||||
expect($client->primary_contact_email)->toBe($principal->email);
|
||||
});
|
||||
|
||||
test('primary_contact_email falls back to old contact_email column', function () {
|
||||
$client = Client::factory()->create([
|
||||
'contact_email' => 'fallback@test.com',
|
||||
]);
|
||||
|
||||
// Remove all contacts to force fallback
|
||||
$client->contacts()->delete();
|
||||
$client->unsetRelation('primaryContact');
|
||||
|
||||
expect($client->primary_contact_email)->toBe('fallback@test.com');
|
||||
});
|
||||
|
||||
test('primary_contact_email returns null when no contact exists', function () {
|
||||
$client = Client::factory()->create([
|
||||
'contact_email' => null,
|
||||
]);
|
||||
|
||||
$client->contacts()->delete();
|
||||
$client->unsetRelation('primaryContact');
|
||||
|
||||
expect($client->primary_contact_email)->toBeNull();
|
||||
});
|
||||
16
tests/Feature/DashboardTest.php
Normal file
16
tests/Feature/DashboardTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
test('guests are redirected to the login page', function () {
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('authenticated users can visit the dashboard', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertOk();
|
||||
});
|
||||
51
tests/Feature/Database/RenameFoldersToDeclarationsTest.php
Normal file
51
tests/Feature/Database/RenameFoldersToDeclarationsTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
test('declarations table exists after migration', function () {
|
||||
expect(Schema::hasTable('declarations'))->toBeTrue();
|
||||
expect(Schema::hasTable('folders'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('declaration_invitations table exists after migration', function () {
|
||||
expect(Schema::hasTable('declaration_invitations'))->toBeTrue();
|
||||
expect(Schema::hasTable('folder_invitations'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('messages table has declaration_id column instead of folder_id', function () {
|
||||
expect(Schema::hasColumn('messages', 'declaration_id'))->toBeTrue();
|
||||
expect(Schema::hasColumn('messages', 'folder_id'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('declaration_invitations table has declaration_id column instead of folder_id', function () {
|
||||
expect(Schema::hasColumn('declaration_invitations', 'declaration_id'))->toBeTrue();
|
||||
expect(Schema::hasColumn('declaration_invitations', 'folder_id'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('composite index exists on messages declaration_id and created_at', function () {
|
||||
$indexes = collect(DB::select("PRAGMA index_list('messages')"));
|
||||
$indexColumns = $indexes->flatMap(function ($index) {
|
||||
return collect(DB::select("PRAGMA index_info('{$index->name}')"))
|
||||
->pluck('name')
|
||||
->all();
|
||||
});
|
||||
|
||||
expect($indexColumns->contains('declaration_id'))->toBeTrue();
|
||||
expect($indexColumns->contains('created_at'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('migration is reversible and rollback restores folder tables', function () {
|
||||
// Rollback the rename migration (last one applied)
|
||||
$this->artisan('migrate:rollback', ['--step' => 1]);
|
||||
|
||||
expect(Schema::hasTable('folders'))->toBeTrue();
|
||||
expect(Schema::hasTable('declarations'))->toBeFalse();
|
||||
expect(Schema::hasTable('folder_invitations'))->toBeTrue();
|
||||
expect(Schema::hasTable('declaration_invitations'))->toBeFalse();
|
||||
expect(Schema::hasColumn('messages', 'folder_id'))->toBeTrue();
|
||||
expect(Schema::hasColumn('messages', 'declaration_id'))->toBeFalse();
|
||||
|
||||
// Re-apply to leave DB in correct state for other tests
|
||||
$this->artisan('migrate');
|
||||
});
|
||||
7
tests/Feature/ExampleTest.php
Normal file
7
tests/Feature/ExampleTest.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
test('returns a successful response', function () {
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
134
tests/Feature/Folder/FolderTypeTest.php
Normal file
134
tests/Feature/Folder/FolderTypeTest.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
test('can create vat_monthly folder requiring month', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Mensuelle Mars 2026',
|
||||
'type' => 'vat_monthly',
|
||||
'period_year' => 2026,
|
||||
'period_month' => 3,
|
||||
'status' => 'draft',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$folder = $client->folders()->where('type', 'vat_monthly')->first();
|
||||
expect($folder)->not->toBeNull();
|
||||
expect($folder->period_month)->toBe(3);
|
||||
expect($folder->period_quarter)->toBeNull();
|
||||
});
|
||||
|
||||
test('vat_monthly validation fails without month', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Mensuelle Sans Mois',
|
||||
'type' => 'vat_monthly',
|
||||
'period_year' => 2026,
|
||||
'status' => 'draft',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('period_month');
|
||||
});
|
||||
|
||||
test('can create vat_quarterly folder requiring quarter', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Trimestrielle T1 2026',
|
||||
'type' => 'vat_quarterly',
|
||||
'period_year' => 2026,
|
||||
'period_quarter' => 1,
|
||||
'status' => 'draft',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$folder = $client->folders()->where('type', 'vat_quarterly')->first();
|
||||
expect($folder)->not->toBeNull();
|
||||
expect($folder->period_quarter)->toBe(1);
|
||||
expect($folder->period_month)->toBeNull();
|
||||
});
|
||||
|
||||
test('vat_quarterly validation fails without quarter', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Trimestrielle Sans Trimestre',
|
||||
'type' => 'vat_quarterly',
|
||||
'period_year' => 2026,
|
||||
'status' => 'draft',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('period_quarter');
|
||||
});
|
||||
|
||||
test('server rejects old vat type', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'Old VAT',
|
||||
'type' => 'vat',
|
||||
'period_year' => 2026,
|
||||
'status' => 'draft',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('type');
|
||||
});
|
||||
|
||||
test('vat_monthly nulls quarter field server-side', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('folders.store'), [
|
||||
'client_id' => $client->id,
|
||||
'title' => 'TVA Mensuelle With Quarter',
|
||||
'type' => 'vat_monthly',
|
||||
'period_year' => 2026,
|
||||
'period_month' => 6,
|
||||
'period_quarter' => 2,
|
||||
'status' => 'draft',
|
||||
'priority' => 'medium',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$folder = $client->folders()->where('title', 'TVA Mensuelle With Quarter')->first();
|
||||
expect($folder->period_quarter)->toBeNull();
|
||||
expect($folder->period_month)->toBe(6);
|
||||
});
|
||||
103
tests/Feature/Folder/MediaDownloadTest.php
Normal file
103
tests/Feature/Folder/MediaDownloadTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Folder;
|
||||
use App\Models\MediaDownload;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
function setupFolderWithMedia(): array
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user, ['role' => 'owner']);
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$folder = Folder::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
Storage::fake('public');
|
||||
$file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf');
|
||||
$media = $folder->addMedia($file)->toMediaCollection('documents');
|
||||
|
||||
return [$user, $workspace, $folder, $media];
|
||||
}
|
||||
|
||||
test('downloading creates a media download record', function () {
|
||||
[$user, $workspace, $folder, $media] = setupFolderWithMedia();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$this->actingAs($user)->get(route('folders.media.download', [
|
||||
'folder' => $folder,
|
||||
'mediaId' => $media->id,
|
||||
]));
|
||||
|
||||
$download = MediaDownload::query()
|
||||
->where('media_id', $media->id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
expect($download)->not->toBeNull();
|
||||
expect($download->downloaded_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('re-downloading updates timestamp without creating duplicates', function () {
|
||||
[$user, $workspace, $folder, $media] = setupFolderWithMedia();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$this->actingAs($user)->get(route('folders.media.download', [
|
||||
'folder' => $folder,
|
||||
'mediaId' => $media->id,
|
||||
]));
|
||||
|
||||
$firstDownload = MediaDownload::query()
|
||||
->where('media_id', $media->id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
$firstTimestamp = $firstDownload->downloaded_at;
|
||||
|
||||
$this->travel(5)->minutes();
|
||||
|
||||
$this->actingAs($user)->get(route('folders.media.download', [
|
||||
'folder' => $folder,
|
||||
'mediaId' => $media->id,
|
||||
]));
|
||||
|
||||
$count = MediaDownload::query()
|
||||
->where('media_id', $media->id)
|
||||
->where('user_id', $user->id)
|
||||
->count();
|
||||
|
||||
expect($count)->toBe(1);
|
||||
|
||||
$firstDownload->refresh();
|
||||
expect($firstDownload->downloaded_at->gt($firstTimestamp))->toBeTrue();
|
||||
});
|
||||
|
||||
test('download status is per-user in show endpoint', function () {
|
||||
[$user, $workspace, $folder, $media] = setupFolderWithMedia();
|
||||
$otherUser = User::factory()->create();
|
||||
$workspace->users()->attach($otherUser, ['role' => 'member']);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
MediaDownload::query()->create([
|
||||
'media_id' => $media->id,
|
||||
'user_id' => $user->id,
|
||||
'downloaded_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('folders.show', $folder));
|
||||
$response->assertOk();
|
||||
$documents = $response->original->getData()['page']['props']['documents'];
|
||||
$doc = collect($documents)->firstWhere('id', $media->id);
|
||||
expect($doc['is_downloaded'])->toBeTrue();
|
||||
|
||||
$response2 = $this->actingAs($otherUser)->get(route('folders.show', $folder));
|
||||
$response2->assertOk();
|
||||
$documents2 = $response2->original->getData()['page']['props']['documents'];
|
||||
$doc2 = collect($documents2)->firstWhere('id', $media->id);
|
||||
expect($doc2['is_downloaded'])->toBeFalse();
|
||||
});
|
||||
97
tests/Feature/Notification/FolderMentionTest.php
Normal file
97
tests/Feature/Notification/FolderMentionTest.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Folder;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Notifications\FolderMentionNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
function setupMentionScenario(string $role = 'owner'): array
|
||||
{
|
||||
$sender = User::factory()->create();
|
||||
$target = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($sender, ['role' => $role]);
|
||||
$workspace->users()->attach($target, ['role' => 'member']);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$folder = Folder::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
return [$sender, $target, $workspace, $folder];
|
||||
}
|
||||
|
||||
test('owner can mention a workspace user', function () {
|
||||
Notification::fake();
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('owner');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Please check this folder.',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
Notification::assertSentTo($target, FolderMentionNotification::class);
|
||||
});
|
||||
|
||||
test('manager can mention a workspace user', function () {
|
||||
Notification::fake();
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('manager');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Please check this folder.',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
Notification::assertSentTo($target, FolderMentionNotification::class);
|
||||
});
|
||||
|
||||
test('member cannot mention a workspace user', function () {
|
||||
Notification::fake();
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('member');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Please check this folder.',
|
||||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
test('cannot mention user from another workspace', function () {
|
||||
Notification::fake();
|
||||
[$sender, , $workspace, $folder] = setupMentionScenario('owner');
|
||||
$outsider = User::factory()->create();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
'user_id' => $outsider->id,
|
||||
'message' => 'Hello',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('user_id');
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
test('notification is persisted in database', function () {
|
||||
[$sender, $target, $workspace, $folder] = setupMentionScenario('owner');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$this->actingAs($sender)->post(route('folders.mentions.store', $folder), [
|
||||
'user_id' => $target->id,
|
||||
'message' => 'Check this.',
|
||||
]);
|
||||
|
||||
expect($target->notifications()->count())->toBe(1);
|
||||
$notif = $target->notifications()->first();
|
||||
expect($notif->data['folder_id'])->toBe($folder->id);
|
||||
expect($notif->data['message'])->toBe('Check this.');
|
||||
});
|
||||
65
tests/Feature/Notification/NotificationControllerTest.php
Normal file
65
tests/Feature/Notification/NotificationControllerTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Notifications\FolderMentionNotification;
|
||||
|
||||
test('user can mark own notification as read', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->notify(new FolderMentionNotification(
|
||||
folderId: 1,
|
||||
folderTitle: 'Test Folder',
|
||||
mentionedById: 999,
|
||||
mentionedByName: 'Admin',
|
||||
message: 'Please review.',
|
||||
url: '/folders/1',
|
||||
));
|
||||
|
||||
$notification = $user->notifications()->first();
|
||||
expect($notification->read_at)->toBeNull();
|
||||
|
||||
$response = $this->actingAs($user)->post(route('notifications.read', $notification->id));
|
||||
|
||||
$response->assertRedirect();
|
||||
$notification->refresh();
|
||||
expect($notification->read_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('cannot mark another user notification as read', function () {
|
||||
$user = User::factory()->create();
|
||||
$other = User::factory()->create();
|
||||
$other->notify(new FolderMentionNotification(
|
||||
folderId: 1,
|
||||
folderTitle: 'Test',
|
||||
mentionedById: 999,
|
||||
mentionedByName: 'Admin',
|
||||
message: 'Hey.',
|
||||
url: '/folders/1',
|
||||
));
|
||||
|
||||
$notification = $other->notifications()->first();
|
||||
|
||||
$response = $this->actingAs($user)->post(route('notifications.read', $notification->id));
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
test('user can mark all notifications as read', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$user->notify(new FolderMentionNotification(
|
||||
folderId: $i,
|
||||
folderTitle: "Folder $i",
|
||||
mentionedById: 999,
|
||||
mentionedByName: 'Admin',
|
||||
message: "Message $i",
|
||||
url: "/folders/$i",
|
||||
));
|
||||
}
|
||||
|
||||
expect($user->unreadNotifications()->count())->toBe(3);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('notifications.readAll'));
|
||||
$response->assertRedirect();
|
||||
|
||||
expect($user->unreadNotifications()->count())->toBe(0);
|
||||
});
|
||||
50
tests/Feature/Settings/PasswordUpdateTest.php
Normal file
50
tests/Feature/Settings/PasswordUpdateTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
test('password update page is displayed', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('user-password.edit'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('password can be updated', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from(route('user-password.edit'))
|
||||
->put(route('user-password.update'), [
|
||||
'current_password' => 'password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('user-password.edit'));
|
||||
|
||||
expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue();
|
||||
});
|
||||
|
||||
test('correct password must be provided to update password', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from(route('user-password.edit'))
|
||||
->put(route('user-password.update'), [
|
||||
'current_password' => 'wrong-password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrors('current_password')
|
||||
->assertRedirect(route('user-password.edit'));
|
||||
});
|
||||
85
tests/Feature/Settings/ProfileUpdateTest.php
Normal file
85
tests/Feature/Settings/ProfileUpdateTest.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
test('profile page is displayed', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('profile.edit'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('profile information can be updated', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch(route('profile.update'), [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('profile.edit'));
|
||||
|
||||
$user->refresh();
|
||||
|
||||
expect($user->name)->toBe('Test User');
|
||||
expect($user->email)->toBe('test@example.com');
|
||||
expect($user->email_verified_at)->toBeNull();
|
||||
});
|
||||
|
||||
test('email verification status is unchanged when the email address is unchanged', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch(route('profile.update'), [
|
||||
'name' => 'Test User',
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('profile.edit'));
|
||||
|
||||
expect($user->refresh()->email_verified_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('user can delete their account', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->delete(route('profile.destroy'), [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
$this->assertGuest();
|
||||
expect($user->fresh())->toBeNull();
|
||||
});
|
||||
|
||||
test('correct password must be provided to delete account', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from(route('profile.edit'))
|
||||
->delete(route('profile.destroy'), [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrors('password')
|
||||
->assertRedirect(route('profile.edit'));
|
||||
|
||||
expect($user->fresh())->not->toBeNull();
|
||||
});
|
||||
79
tests/Feature/Settings/TwoFactorAuthenticationTest.php
Normal file
79
tests/Feature/Settings/TwoFactorAuthenticationTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
test('two factor settings page can be rendered', function () {
|
||||
if (! Features::canManageTwoFactorAuthentication()) {
|
||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
||||
}
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['auth.password_confirmed_at' => time()])
|
||||
->get(route('two-factor.show'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('settings/TwoFactor')
|
||||
->where('twoFactorEnabled', false),
|
||||
);
|
||||
});
|
||||
|
||||
test('two factor settings page requires password confirmation when enabled', function () {
|
||||
if (! Features::canManageTwoFactorAuthentication()) {
|
||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
||||
}
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(route('two-factor.show'));
|
||||
|
||||
$response->assertRedirect(route('password.confirm'));
|
||||
});
|
||||
|
||||
test('two factor settings page does not requires password confirmation when disabled', function () {
|
||||
if (! Features::canManageTwoFactorAuthentication()) {
|
||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
||||
}
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => false,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('two-factor.show'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('settings/TwoFactor'),
|
||||
);
|
||||
});
|
||||
|
||||
test('two factor settings page returns forbidden response when two factor is disabled', function () {
|
||||
if (! Features::canManageTwoFactorAuthentication()) {
|
||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
||||
}
|
||||
|
||||
config(['fortify.features' => []]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['auth.password_confirmed_at' => time()])
|
||||
->get(route('two-factor.show'))
|
||||
->assertForbidden();
|
||||
});
|
||||
33
tests/Feature/UserGroupTest.php
Normal file
33
tests/Feature/UserGroupTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\UserGroup;
|
||||
use App\Models\User;
|
||||
|
||||
test('user factory creates user with default User group', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect($user->group)->toBeInstanceOf(UserGroup::class)
|
||||
->and($user->group->is(UserGroup::User))->toBeTrue()
|
||||
->and($user->group->value)->toBe('user');
|
||||
});
|
||||
|
||||
test('user factory superadmin state assigns Superadmin group', function () {
|
||||
$user = User::factory()->superadmin()->create();
|
||||
|
||||
expect($user->group->is(UserGroup::Superadmin))->toBeTrue()
|
||||
->and($user->group->value)->toBe('superadmin');
|
||||
});
|
||||
|
||||
test('user factory admin state assigns Admin group', function () {
|
||||
$user = User::factory()->admin()->create();
|
||||
|
||||
expect($user->group->is(UserGroup::Admin))->toBeTrue()
|
||||
->and($user->group->value)->toBe('admin');
|
||||
});
|
||||
|
||||
test('group is persisted and retrieved correctly from database', function () {
|
||||
$user = User::factory()->admin()->create();
|
||||
$user->refresh();
|
||||
|
||||
expect($user->group->is(UserGroup::Admin))->toBeTrue();
|
||||
});
|
||||
Reference in New Issue
Block a user