users and institutions route, tests fixed
This commit is contained in:
@@ -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);
|
||||
|
||||
28
backend/app/Http/Controllers/UserPositionController.php
Normal file
28
backend/app/Http/Controllers/UserPositionController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Traits\ApiResponse;
|
||||
use App\Models\Lists\UserPosition;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class UserPositionController extends Controller
|
||||
{
|
||||
use ApiResponse;
|
||||
|
||||
/**
|
||||
* Elenco delle position.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return $this->collectionResponse(UserPosition::all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dettaglio di una position.
|
||||
*/
|
||||
public function show(UserPosition $userPosition): JsonResponse
|
||||
{
|
||||
return $this->okResponse($userPosition);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, mixed>
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
@@ -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<string, mixed>
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreUserRoleRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* L'autorizzazione (solo Admin) è gestita dal middleware di rotta.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255|unique:user_roles,name',
|
||||
'description' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserRoleRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* L'autorizzazione (solo Admin) è gestita dal middleware di rotta.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
28
backend/app/Models/Lists/UserPosition.php
Normal file
28
backend/app/Models/Lists/UserPosition.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Lists;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use OwenIt\Auditing\Auditable as AuditableTrait;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
|
||||
class UserPosition extends Model implements Auditable
|
||||
{
|
||||
use AuditableTrait;
|
||||
use HasFactory;
|
||||
|
||||
public const PROFESSOR = 'Professor';
|
||||
|
||||
public const RESEARCHER = 'Researcher';
|
||||
|
||||
public const PHD = 'PhD';
|
||||
|
||||
public const STUDENT = 'Student';
|
||||
|
||||
public const ADMINISTRATIVE = 'Administrative personnel';
|
||||
|
||||
protected $table = 'user_positions';
|
||||
|
||||
protected $fillable = ['value'];
|
||||
}
|
||||
@@ -38,20 +38,4 @@ class UserRole extends Model implements Auditable
|
||||
{
|
||||
return $this->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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
26
backend/database/factories/Lists/UserPositionFactory.php
Normal file
26
backend/database/factories/Lists/UserPositionFactory.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories\Lists;
|
||||
|
||||
use App\Models\Lists\UserPosition;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<UserPosition>
|
||||
*/
|
||||
class UserPositionFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected $model = UserPosition::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'value' => fake()->unique()->word(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_positions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('value', 25)->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_positions');
|
||||
}
|
||||
};
|
||||
@@ -19,6 +19,7 @@ class DatabaseSeeder extends Seeder
|
||||
UserRoleSeeder::class,
|
||||
SystemUserSeeder::class,
|
||||
InstitutionCategorySeeder::class,
|
||||
UserPositionSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
29
backend/database/seeders/UserPositionSeeder.php
Normal file
29
backend/database/seeders/UserPositionSeeder.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Lists\UserPosition;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class UserPositionSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$positions = [
|
||||
UserPosition::PROFESSOR,
|
||||
UserPosition::RESEARCHER,
|
||||
UserPosition::PHD,
|
||||
UserPosition::STUDENT,
|
||||
UserPosition::ADMINISTRATIVE,
|
||||
];
|
||||
|
||||
foreach ($positions as $position) {
|
||||
UserPosition::updateOrCreate(
|
||||
['value' => $position]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
@@ -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']);
|
||||
|
||||
12
backend/routes/api/user-positions.php
Normal file
12
backend/routes/api/user-positions.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\UserPositionController;
|
||||
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.
|
||||
|
||||
// Lookup fissa, non gestibile da interfaccia: solo lettura.
|
||||
Route::middleware('throttle:public')->group(function () {
|
||||
Route::apiResource('user-positions', UserPositionController::class)->only(['index', 'show']);
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
@@ -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<string, mixed> $overrides
|
||||
* @return array<string, mixed>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
44
backend/tests/Feature/UserPositionControllerTest.php
Normal file
44
backend/tests/Feature/UserPositionControllerTest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Lists\UserPosition;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserPositionControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
// --- Lettura (pubblica, throttle:public) ----------------------------------
|
||||
|
||||
public function test_index_lists_positions_for_an_operational_user(): void
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
37
backend/tests/Feature/UserPositionSeederTest.php
Normal file
37
backend/tests/Feature/UserPositionSeederTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Lists\UserPosition;
|
||||
use Database\Seeders\UserPositionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserPositionSeederTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_seeds_the_base_positions(): void
|
||||
{
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user