From 032f6a08df81caf13bcb076c980fc13e2e11cd6a Mon Sep 17 00:00:00 2001 From: Giuseppe Naponiello Date: Tue, 23 Jun 2026 12:29:02 +0200 Subject: [PATCH] users and institutions route, tests fixed --- .../InstitutionCategoryController.php | 6 +- .../Controllers/UserPositionController.php | 28 ++++ .../Http/Controllers/UserRoleController.php | 44 +------ .../Http/Requests/IndexInstitutionRequest.php | 10 +- .../app/Http/Requests/IndexUserRequest.php | 10 +- .../Http/Requests/StoreUserRoleRequest.php | 28 ---- .../Http/Requests/UpdateUserRoleRequest.php | 36 ----- backend/app/Models/Lists/UserPosition.php | 28 ++++ backend/app/Models/Lists/UserRole.php | 16 --- backend/bootstrap/app.php | 5 + .../factories/Lists/UserPositionFactory.php | 26 ++++ ...026_06_22_210246_create_user_positions.php | 28 ++++ backend/database/seeders/DatabaseSeeder.php | 1 + .../database/seeders/UserPositionSeeder.php | 29 +++++ backend/routes/api/institution-categories.php | 10 +- backend/routes/api/institutions.php | 9 +- backend/routes/api/user-positions.php | 12 ++ backend/routes/api/user-roles.php | 7 +- .../Feature/InstitutionControllerTest.php | 84 +++++++----- .../Feature/InstitutionLinkControllerTest.php | 12 +- .../Feature/UserPositionControllerTest.php | 44 +++++++ .../tests/Feature/UserPositionSeederTest.php | 37 ++++++ .../tests/Feature/UserRoleControllerTest.php | 123 ++---------------- 23 files changed, 346 insertions(+), 287 deletions(-) create mode 100644 backend/app/Http/Controllers/UserPositionController.php delete mode 100644 backend/app/Http/Requests/StoreUserRoleRequest.php delete mode 100644 backend/app/Http/Requests/UpdateUserRoleRequest.php create mode 100644 backend/app/Models/Lists/UserPosition.php create mode 100644 backend/database/factories/Lists/UserPositionFactory.php create mode 100644 backend/database/migrations/2026_06_22_210246_create_user_positions.php create mode 100644 backend/database/seeders/UserPositionSeeder.php create mode 100644 backend/routes/api/user-positions.php create mode 100644 backend/tests/Feature/UserPositionControllerTest.php create mode 100644 backend/tests/Feature/UserPositionSeederTest.php diff --git a/backend/app/Http/Controllers/InstitutionCategoryController.php b/backend/app/Http/Controllers/InstitutionCategoryController.php index 3423b2d..b9f5f1b 100644 --- a/backend/app/Http/Controllers/InstitutionCategoryController.php +++ b/backend/app/Http/Controllers/InstitutionCategoryController.php @@ -47,8 +47,10 @@ class InstitutionCategoryController extends Controller /** * Aggiorna una categoria. */ - public function update(UpdateInstitutionCategoryRequest $request, InstitutionCategory $institutionCategory): JsonResponse - { + public function update( + UpdateInstitutionCategoryRequest $request, + InstitutionCategory $institutionCategory + ): JsonResponse { $institutionCategory->update($request->validated()); return $this->updatedResponse($institutionCategory); diff --git a/backend/app/Http/Controllers/UserPositionController.php b/backend/app/Http/Controllers/UserPositionController.php new file mode 100644 index 0000000..605535f --- /dev/null +++ b/backend/app/Http/Controllers/UserPositionController.php @@ -0,0 +1,28 @@ +collectionResponse(UserPosition::all()); + } + + /** + * Dettaglio di una position. + */ + public function show(UserPosition $userPosition): JsonResponse + { + return $this->okResponse($userPosition); + } +} diff --git a/backend/app/Http/Controllers/UserRoleController.php b/backend/app/Http/Controllers/UserRoleController.php index 8b27d84..3ef689b 100644 --- a/backend/app/Http/Controllers/UserRoleController.php +++ b/backend/app/Http/Controllers/UserRoleController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use App\Http\Requests\StoreUserRoleRequest; -use App\Http\Requests\UpdateUserRoleRequest; use App\Http\Traits\ApiResponse; use App\Models\Lists\UserRole; use Illuminate\Http\JsonResponse; @@ -20,16 +18,6 @@ class UserRoleController extends Controller return $this->collectionResponse(UserRole::all()); } - /** - * Crea un nuovo ruolo. - */ - public function store(StoreUserRoleRequest $request): JsonResponse - { - $role = UserRole::create($request->validated()); - - return $this->createdResponse($role); - } - /** * Dettaglio di un ruolo. */ @@ -39,40 +27,12 @@ class UserRoleController extends Controller } /** - * Aggiorna un ruolo. - */ - public function update(UpdateUserRoleRequest $request, UserRole $userRole): JsonResponse - { - $userRole->update($request->validated()); - - return $this->updatedResponse($userRole); - } - - /** - * Elimina un ruolo (se non di sistema e non in uso). - */ - public function destroy(UserRole $userRole): JsonResponse - { - if ($userRole->isSystemRole()) { - return $this->conflictResponse('Cannot delete a system role.'); - } - - if ($userRole->isInUse()) { - return $this->conflictResponse('Cannot delete: role assigned to at least one user.'); - } - - $userRole->delete(); - - return $this->deletedResponse(); - } - - /** - * Indica se il ruolo è bloccato per l'eliminazione (di sistema o in uso). + * Numero di utenti con questo ruolo (statistica per Admin). */ public function usage(UserRole $userRole): JsonResponse { return response()->json([ - 'in_use' => $userRole->isSystemRole() || $userRole->isInUse(), + 'user_count' => $userRole->users()->count(), ]); } } diff --git a/backend/app/Http/Requests/IndexInstitutionRequest.php b/backend/app/Http/Requests/IndexInstitutionRequest.php index 17485b2..5b61832 100644 --- a/backend/app/Http/Requests/IndexInstitutionRequest.php +++ b/backend/app/Http/Requests/IndexInstitutionRequest.php @@ -2,22 +2,22 @@ namespace App\Http\Requests; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -/** - * Filtri di query per l'elenco istituzioni. Autorizzazione demandata ai - * middleware di rotta (tier.app). - */ class IndexInstitutionRequest extends FormRequest { + /** + * L'autorizzazione è gestita dal middleware di rotta (tier.app). + */ public function authorize(): bool { return true; } /** - * @return array + * @return array|string> */ public function rules(): array { diff --git a/backend/app/Http/Requests/IndexUserRequest.php b/backend/app/Http/Requests/IndexUserRequest.php index 9193980..755d9bf 100644 --- a/backend/app/Http/Requests/IndexUserRequest.php +++ b/backend/app/Http/Requests/IndexUserRequest.php @@ -2,22 +2,22 @@ namespace App\Http\Requests; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -/** - * Validazione dei filtri di query per l'elenco utenti (area admin). - * L'autorizzazione è demandata ai middleware di rotta (auth + admin + setup). - */ class IndexUserRequest extends FormRequest { + /** + * L'autorizzazione (auth + admin + setup) è gestita dal middleware di rotta. + */ public function authorize(): bool { return true; } /** - * @return array + * @return array|string> */ public function rules(): array { diff --git a/backend/app/Http/Requests/StoreUserRoleRequest.php b/backend/app/Http/Requests/StoreUserRoleRequest.php deleted file mode 100644 index fb82574..0000000 --- a/backend/app/Http/Requests/StoreUserRoleRequest.php +++ /dev/null @@ -1,28 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'name' => 'required|string|max:255|unique:user_roles,name', - 'description' => 'nullable|string', - ]; - } -} diff --git a/backend/app/Http/Requests/UpdateUserRoleRequest.php b/backend/app/Http/Requests/UpdateUserRoleRequest.php deleted file mode 100644 index 9fb4e69..0000000 --- a/backend/app/Http/Requests/UpdateUserRoleRequest.php +++ /dev/null @@ -1,36 +0,0 @@ -|string> - */ - public function rules(): array - { - $id = $this->route('user_role')->id; - - return [ - 'name' => [ - 'required', - 'string', - 'max:255', - Rule::unique('user_roles', 'name')->ignore($id), - ], - 'description' => ['nullable', 'string'], - ]; - } -} diff --git a/backend/app/Models/Lists/UserPosition.php b/backend/app/Models/Lists/UserPosition.php new file mode 100644 index 0000000..8888b24 --- /dev/null +++ b/backend/app/Models/Lists/UserPosition.php @@ -0,0 +1,28 @@ +hasMany(User::class, 'role_id'); } - - /** - * Ruolo predefinito di sistema: non eliminabile. - */ - public function isSystemRole(): bool - { - return in_array($this->name, [self::ADMIN, self::SUPERVISOR, self::USER, self::GUEST], true); - } - - /** - * Il ruolo è assegnato ad almeno un utente. - */ - public function isInUse(): bool - { - return $this->users()->exists(); - } } diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index cd67ffb..a21c8bd 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -29,6 +29,11 @@ return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { // per Sanctum $middleware->statefulApi(); + // App API-only: nessuna rotta 'login' esiste. Senza questo, Laravel usa + // il suo default redirectGuestsTo(route('login')) e un utente non + // autenticato che richiede una pagina senza Accept: application/json + // (es. browser diretto) genera RouteNotFoundException (500) invece di 401. + $middleware->redirectGuestsTo(null); // Configurazione CORS $middleware->preventRequestForgery(except: ['api/*']); $middleware->alias([ diff --git a/backend/database/factories/Lists/UserPositionFactory.php b/backend/database/factories/Lists/UserPositionFactory.php new file mode 100644 index 0000000..9efee49 --- /dev/null +++ b/backend/database/factories/Lists/UserPositionFactory.php @@ -0,0 +1,26 @@ + + */ +class UserPositionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + protected $model = UserPosition::class; + + public function definition(): array + { + return [ + 'value' => fake()->unique()->word(), + ]; + } +} diff --git a/backend/database/migrations/2026_06_22_210246_create_user_positions.php b/backend/database/migrations/2026_06_22_210246_create_user_positions.php new file mode 100644 index 0000000..7790bd2 --- /dev/null +++ b/backend/database/migrations/2026_06_22_210246_create_user_positions.php @@ -0,0 +1,28 @@ +id(); + $table->string('value', 25)->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_positions'); + } +}; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index 0a3555f..7f988f7 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -19,6 +19,7 @@ class DatabaseSeeder extends Seeder UserRoleSeeder::class, SystemUserSeeder::class, InstitutionCategorySeeder::class, + UserPositionSeeder::class, ]); } } diff --git a/backend/database/seeders/UserPositionSeeder.php b/backend/database/seeders/UserPositionSeeder.php new file mode 100644 index 0000000..ffc8257 --- /dev/null +++ b/backend/database/seeders/UserPositionSeeder.php @@ -0,0 +1,29 @@ + $position] + ); + } + } +} diff --git a/backend/routes/api/institution-categories.php b/backend/routes/api/institution-categories.php index 875f10e..54dc8aa 100644 --- a/backend/routes/api/institution-categories.php +++ b/backend/routes/api/institution-categories.php @@ -3,15 +3,21 @@ use App\Http\Controllers\InstitutionCategoryController; use Illuminate\Support\Facades\Route; +// rotte pubbliche con throttling +Route::middleware('throttle:public')->group(function () { + Route::apiResource('institution-categories', InstitutionCategoryController::class) + ->only(['index', 'show']); +}); + // Prefisso `api` e tier applicati dal loader in bootstrap/app.php. // Lettura: utente pienamente operativo. Route::middleware('tier.app')->group(function () { - Route::apiResource('institution-categories', InstitutionCategoryController::class)->only(['index', 'show']); Route::get('institution-categories/{institution_category}/usage', [InstitutionCategoryController::class, 'usage']); }); // Scrittura: solo Admin. Route::middleware('tier.admin')->group(function () { - Route::apiResource('institution-categories', InstitutionCategoryController::class)->only(['store', 'update', 'destroy']); + Route::apiResource('institution-categories', InstitutionCategoryController::class) + ->only(['store', 'update', 'destroy']); }); diff --git a/backend/routes/api/institutions.php b/backend/routes/api/institutions.php index 79830c3..dc43254 100644 --- a/backend/routes/api/institutions.php +++ b/backend/routes/api/institutions.php @@ -7,14 +7,19 @@ use Illuminate\Support\Facades\Route; // Prefisso `api` e tier (`tier.app` / `tier.admin`) applicati dal loader in // bootstrap/app.php. Le istituzioni si risolvono per `uuid` (route key del model). -// Lettura: utente pienamente operativo. -Route::middleware('tier.app')->group(function () { +// rotte pubbliche con throttling +Route::middleware('throttle:public')->group(function () { Route::apiResource('institutions', InstitutionController::class)->only(['index', 'show']); Route::apiResource('institutions.links', InstitutionLinkController::class) ->only(['index', 'show']) ->scoped(); }); +// Lettura: utente pienamente operativo. +Route::middleware('tier.app')->group(function () { + // +}); + // Scrittura + soft delete: solo Admin. Route::middleware('tier.admin')->group(function () { Route::apiResource('institutions', InstitutionController::class)->only(['store', 'update', 'destroy']); diff --git a/backend/routes/api/user-positions.php b/backend/routes/api/user-positions.php new file mode 100644 index 0000000..7b86c84 --- /dev/null +++ b/backend/routes/api/user-positions.php @@ -0,0 +1,12 @@ +group(function () { + Route::apiResource('user-positions', UserPositionController::class)->only(['index', 'show']); +}); diff --git a/backend/routes/api/user-roles.php b/backend/routes/api/user-roles.php index 3cec2cb..89d7bcb 100644 --- a/backend/routes/api/user-roles.php +++ b/backend/routes/api/user-roles.php @@ -6,13 +6,12 @@ use Illuminate\Support\Facades\Route; // Prefisso `api` e stack `api` sono applicati dal loader in bootstrap/app.php. // I tier `tier.app` / `tier.admin` sono definiti lì come middleware group. -// Lettura: utente pienamente operativo (verificato + setup completato). +// Lookup fissa, non gestibile da interfaccia: solo lettura. Route::middleware('tier.app')->group(function () { Route::apiResource('user-roles', UserRoleController::class)->only(['index', 'show']); - Route::get('user-roles/{user_role}/usage', [UserRoleController::class, 'usage']); }); -// Scrittura: solo Admin. +// Statistiche: solo Admin. Route::middleware('tier.admin')->group(function () { - Route::apiResource('user-roles', UserRoleController::class)->only(['store', 'update', 'destroy']); + Route::get('user-roles/{user_role}/usage', [UserRoleController::class, 'usage']); }); diff --git a/backend/tests/Feature/InstitutionControllerTest.php b/backend/tests/Feature/InstitutionControllerTest.php index ab56d38..a69a1c6 100644 --- a/backend/tests/Feature/InstitutionControllerTest.php +++ b/backend/tests/Feature/InstitutionControllerTest.php @@ -14,6 +14,16 @@ class InstitutionControllerTest extends TestCase { use RefreshDatabase; + private const BASE_ROUTE = '/api/institutions'; + + private const TEST_MUSEUM = 'Test Museum'; + + private const JSON_HEADERS = ['Accept' => 'application/json']; + + private const OLD_LOGO_PATH = 'institution_logo/old.jpg'; + + private const GONE_LOGO_PATH = 'institution_logo/gone.jpg'; + /** * @param array $overrides * @return array @@ -22,7 +32,7 @@ class InstitutionControllerTest extends TestCase { return array_merge([ 'category_id' => InstitutionCategory::factory()->create()->id, - 'name' => 'Test Museum', + 'name' => self::TEST_MUSEUM, 'abbreviation' => 'TM', 'address' => 'Some Road 1', 'city' => 'Lund', @@ -41,7 +51,7 @@ class InstitutionControllerTest extends TestCase $this->actingAs($this->operationalUser(), 'sanctum'); Institution::factory()->count(2)->create(); - $this->getJson('/api/institutions') + $this->getJson(self::BASE_ROUTE) ->assertOk() ->assertJsonStructure(['message', 'data', 'meta' => ['current_page', 'total']]) ->assertJsonCount(2, 'data'); @@ -54,12 +64,12 @@ class InstitutionControllerTest extends TestCase Institution::factory()->create(['category_id' => $category->id, 'name' => 'Blekinge Museum']); Institution::factory()->create(['name' => 'Other Place']); - $this->getJson("/api/institutions?category_id={$category->id}") + $this->getJson(self::BASE_ROUTE."?category_id={$category->id}") ->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.name', 'Blekinge Museum'); - $this->getJson('/api/institutions?search=Blekinge') + $this->getJson(self::BASE_ROUTE.'?search=Blekinge') ->assertOk() ->assertJsonCount(1, 'data'); } @@ -71,12 +81,12 @@ class InstitutionControllerTest extends TestCase Institution::factory()->create(); $trashed->delete(); - $this->getJson('/api/institutions')->assertJsonCount(1, 'data'); - $this->getJson('/api/institutions?trashed=only') + $this->getJson(self::BASE_ROUTE)->assertJsonCount(1, 'data'); + $this->getJson(self::BASE_ROUTE.'?trashed=only') ->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $trashed->id); - $this->getJson('/api/institutions?trashed=with')->assertJsonCount(2, 'data'); + $this->getJson(self::BASE_ROUTE.'?trashed=with')->assertJsonCount(2, 'data'); } public function test_show_returns_institution_with_category_and_links(): void @@ -85,7 +95,7 @@ class InstitutionControllerTest extends TestCase $institution = Institution::factory()->create(); InstitutionLink::factory()->create(['institution_id' => $institution->id]); - $this->getJson("/api/institutions/{$institution->uuid}") + $this->getJson(self::BASE_ROUTE."/{$institution->uuid}") ->assertOk() ->assertJsonPath('data.uuid', $institution->uuid) ->assertJsonStructure(['data' => ['category', 'links']]); @@ -97,7 +107,7 @@ class InstitutionControllerTest extends TestCase $institution = Institution::factory()->create(); // L'id interno non è una route key valida. - $this->getJson("/api/institutions/{$institution->id}")->assertNotFound(); + $this->getJson(self::BASE_ROUTE."/{$institution->id}")->assertNotFound(); } // --- Scrittura (tier.admin) --------------------------------------------- @@ -107,15 +117,15 @@ class InstitutionControllerTest extends TestCase Storage::fake('public'); $this->actingAs($this->adminUser(), 'sanctum'); - $this->post('/api/institutions', $this->validPayload(), ['Accept' => 'application/json']) + $this->post(self::BASE_ROUTE, $this->validPayload(), self::JSON_HEADERS) ->assertCreated() - ->assertJsonPath('data.name', 'Test Museum') + ->assertJsonPath('data.name', self::TEST_MUSEUM) ->assertJsonStructure(['data' => ['uuid', 'category']]); - $institution = Institution::firstWhere('name', 'Test Museum'); + $institution = Institution::firstWhere('name', self::TEST_MUSEUM); $this->assertNotNull($institution); $this->assertNotEmpty($institution->uuid); - Storage::disk('public')->assertExists($institution->logo); + $this->assertTrue(Storage::disk('public')->exists($institution->logo)); } public function test_create_applies_default_color_and_storage_flag(): void @@ -126,10 +136,10 @@ class InstitutionControllerTest extends TestCase $payload = $this->validPayload(); unset($payload['color'], $payload['is_storage_place']); - $this->post('/api/institutions', $payload, ['Accept' => 'application/json'])->assertCreated(); + $this->post(self::BASE_ROUTE, $payload, self::JSON_HEADERS)->assertCreated(); $this->assertDatabaseHas('institutions', [ - 'name' => 'Test Museum', + 'name' => self::TEST_MUSEUM, 'color' => '#c5cae9', 'is_storage_place' => true, ]); @@ -139,7 +149,7 @@ class InstitutionControllerTest extends TestCase { $this->actingAs($this->adminUser(), 'sanctum'); - $this->postJson('/api/institutions', []) + $this->postJson(self::BASE_ROUTE, []) ->assertStatus(422) ->assertJsonValidationErrors(['category_id', 'name', 'abbreviation', 'lat', 'lon', 'logo']); } @@ -152,7 +162,7 @@ class InstitutionControllerTest extends TestCase $payload = $this->validPayload(['name' => 'New Name', 'category_id' => $institution->category_id]); unset($payload['logo']); - $this->put("/api/institutions/{$institution->uuid}", $payload, ['Accept' => 'application/json']) + $this->put(self::BASE_ROUTE."/{$institution->uuid}", $payload, self::JSON_HEADERS) ->assertOk() ->assertJsonPath('data.name', 'New Name'); @@ -163,15 +173,15 @@ class InstitutionControllerTest extends TestCase { Storage::fake('public'); $this->actingAs($this->adminUser(), 'sanctum'); - Storage::disk('public')->put('institution_logo/old.jpg', 'x'); - $institution = Institution::factory()->create(['logo' => 'institution_logo/old.jpg']); + Storage::disk('public')->put(self::OLD_LOGO_PATH, 'x'); + $institution = Institution::factory()->create(['logo' => self::OLD_LOGO_PATH]); $payload = $this->validPayload(['category_id' => $institution->category_id]); - $this->put("/api/institutions/{$institution->uuid}", $payload, ['Accept' => 'application/json'])->assertOk(); + $this->put(self::BASE_ROUTE."/{$institution->uuid}", $payload, self::JSON_HEADERS)->assertOk(); - Storage::disk('public')->assertMissing('institution_logo/old.jpg'); - Storage::disk('public')->assertExists($institution->fresh()->logo); + $this->assertFalse(Storage::disk('public')->exists(self::OLD_LOGO_PATH)); + $this->assertTrue(Storage::disk('public')->exists($institution->fresh()->logo)); } public function test_admin_can_soft_delete_an_institution(): void @@ -179,7 +189,7 @@ class InstitutionControllerTest extends TestCase $this->actingAs($this->adminUser(), 'sanctum'); $institution = Institution::factory()->create(); - $this->deleteJson("/api/institutions/{$institution->uuid}")->assertOk(); + $this->deleteJson(self::BASE_ROUTE."/{$institution->uuid}")->assertOk(); $this->assertSoftDeleted($institution); } @@ -190,7 +200,7 @@ class InstitutionControllerTest extends TestCase $institution = Institution::factory()->create(); $institution->delete(); - $this->postJson("/api/institutions/{$institution->uuid}/restore") + $this->postJson(self::BASE_ROUTE."/{$institution->uuid}/restore") ->assertOk() ->assertJsonPath('data.uuid', $institution->uuid); @@ -201,16 +211,16 @@ class InstitutionControllerTest extends TestCase { Storage::fake('public'); $this->actingAs($this->adminUser(), 'sanctum'); - Storage::disk('public')->put('institution_logo/gone.jpg', 'x'); - $institution = Institution::factory()->create(['logo' => 'institution_logo/gone.jpg']); + Storage::disk('public')->put(self::GONE_LOGO_PATH, 'x'); + $institution = Institution::factory()->create(['logo' => self::GONE_LOGO_PATH]); $link = InstitutionLink::factory()->create(['institution_id' => $institution->id]); $institution->delete(); - $this->deleteJson("/api/institutions/{$institution->uuid}/force")->assertOk(); + $this->deleteJson(self::BASE_ROUTE."/{$institution->uuid}/force")->assertOk(); $this->assertDatabaseMissing('institutions', ['id' => $institution->id]); $this->assertDatabaseMissing('institution_links', ['id' => $link->id]); - Storage::disk('public')->assertMissing('institution_logo/gone.jpg'); + Storage::assertMissing(self::GONE_LOGO_PATH); } // --- Autorizzazione ------------------------------------------------------ @@ -220,12 +230,22 @@ class InstitutionControllerTest extends TestCase $this->actingAs($this->operationalUser(), 'sanctum'); $institution = Institution::factory()->create(); - $this->postJson('/api/institutions', [])->assertForbidden(); - $this->deleteJson("/api/institutions/{$institution->uuid}")->assertForbidden(); + $this->postJson(self::BASE_ROUTE, [])->assertForbidden(); + $this->deleteJson(self::BASE_ROUTE."/{$institution->uuid}")->assertForbidden(); } - public function test_guests_cannot_access_institutions(): void + public function test_guests_can_read_institutions(): void { - $this->getJson('/api/institutions')->assertUnauthorized(); + Institution::factory()->create(); + + $this->getJson(self::BASE_ROUTE)->assertOk(); + } + + public function test_guests_cannot_write_institutions(): void + { + $institution = Institution::factory()->create(); + + $this->postJson(self::BASE_ROUTE, [])->assertUnauthorized(); + $this->deleteJson(self::BASE_ROUTE."/{$institution->uuid}")->assertUnauthorized(); } } diff --git a/backend/tests/Feature/InstitutionLinkControllerTest.php b/backend/tests/Feature/InstitutionLinkControllerTest.php index 1fde23c..5d8796d 100644 --- a/backend/tests/Feature/InstitutionLinkControllerTest.php +++ b/backend/tests/Feature/InstitutionLinkControllerTest.php @@ -140,10 +140,18 @@ class InstitutionLinkControllerTest extends TestCase $this->postJson("/api/institutions/{$institution->uuid}/links", $this->validPayload())->assertForbidden(); } - public function test_guests_cannot_access_links(): void + public function test_guests_can_read_links(): void + { + $institution = Institution::factory()->create(); + InstitutionLink::factory()->create(['institution_id' => $institution->id]); + + $this->getJson("/api/institutions/{$institution->uuid}/links")->assertOk(); + } + + public function test_guests_cannot_write_links(): void { $institution = Institution::factory()->create(); - $this->getJson("/api/institutions/{$institution->uuid}/links")->assertUnauthorized(); + $this->postJson("/api/institutions/{$institution->uuid}/links", $this->validPayload())->assertUnauthorized(); } } diff --git a/backend/tests/Feature/UserPositionControllerTest.php b/backend/tests/Feature/UserPositionControllerTest.php new file mode 100644 index 0000000..0102fac --- /dev/null +++ b/backend/tests/Feature/UserPositionControllerTest.php @@ -0,0 +1,44 @@ +actingAs($this->operationalUser(), 'sanctum'); + UserPosition::factory()->count(2)->create(); + + $this->getJson('/api/user-positions') + ->assertOk() + ->assertJsonStructure(['message', 'data' => [['id', 'value']]]); + } + + public function test_show_returns_a_single_position(): void + { + $this->actingAs($this->operationalUser(), 'sanctum'); + $position = UserPosition::factory()->create(); + + $this->getJson("/api/user-positions/{$position->id}") + ->assertOk() + ->assertJsonPath('data.id', $position->id) + ->assertJsonPath('data.value', $position->value); + } + + // --- Gating --------------------------------------------------------------- + + public function test_guests_can_read_positions(): void + { + UserPosition::factory()->create(); + + $this->getJson('/api/user-positions')->assertOk(); + } +} diff --git a/backend/tests/Feature/UserPositionSeederTest.php b/backend/tests/Feature/UserPositionSeederTest.php new file mode 100644 index 0000000..7b067c8 --- /dev/null +++ b/backend/tests/Feature/UserPositionSeederTest.php @@ -0,0 +1,37 @@ +seed(UserPositionSeeder::class); + + $this->assertSame(5, UserPosition::count()); + foreach ([ + UserPosition::PROFESSOR, + UserPosition::RESEARCHER, + UserPosition::PHD, + UserPosition::STUDENT, + UserPosition::ADMINISTRATIVE, + ] as $value) { + $this->assertDatabaseHas('user_positions', ['value' => $value]); + } + } + + public function test_it_is_idempotent(): void + { + $this->seed(UserPositionSeeder::class); + $this->seed(UserPositionSeeder::class); + + $this->assertSame(5, UserPosition::count()); + } +} diff --git a/backend/tests/Feature/UserRoleControllerTest.php b/backend/tests/Feature/UserRoleControllerTest.php index d5a8397..f463d51 100644 --- a/backend/tests/Feature/UserRoleControllerTest.php +++ b/backend/tests/Feature/UserRoleControllerTest.php @@ -33,131 +33,32 @@ class UserRoleControllerTest extends TestCase ->assertJsonPath('data.name', $role->name); } - public function test_usage_flags_system_role_as_locked(): void + // --- Statistiche (tier.admin) --------------------------------------------- + + public function test_admin_can_see_the_user_count_of_a_role(): void { - $this->actingAs($this->operationalUser(), 'sanctum'); - $role = UserRole::create(['name' => UserRole::SUPERVISOR]); + $this->actingAs($this->adminUser(), 'sanctum'); + $role = UserRole::factory()->create(); + $this->operationalUser(['role_id' => $role->id]); + $this->operationalUser(['role_id' => $role->id]); $this->getJson("/api/user-roles/{$role->id}/usage") ->assertOk() - ->assertJsonPath('in_use', true); + ->assertJsonPath('user_count', 2); } - public function test_usage_flags_free_custom_role_as_unlocked(): void + public function test_a_non_admin_cannot_see_usage_stats(): 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]); + $this->getJson("/api/user-roles/{$role->id}/usage")->assertForbidden(); } // --- Gating --------------------------------------------------------------- - public function test_non_admin_cannot_write(): void + public function test_guests_cannot_access_roles(): void { - $this->actingAs($this->operationalUser(), 'sanctum'); - - $this->postJson('/api/user-roles', ['name' => 'Nope'])->assertForbidden(); + $this->getJson('/api/user-roles')->assertUnauthorized(); } }