auth tests
This commit is contained in:
73
backend/tests/Feature/Auth/AccessTierTest.php
Normal file
73
backend/tests/Feature/Auth/AccessTierTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Verifica la scala di accesso (tier.app / tier.admin) attraverso rotte reali:
|
||||
* - tier.app → user-roles index (auth + verified + setup.complete)
|
||||
* - tier.admin→ users index (+ admin)
|
||||
* Copre i rami di EnsureSetupComplete, del middleware `verified` e di IsAdmin.
|
||||
*/
|
||||
class AccessTierTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const APP_ROUTE = '/api/user-roles';
|
||||
|
||||
private const ADMIN_ROUTE = '/api/users';
|
||||
|
||||
public function test_tier_app_allows_a_fully_operational_user(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
|
||||
$this->getJson(self::APP_ROUTE)->assertOk();
|
||||
}
|
||||
|
||||
public function test_tier_app_requires_authentication(): void
|
||||
{
|
||||
$this->getJson(self::APP_ROUTE)->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_tier_app_blocks_user_who_must_change_password(): void
|
||||
{
|
||||
$this->actingAs($this->setupIncompleteUser(), 'sanctum');
|
||||
|
||||
$this->getJson(self::APP_ROUTE)
|
||||
->assertForbidden()
|
||||
->assertJsonPath('setup_status', 'password_required');
|
||||
}
|
||||
|
||||
public function test_tier_app_blocks_user_without_two_factor_setup(): void
|
||||
{
|
||||
$user = $this->operationalUser(['two_factor_setup_completed_at' => null]);
|
||||
$this->actingAs($user, 'sanctum');
|
||||
|
||||
$this->getJson(self::APP_ROUTE)
|
||||
->assertForbidden()
|
||||
->assertJsonPath('setup_status', '2fa_setup_required');
|
||||
}
|
||||
|
||||
public function test_tier_app_blocks_unverified_user(): void
|
||||
{
|
||||
$this->actingAs($this->unverifiedUser(), 'sanctum');
|
||||
|
||||
$this->getJson(self::APP_ROUTE)->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_tier_admin_allows_admin(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->getJson(self::ADMIN_ROUTE)->assertOk();
|
||||
}
|
||||
|
||||
public function test_tier_admin_forbids_non_admin(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
|
||||
$this->getJson(self::ADMIN_ROUTE)->assertForbidden();
|
||||
}
|
||||
}
|
||||
86
backend/tests/Feature/Auth/AuthControllerTest.php
Normal file
86
backend/tests/Feature/Auth/AuthControllerTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_me_returns_authenticated_user_with_setup_status(): void
|
||||
{
|
||||
$user = $this->operationalUser();
|
||||
$this->actingAs($user, 'sanctum');
|
||||
|
||||
$response = $this->getJson('/api/user');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('id', $user->id)
|
||||
->assertJsonPath('email', $user->email)
|
||||
->assertJsonPath('setup_status', 'complete');
|
||||
|
||||
// Gli attributi sensibili non devono mai essere serializzati.
|
||||
$this->assertArrayNotHasKey('password', $response->json());
|
||||
$this->assertArrayNotHasKey('two_factor_secret', $response->json());
|
||||
$this->assertArrayNotHasKey('two_factor_recovery_codes', $response->json());
|
||||
}
|
||||
|
||||
public function test_me_is_reachable_during_setup(): void
|
||||
{
|
||||
// La rotta /user è nel tier "transitional": deve restare accessibile anche
|
||||
// a setup incompleto, così il frontend può leggere setup_status.
|
||||
$user = $this->setupIncompleteUser();
|
||||
$this->actingAs($user, 'sanctum');
|
||||
|
||||
$this->getJson('/api/user')
|
||||
->assertOk()
|
||||
->assertJsonPath('setup_status', 'password_required');
|
||||
}
|
||||
|
||||
public function test_me_requires_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/user')->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_logout_succeeds_and_forgets_cookies(): void
|
||||
{
|
||||
$user = $this->operationalUser();
|
||||
$this->actingAs($user, 'sanctum');
|
||||
|
||||
$response = $this->postJson('/api/logout');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('message', config('messages.auth.logout_success'))
|
||||
->assertCookieExpired('XSRF-TOKEN');
|
||||
}
|
||||
|
||||
public function test_destroy_account_soft_deletes_the_user(): void
|
||||
{
|
||||
$user = $this->operationalUser();
|
||||
$this->actingAs($user, 'sanctum');
|
||||
|
||||
$this->deleteJson('/api/account')
|
||||
->assertOk()
|
||||
->assertJsonPath('message', config('messages.account.deleted'));
|
||||
|
||||
$this->assertSoftDeleted('users', ['id' => $user->id]);
|
||||
}
|
||||
|
||||
public function test_destroy_account_is_forbidden_for_the_system_user(): void
|
||||
{
|
||||
$user = $this->systemUser();
|
||||
$this->actingAs($user, 'sanctum');
|
||||
|
||||
$this->deleteJson('/api/account')->assertForbidden();
|
||||
|
||||
$this->assertDatabaseHas('users', ['id' => $user->id, 'deleted_at' => null]);
|
||||
}
|
||||
|
||||
public function test_session_endpoints_require_authentication(): void
|
||||
{
|
||||
$this->postJson('/api/logout')->assertUnauthorized();
|
||||
$this->deleteJson('/api/account')->assertUnauthorized();
|
||||
}
|
||||
}
|
||||
24
backend/tests/Feature/EnvironmentSafetyTest.php
Normal file
24
backend/tests/Feature/EnvironmentSafetyTest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Guardia di sicurezza: garantisce che la suite giri sul DB di test dedicato
|
||||
* (servizio db-test), non sul database di sviluppo `dyncoll`. È volutamente
|
||||
* read-only (non usa RefreshDatabase): qui assertiamo che l'host e il nome DB
|
||||
* siano quelli di test. La protezione "dura" — che aborta PRIMA di un eventuale
|
||||
* migrate:fresh — sta nel guard beforeRefreshingDatabase() di Tests\TestCase.
|
||||
*/
|
||||
class EnvironmentSafetyTest extends TestCase
|
||||
{
|
||||
public function test_suite_runs_against_the_dedicated_test_database(): void
|
||||
{
|
||||
$this->assertSame('testing', app()->environment());
|
||||
$this->assertSame('mysql', config('database.default'));
|
||||
$this->assertSame('db-test', config('database.connections.mysql.host'));
|
||||
$this->assertSame('dyncoll_test', DB::connection()->getDatabaseName());
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
77
backend/tests/Feature/PurgeAnonymizableUsersCommandTest.php
Normal file
77
backend/tests/Feature/PurgeAnonymizableUsersCommandTest.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PurgeAnonymizableUsersCommandTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** Crea un utente cestinato con deleted_at retrodatato di $days giorni. */
|
||||
private function trashedDaysAgo(int $days): User
|
||||
{
|
||||
$user = $this->operationalUser();
|
||||
$user->delete();
|
||||
User::withTrashed()->whereKey($user->id)->update(['deleted_at' => now()->subDays($days)]);
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
public function test_it_anonymizes_users_past_the_retention_window(): void
|
||||
{
|
||||
$this->systemUser();
|
||||
$old = $this->trashedDaysAgo(40);
|
||||
|
||||
$this->artisan('users:purge', ['--days' => 30])->assertSuccessful();
|
||||
|
||||
$old->refresh();
|
||||
$this->assertNotNull($old->anonymized_at);
|
||||
$this->assertSame('Deleted user', $old->name);
|
||||
}
|
||||
|
||||
public function test_it_skips_users_still_within_retention(): void
|
||||
{
|
||||
$this->systemUser();
|
||||
$recent = $this->trashedDaysAgo(5);
|
||||
|
||||
$this->artisan('users:purge', ['--days' => 30])->assertSuccessful();
|
||||
|
||||
$recent->refresh();
|
||||
$this->assertNull($recent->anonymized_at);
|
||||
}
|
||||
|
||||
public function test_dry_run_does_not_modify_anyone(): void
|
||||
{
|
||||
$this->systemUser();
|
||||
$old = $this->trashedDaysAgo(40);
|
||||
|
||||
$this->artisan('users:purge', ['--days' => 30, '--dry-run' => true])->assertSuccessful();
|
||||
|
||||
$old->refresh();
|
||||
$this->assertNull($old->anonymized_at);
|
||||
}
|
||||
|
||||
public function test_it_never_touches_the_system_user(): void
|
||||
{
|
||||
$system = $this->systemUser();
|
||||
// Anche se (per assurdo) cestinato e vecchio, l'utente di sistema è escluso.
|
||||
$system->delete();
|
||||
User::withTrashed()->whereKey($system->id)->update(['deleted_at' => now()->subDays(99)]);
|
||||
|
||||
$this->artisan('users:purge', ['--days' => 30])->assertSuccessful();
|
||||
|
||||
$this->assertNull($system->fresh()->anonymized_at);
|
||||
}
|
||||
|
||||
public function test_it_reports_when_there_is_nothing_to_do(): void
|
||||
{
|
||||
$this->systemUser();
|
||||
|
||||
$this->artisan('users:purge', ['--days' => 30])
|
||||
->expectsOutputToContain('No users to anonymize.')
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
55
backend/tests/Feature/PurgeUserActionTest.php
Normal file
55
backend/tests/Feature/PurgeUserActionTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Actions\PurgeUserAction;
|
||||
use App\Exceptions\CannotDeleteSystemUserException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PurgeUserActionTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function purge(): PurgeUserAction
|
||||
{
|
||||
return app(PurgeUserAction::class);
|
||||
}
|
||||
|
||||
public function test_it_anonymizes_personal_data_in_place(): void
|
||||
{
|
||||
$this->systemUser();
|
||||
$user = $this->operationalUser(['name' => 'Mario Rossi']);
|
||||
|
||||
$this->purge()->execute($user);
|
||||
|
||||
$user->refresh();
|
||||
$this->assertSame('Deleted user', $user->name);
|
||||
$this->assertSame("deleted-{$user->id}@anonymized.invalid", $user->email);
|
||||
$this->assertNotNull($user->anonymized_at);
|
||||
$this->assertNull($user->email_verified_at);
|
||||
$this->assertNull($user->two_factor_setup_completed_at);
|
||||
$this->assertFalse($user->must_change_password);
|
||||
}
|
||||
|
||||
public function test_it_is_idempotent_on_already_anonymized_users(): void
|
||||
{
|
||||
$this->systemUser();
|
||||
$user = $this->operationalUser(['name' => 'Keep Me', 'anonymized_at' => now()]);
|
||||
|
||||
$this->purge()->execute($user);
|
||||
|
||||
$user->refresh();
|
||||
// Già anonimizzato → uscita anticipata: nessuna riscrittura del nome.
|
||||
$this->assertSame('Keep Me', $user->name);
|
||||
}
|
||||
|
||||
public function test_it_refuses_to_anonymize_the_system_user(): void
|
||||
{
|
||||
$system = $this->systemUser();
|
||||
|
||||
$this->expectException(CannotDeleteSystemUserException::class);
|
||||
|
||||
$this->purge()->execute($system);
|
||||
}
|
||||
}
|
||||
73
backend/tests/Feature/SeederTest.php
Normal file
73
backend/tests/Feature/SeederTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Lists\UserRole;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\DatabaseSeeder;
|
||||
use Database\Seeders\SystemUserSeeder;
|
||||
use Database\Seeders\UserRoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SeederTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_user_role_seeder_creates_the_base_roles(): void
|
||||
{
|
||||
$this->seed(UserRoleSeeder::class);
|
||||
|
||||
$this->assertSame(4, UserRole::count());
|
||||
foreach ([UserRole::ADMIN, UserRole::SUPERVISOR, UserRole::USER, UserRole::GUEST] as $name) {
|
||||
$this->assertDatabaseHas('user_roles', ['name' => $name]);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_role_descriptions_are_in_english(): void
|
||||
{
|
||||
$this->seed(UserRoleSeeder::class);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Full access',
|
||||
(string) UserRole::where('name', UserRole::ADMIN)->value('description')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_user_role_seeder_is_idempotent(): void
|
||||
{
|
||||
$this->seed(UserRoleSeeder::class);
|
||||
$this->seed(UserRoleSeeder::class);
|
||||
|
||||
$this->assertSame(4, UserRole::count());
|
||||
}
|
||||
|
||||
public function test_system_user_seeder_creates_a_locked_admin_account(): void
|
||||
{
|
||||
$this->seed(UserRoleSeeder::class);
|
||||
$this->seed(SystemUserSeeder::class);
|
||||
|
||||
$system = User::where('email', SystemUserSeeder::EMAIL)->firstOrFail();
|
||||
$this->assertTrue($system->is_system);
|
||||
$this->assertFalse($system->must_change_password);
|
||||
$this->assertSame(UserRole::ADMIN, $system->role->name);
|
||||
$this->assertSame('complete', $system->setup_status);
|
||||
}
|
||||
|
||||
public function test_system_user_seeder_is_idempotent(): void
|
||||
{
|
||||
$this->seed(UserRoleSeeder::class);
|
||||
$this->seed(SystemUserSeeder::class);
|
||||
$this->seed(SystemUserSeeder::class);
|
||||
|
||||
$this->assertSame(1, User::where('email', SystemUserSeeder::EMAIL)->count());
|
||||
}
|
||||
|
||||
public function test_database_seeder_runs_the_full_chain(): void
|
||||
{
|
||||
$this->seed(DatabaseSeeder::class);
|
||||
|
||||
$this->assertSame(4, UserRole::count());
|
||||
$this->assertDatabaseHas('users', ['email' => SystemUserSeeder::EMAIL, 'is_system' => true]);
|
||||
}
|
||||
}
|
||||
170
backend/tests/Feature/UserControllerTest.php
Normal file
170
backend/tests/Feature/UserControllerTest.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Lists\UserRole;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @return list<int> id presenti in data della risposta. */
|
||||
private function idsFrom(TestResponse $response): array
|
||||
{
|
||||
return collect($response->json('data'))->pluck('id')->all();
|
||||
}
|
||||
|
||||
// --- Gating ---------------------------------------------------------------
|
||||
|
||||
public function test_index_requires_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/users')->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_index_is_forbidden_for_non_admin(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
|
||||
$this->getJson('/api/users')->assertForbidden();
|
||||
}
|
||||
|
||||
// --- Index ----------------------------------------------------------------
|
||||
|
||||
public function test_index_returns_paginated_envelope(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->getJson('/api/users')
|
||||
->assertOk()
|
||||
->assertJsonStructure([
|
||||
'message',
|
||||
'data',
|
||||
'meta' => ['current_page', 'last_page', 'per_page', 'total'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_index_filters_by_search_term(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$match = $this->operationalUser(['name' => 'Zzz Unique Marker']);
|
||||
$other = $this->operationalUser(['name' => 'Someone Else']);
|
||||
|
||||
$ids = $this->idsFrom($this->getJson('/api/users?search=Unique+Marker')->assertOk());
|
||||
|
||||
$this->assertContains($match->id, $ids);
|
||||
$this->assertNotContains($other->id, $ids);
|
||||
}
|
||||
|
||||
public function test_index_filters_by_role(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$role = UserRole::factory()->create();
|
||||
$inRole = $this->operationalUser(['role_id' => $role->id]);
|
||||
$outRole = $this->operationalUser();
|
||||
|
||||
$ids = $this->idsFrom($this->getJson("/api/users?role_id={$role->id}")->assertOk());
|
||||
|
||||
$this->assertContains($inRole->id, $ids);
|
||||
$this->assertNotContains($outRole->id, $ids);
|
||||
}
|
||||
|
||||
public function test_index_excludes_trashed_by_default(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$trashed = $this->operationalUser();
|
||||
$trashed->delete();
|
||||
|
||||
$ids = $this->idsFrom($this->getJson('/api/users')->assertOk());
|
||||
|
||||
$this->assertNotContains($trashed->id, $ids);
|
||||
}
|
||||
|
||||
public function test_index_can_include_trashed(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$active = $this->operationalUser();
|
||||
$trashed = $this->operationalUser();
|
||||
$trashed->delete();
|
||||
|
||||
$ids = $this->idsFrom($this->getJson('/api/users?trashed=with')->assertOk());
|
||||
|
||||
$this->assertContains($active->id, $ids);
|
||||
$this->assertContains($trashed->id, $ids);
|
||||
}
|
||||
|
||||
public function test_index_can_return_only_trashed(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$active = $this->operationalUser();
|
||||
$trashed = $this->operationalUser();
|
||||
$trashed->delete();
|
||||
|
||||
$ids = $this->idsFrom($this->getJson('/api/users?trashed=only')->assertOk());
|
||||
|
||||
$this->assertContains($trashed->id, $ids);
|
||||
$this->assertNotContains($active->id, $ids);
|
||||
}
|
||||
|
||||
public function test_index_respects_per_page(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->getJson('/api/users?per_page=5')
|
||||
->assertOk()
|
||||
->assertJsonPath('meta.per_page', 5);
|
||||
}
|
||||
|
||||
public function test_index_rejects_an_excessive_per_page(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->getJson('/api/users?per_page=500')
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('per_page');
|
||||
}
|
||||
|
||||
public function test_index_rejects_an_unknown_trashed_value(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->getJson('/api/users?trashed=garbage')
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('trashed');
|
||||
}
|
||||
|
||||
// --- Show -----------------------------------------------------------------
|
||||
|
||||
public function test_show_returns_a_user_with_role(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$user = $this->operationalUser();
|
||||
|
||||
$this->getJson("/api/users/{$user->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.id', $user->id)
|
||||
->assertJsonPath('data.setup_status', 'complete');
|
||||
}
|
||||
|
||||
public function test_show_can_load_a_trashed_user(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$user = $this->operationalUser();
|
||||
$user->delete();
|
||||
|
||||
$this->getJson("/api/users/{$user->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.id', $user->id);
|
||||
}
|
||||
|
||||
public function test_show_is_forbidden_for_non_admin(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$target = User::factory()->create();
|
||||
|
||||
$this->getJson("/api/users/{$target->id}")->assertForbidden();
|
||||
}
|
||||
}
|
||||
163
backend/tests/Feature/UserRoleControllerTest.php
Normal file
163
backend/tests/Feature/UserRoleControllerTest.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Lists\UserRole;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserRoleControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
// --- Lettura (tier.app) --------------------------------------------------
|
||||
|
||||
public function test_index_lists_roles_for_an_operational_user(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
UserRole::factory()->count(2)->create();
|
||||
|
||||
$this->getJson('/api/user-roles')
|
||||
->assertOk()
|
||||
->assertJsonStructure(['message', 'data' => [['id', 'name', 'description']]]);
|
||||
}
|
||||
|
||||
public function test_show_returns_a_single_role(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$role = UserRole::factory()->create();
|
||||
|
||||
$this->getJson("/api/user-roles/{$role->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.id', $role->id)
|
||||
->assertJsonPath('data.name', $role->name);
|
||||
}
|
||||
|
||||
public function test_usage_flags_system_role_as_locked(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$role = UserRole::create(['name' => UserRole::SUPERVISOR]);
|
||||
|
||||
$this->getJson("/api/user-roles/{$role->id}/usage")
|
||||
->assertOk()
|
||||
->assertJsonPath('in_use', true);
|
||||
}
|
||||
|
||||
public function test_usage_flags_free_custom_role_as_unlocked(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$role = UserRole::factory()->create();
|
||||
|
||||
$this->getJson("/api/user-roles/{$role->id}/usage")
|
||||
->assertOk()
|
||||
->assertJsonPath('in_use', false);
|
||||
}
|
||||
|
||||
// --- Scrittura (tier.admin) ---------------------------------------------
|
||||
|
||||
public function test_admin_can_create_a_role(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->postJson('/api/user-roles', ['name' => 'Editor', 'description' => 'Edits stuff'])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.name', 'Editor');
|
||||
|
||||
$this->assertDatabaseHas('user_roles', ['name' => 'Editor']);
|
||||
}
|
||||
|
||||
public function test_create_requires_a_name(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->postJson('/api/user-roles', ['description' => 'no name'])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('name');
|
||||
}
|
||||
|
||||
public function test_create_rejects_a_duplicate_name(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
UserRole::factory()->create(['name' => 'Editor']);
|
||||
|
||||
$this->postJson('/api/user-roles', ['name' => 'Editor'])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('name');
|
||||
}
|
||||
|
||||
public function test_admin_can_update_a_role(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$role = UserRole::factory()->create(['name' => 'Old']);
|
||||
|
||||
$this->putJson("/api/user-roles/{$role->id}", ['name' => 'New'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.name', 'New');
|
||||
|
||||
$this->assertDatabaseHas('user_roles', ['id' => $role->id, 'name' => 'New']);
|
||||
}
|
||||
|
||||
public function test_update_can_keep_the_same_name(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$role = UserRole::factory()->create(['name' => 'Stable']);
|
||||
|
||||
$this->putJson("/api/user-roles/{$role->id}", ['name' => 'Stable'])
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
public function test_update_rejects_a_name_taken_by_another_role(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
UserRole::factory()->create(['name' => 'Taken']);
|
||||
$role = UserRole::factory()->create(['name' => 'Mine']);
|
||||
|
||||
$this->putJson("/api/user-roles/{$role->id}", ['name' => 'Taken'])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('name');
|
||||
}
|
||||
|
||||
public function test_cannot_delete_a_system_role(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$role = UserRole::create(['name' => UserRole::GUEST]);
|
||||
|
||||
$this->deleteJson("/api/user-roles/{$role->id}")
|
||||
->assertStatus(409)
|
||||
->assertJsonPath('in_use', true);
|
||||
|
||||
$this->assertDatabaseHas('user_roles', ['id' => $role->id]);
|
||||
}
|
||||
|
||||
public function test_cannot_delete_a_role_in_use(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$role = UserRole::factory()->create(['name' => 'Busy']);
|
||||
$this->operationalUser(['role_id' => $role->id]);
|
||||
|
||||
$this->deleteJson("/api/user-roles/{$role->id}")
|
||||
->assertStatus(409)
|
||||
->assertJsonPath('in_use', true);
|
||||
|
||||
$this->assertDatabaseHas('user_roles', ['id' => $role->id]);
|
||||
}
|
||||
|
||||
public function test_admin_can_delete_a_free_custom_role(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$role = UserRole::factory()->create(['name' => 'Disposable']);
|
||||
|
||||
$this->deleteJson("/api/user-roles/{$role->id}")->assertOk();
|
||||
|
||||
$this->assertDatabaseMissing('user_roles', ['id' => $role->id]);
|
||||
}
|
||||
|
||||
// --- Gating ---------------------------------------------------------------
|
||||
|
||||
public function test_non_admin_cannot_write(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
|
||||
$this->postJson('/api/user-roles', ['name' => 'Nope'])->assertForbidden();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user