feature test su institution
This commit is contained in:
74
backend/tests/Concerns/CreatesLegacyDatabase.php
Normal file
74
backend/tests/Concerns/CreatesLegacyDatabase.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Concerns;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Costruisce una connessione `legacy` SQLite in-memory che replica le tabelle v1
|
||||
* lette dall'ETL (`institution`), così gli importer si testano senza un MySQL v1.
|
||||
*/
|
||||
trait CreatesLegacyDatabase
|
||||
{
|
||||
/** Riconfigura la connessione `legacy` su SQLite :memory: e ne crea lo schema v1. */
|
||||
protected function fakeLegacyConnection(): void
|
||||
{
|
||||
config()->set('database.connections.legacy', [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => false,
|
||||
]);
|
||||
DB::purge('legacy');
|
||||
|
||||
$schema = Schema::connection('legacy');
|
||||
$schema->dropIfExists('institution');
|
||||
|
||||
$schema->create('institution', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('id')->primary();
|
||||
$table->unsignedBigInteger('category');
|
||||
$table->string('name', 255);
|
||||
$table->string('abbreviation', 5);
|
||||
$table->string('address', 255);
|
||||
$table->decimal('lat', 10, 6);
|
||||
$table->decimal('lon', 10, 6);
|
||||
$table->string('url', 2000)->nullable();
|
||||
$table->string('logo', 255);
|
||||
$table->string('uuid', 36);
|
||||
$table->string('color', 50)->nullable();
|
||||
$table->string('city', 100);
|
||||
$table->boolean('is_storage_place');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserisce una riga `institution` legacy e ne restituisce l'id.
|
||||
*
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
protected function insertLegacyInstitution(array $overrides = []): int
|
||||
{
|
||||
$row = array_merge([
|
||||
'id' => fake()->unique()->numberBetween(1, 1_000_000),
|
||||
'category' => 3,
|
||||
'name' => 'Some Museum',
|
||||
'abbreviation' => 'SM',
|
||||
'address' => 'Main Street 1',
|
||||
'lat' => 55.704660,
|
||||
'lon' => 13.191007,
|
||||
'url' => 'https://example.org/',
|
||||
'logo' => 'default.jpg',
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'color' => '#abcdef',
|
||||
'city' => 'Lund',
|
||||
'is_storage_place' => 1,
|
||||
], $overrides);
|
||||
|
||||
DB::connection('legacy')->table('institution')->insert($row);
|
||||
|
||||
return (int) $row['id'];
|
||||
}
|
||||
}
|
||||
168
backend/tests/Feature/Etl/InstitutionImporterTest.php
Normal file
168
backend/tests/Feature/Etl/InstitutionImporterTest.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Etl;
|
||||
|
||||
use App\Etl\Importers\InstitutionImporter;
|
||||
use App\Models\Institution;
|
||||
use Database\Seeders\InstitutionCategorySeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Concerns\CreatesLegacyDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstitutionImporterTest extends TestCase
|
||||
{
|
||||
use CreatesLegacyDatabase;
|
||||
use RefreshDatabase;
|
||||
|
||||
/** Categorie v2 con gli id legacy fissi (2,3,4,6). */
|
||||
private function seedV2Categories(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
}
|
||||
|
||||
public function test_it_imports_an_institution_and_creates_the_official_link(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution([
|
||||
'id' => 1,
|
||||
'category' => 3,
|
||||
'name' => 'Blekinge Museum',
|
||||
'abbreviation' => 'BLM',
|
||||
'city' => 'Karlskrona',
|
||||
'url' => 'https://blekingemuseum.se/',
|
||||
]);
|
||||
|
||||
$summary = (new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertSame(1, $summary->created);
|
||||
$this->assertDatabaseHas('institutions', [
|
||||
'legacy_id' => 1,
|
||||
'category_id' => 3,
|
||||
'name' => 'Blekinge Museum',
|
||||
'city' => 'Karlskrona',
|
||||
]);
|
||||
|
||||
$institution = Institution::where('legacy_id', 1)->firstOrFail();
|
||||
$this->assertNotEmpty($institution->uuid);
|
||||
$this->assertDatabaseHas('institution_links', [
|
||||
'institution_id' => $institution->id,
|
||||
'type' => 'official',
|
||||
'url' => 'https://blekingemuseum.se/',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_creates_no_link_when_the_url_is_blank(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 21, 'url' => ' ']);
|
||||
|
||||
(new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertDatabaseCount('institution_links', 0);
|
||||
}
|
||||
|
||||
public function test_it_trims_textual_fields(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 20, 'name' => 'Lund University Library ']);
|
||||
|
||||
(new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertDatabaseHas('institutions', ['legacy_id' => 20, 'name' => 'Lund University Library']);
|
||||
}
|
||||
|
||||
public function test_it_falls_back_to_the_default_color_when_legacy_color_is_blank(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1, 'color' => null]);
|
||||
|
||||
(new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertDatabaseHas('institutions', ['legacy_id' => 1, 'color' => '#c5cae9']);
|
||||
}
|
||||
|
||||
public function test_it_skips_institutions_with_an_unknown_category(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 99, 'category' => 999]);
|
||||
|
||||
$summary = (new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertSame(1, $summary->skipped);
|
||||
$this->assertNotEmpty($summary->warnings);
|
||||
$this->assertDatabaseMissing('institutions', ['legacy_id' => 99]);
|
||||
}
|
||||
|
||||
public function test_it_regenerates_the_uuid_instead_of_copying_the_legacy_one(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$legacyUuid = '00000000-0000-0000-0000-000000000000';
|
||||
$this->insertLegacyInstitution(['id' => 1, 'uuid' => $legacyUuid]);
|
||||
|
||||
(new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertNotSame($legacyUuid, Institution::where('legacy_id', 1)->value('uuid'));
|
||||
}
|
||||
|
||||
public function test_it_is_idempotent_and_keeps_the_uuid_stable(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1]);
|
||||
|
||||
$first = (new InstitutionImporter)->import(false);
|
||||
$uuid = Institution::where('legacy_id', 1)->value('uuid');
|
||||
$second = (new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertSame(1, $first->created);
|
||||
$this->assertSame(0, $second->created);
|
||||
$this->assertSame(1, $second->updated);
|
||||
$this->assertDatabaseCount('institutions', 1);
|
||||
$this->assertSame($uuid, Institution::where('legacy_id', 1)->value('uuid'));
|
||||
}
|
||||
|
||||
public function test_dry_run_writes_nothing(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1]);
|
||||
|
||||
$summary = (new InstitutionImporter)->import(true);
|
||||
|
||||
$this->assertSame(1, $summary->created);
|
||||
$this->assertDatabaseCount('institutions', 0);
|
||||
}
|
||||
|
||||
public function test_dry_run_counts_an_existing_institution_as_updated(): void
|
||||
{
|
||||
$this->seedV2Categories();
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1]);
|
||||
|
||||
(new InstitutionImporter)->import(false);
|
||||
$summary = (new InstitutionImporter)->import(true);
|
||||
|
||||
$this->assertSame(0, $summary->created);
|
||||
$this->assertSame(1, $summary->updated);
|
||||
$this->assertDatabaseCount('institutions', 1);
|
||||
}
|
||||
|
||||
public function test_it_warns_and_skips_when_the_v2_lookup_is_empty(): void
|
||||
{
|
||||
// Nessun seed delle categorie v2.
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1]);
|
||||
|
||||
$summary = (new InstitutionImporter)->import(false);
|
||||
|
||||
$this->assertNotEmpty($summary->warnings);
|
||||
$this->assertSame(0, $summary->total());
|
||||
$this->assertDatabaseCount('institutions', 0);
|
||||
}
|
||||
}
|
||||
94
backend/tests/Feature/Etl/V1ImportCommandTest.php
Normal file
94
backend/tests/Feature/Etl/V1ImportCommandTest.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Etl;
|
||||
|
||||
use Database\Seeders\InstitutionCategorySeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\CreatesLegacyDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class V1ImportCommandTest extends TestCase
|
||||
{
|
||||
use CreatesLegacyDatabase;
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_imports_and_reports_counts(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1]);
|
||||
|
||||
$this->artisan('v1:import')
|
||||
->expectsOutputToContain('created 1, updated 0, skipped 0')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseCount('institutions', 1);
|
||||
}
|
||||
|
||||
public function test_dry_run_writes_nothing(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1]);
|
||||
|
||||
$this->artisan('v1:import --dry-run')
|
||||
->expectsOutputToContain('DRY-RUN')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseCount('institutions', 0);
|
||||
}
|
||||
|
||||
public function test_only_filter_skips_unmatched_importers(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 1]);
|
||||
|
||||
$this->artisan('v1:import --only=nonexistent')->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseCount('institutions', 0);
|
||||
}
|
||||
|
||||
public function test_it_prints_importer_warnings(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
$this->fakeLegacyConnection();
|
||||
$this->insertLegacyInstitution(['id' => 99, 'category' => 999]);
|
||||
|
||||
$this->artisan('v1:import')
|
||||
->expectsOutputToContain('category 999')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_it_fails_when_an_importer_throws(): void
|
||||
{
|
||||
// Connessione raggiungibile (getPdo ok) ma senza la tabella `institution`:
|
||||
// l'import lancia, il comando intercetta e fallisce.
|
||||
config()->set('database.connections.legacy', [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]);
|
||||
DB::purge('legacy');
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
|
||||
$this->artisan('v1:import')
|
||||
->expectsOutputToContain('failed')
|
||||
->assertFailed();
|
||||
}
|
||||
|
||||
public function test_it_fails_when_the_legacy_connection_is_unavailable(): void
|
||||
{
|
||||
config()->set('database.connections.legacy', [
|
||||
'driver' => 'sqlite',
|
||||
'database' => '/nonexistent/path/legacy.sqlite',
|
||||
'prefix' => '',
|
||||
]);
|
||||
DB::purge('legacy');
|
||||
|
||||
$this->artisan('v1:import')
|
||||
->expectsOutputToContain('Legacy connection unavailable')
|
||||
->assertFailed();
|
||||
}
|
||||
}
|
||||
110
backend/tests/Feature/InstitutionCategoryControllerTest.php
Normal file
110
backend/tests/Feature/InstitutionCategoryControllerTest.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Institution;
|
||||
use App\Models\Lists\InstitutionCategory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstitutionCategoryControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
// --- Lettura (tier.app) --------------------------------------------------
|
||||
|
||||
public function test_index_lists_categories(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
InstitutionCategory::factory()->count(2)->create();
|
||||
|
||||
$this->getJson('/api/institution-categories')
|
||||
->assertOk()
|
||||
->assertJsonStructure(['message', 'data' => [['id', 'value']]])
|
||||
->assertJsonCount(2, 'data');
|
||||
}
|
||||
|
||||
public function test_show_returns_a_single_category(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$category = InstitutionCategory::factory()->create();
|
||||
|
||||
$this->getJson("/api/institution-categories/{$category->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.id', $category->id);
|
||||
}
|
||||
|
||||
public function test_usage_reflects_whether_the_category_is_used(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$free = InstitutionCategory::factory()->create();
|
||||
$used = InstitutionCategory::factory()->create();
|
||||
Institution::factory()->create(['category_id' => $used->id]);
|
||||
|
||||
$this->getJson("/api/institution-categories/{$free->id}/usage")->assertJsonPath('in_use', false);
|
||||
$this->getJson("/api/institution-categories/{$used->id}/usage")->assertJsonPath('in_use', true);
|
||||
}
|
||||
|
||||
// --- Scrittura (tier.admin) ---------------------------------------------
|
||||
|
||||
public function test_admin_can_create_a_category(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->postJson('/api/institution-categories', ['value' => 'gallery'])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.value', 'gallery');
|
||||
|
||||
$this->assertDatabaseHas('institution_categories', ['value' => 'gallery']);
|
||||
}
|
||||
|
||||
public function test_create_rejects_a_duplicate_value(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
InstitutionCategory::factory()->create(['value' => 'gallery']);
|
||||
|
||||
$this->postJson('/api/institution-categories', ['value' => 'gallery'])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('value');
|
||||
}
|
||||
|
||||
public function test_admin_can_update_a_category(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$category = InstitutionCategory::factory()->create();
|
||||
|
||||
$this->putJson("/api/institution-categories/{$category->id}", ['value' => 'archive'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.value', 'archive');
|
||||
}
|
||||
|
||||
public function test_cannot_delete_a_category_in_use(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$category = InstitutionCategory::factory()->create();
|
||||
Institution::factory()->create(['category_id' => $category->id]);
|
||||
|
||||
$this->deleteJson("/api/institution-categories/{$category->id}")
|
||||
->assertStatus(409)
|
||||
->assertJsonPath('in_use', true);
|
||||
|
||||
$this->assertDatabaseHas('institution_categories', ['id' => $category->id]);
|
||||
}
|
||||
|
||||
public function test_admin_can_delete_a_free_category(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$category = InstitutionCategory::factory()->create();
|
||||
|
||||
$this->deleteJson("/api/institution-categories/{$category->id}")->assertOk();
|
||||
|
||||
$this->assertDatabaseMissing('institution_categories', ['id' => $category->id]);
|
||||
}
|
||||
|
||||
public function test_a_non_admin_cannot_write(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
|
||||
$this->postJson('/api/institution-categories', ['value' => 'x'])->assertForbidden();
|
||||
}
|
||||
}
|
||||
40
backend/tests/Feature/InstitutionCategorySeederTest.php
Normal file
40
backend/tests/Feature/InstitutionCategorySeederTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Lists\InstitutionCategory;
|
||||
use Database\Seeders\InstitutionCategorySeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstitutionCategorySeederTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_seeds_the_categories_with_fixed_legacy_ids(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
|
||||
$this->assertSame(4, InstitutionCategory::count());
|
||||
$this->assertSame('library', InstitutionCategory::find(2)?->value);
|
||||
$this->assertSame('museum', InstitutionCategory::find(3)?->value);
|
||||
$this->assertSame('public administration', InstitutionCategory::find(4)?->value);
|
||||
$this->assertSame('research institute', InstitutionCategory::find(6)?->value);
|
||||
}
|
||||
|
||||
public function test_it_excludes_uncategorized_and_the_gap_ids(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
|
||||
$this->assertNull(InstitutionCategory::find(1));
|
||||
$this->assertNull(InstitutionCategory::find(5));
|
||||
}
|
||||
|
||||
public function test_it_is_idempotent(): void
|
||||
{
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
$this->seed(InstitutionCategorySeeder::class);
|
||||
|
||||
$this->assertSame(4, InstitutionCategory::count());
|
||||
}
|
||||
}
|
||||
231
backend/tests/Feature/InstitutionControllerTest.php
Normal file
231
backend/tests/Feature/InstitutionControllerTest.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Institution;
|
||||
use App\Models\InstitutionLink;
|
||||
use App\Models\Lists\InstitutionCategory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstitutionControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function validPayload(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'category_id' => InstitutionCategory::factory()->create()->id,
|
||||
'name' => 'Test Museum',
|
||||
'abbreviation' => 'TM',
|
||||
'address' => 'Some Road 1',
|
||||
'city' => 'Lund',
|
||||
'lat' => 55.7,
|
||||
'lon' => 13.19,
|
||||
'color' => '#ffffff',
|
||||
'is_storage_place' => true,
|
||||
'logo' => UploadedFile::fake()->image('logo.png'),
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
// --- Lettura (tier.app) --------------------------------------------------
|
||||
|
||||
public function test_index_lists_institutions_for_an_operational_user(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
Institution::factory()->count(2)->create();
|
||||
|
||||
$this->getJson('/api/institutions')
|
||||
->assertOk()
|
||||
->assertJsonStructure(['message', 'data', 'meta' => ['current_page', 'total']])
|
||||
->assertJsonCount(2, 'data');
|
||||
}
|
||||
|
||||
public function test_index_filters_by_category_and_search(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$category = InstitutionCategory::factory()->create();
|
||||
Institution::factory()->create(['category_id' => $category->id, 'name' => 'Blekinge Museum']);
|
||||
Institution::factory()->create(['name' => 'Other Place']);
|
||||
|
||||
$this->getJson("/api/institutions?category_id={$category->id}")
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.name', 'Blekinge Museum');
|
||||
|
||||
$this->getJson('/api/institutions?search=Blekinge')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data');
|
||||
}
|
||||
|
||||
public function test_index_can_list_only_trashed(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$trashed = Institution::factory()->create();
|
||||
Institution::factory()->create();
|
||||
$trashed->delete();
|
||||
|
||||
$this->getJson('/api/institutions')->assertJsonCount(1, 'data');
|
||||
$this->getJson('/api/institutions?trashed=only')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', $trashed->id);
|
||||
$this->getJson('/api/institutions?trashed=with')->assertJsonCount(2, 'data');
|
||||
}
|
||||
|
||||
public function test_show_returns_institution_with_category_and_links(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
InstitutionLink::factory()->create(['institution_id' => $institution->id]);
|
||||
|
||||
$this->getJson("/api/institutions/{$institution->uuid}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.uuid', $institution->uuid)
|
||||
->assertJsonStructure(['data' => ['category', 'links']]);
|
||||
}
|
||||
|
||||
public function test_show_resolves_by_uuid_not_id(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
// L'id interno non è una route key valida.
|
||||
$this->getJson("/api/institutions/{$institution->id}")->assertNotFound();
|
||||
}
|
||||
|
||||
// --- Scrittura (tier.admin) ---------------------------------------------
|
||||
|
||||
public function test_admin_can_create_an_institution_with_a_logo(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->post('/api/institutions', $this->validPayload(), ['Accept' => 'application/json'])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.name', 'Test Museum')
|
||||
->assertJsonStructure(['data' => ['uuid', 'category']]);
|
||||
|
||||
$institution = Institution::firstWhere('name', 'Test Museum');
|
||||
$this->assertNotNull($institution);
|
||||
$this->assertNotEmpty($institution->uuid);
|
||||
Storage::disk('public')->assertExists($institution->logo);
|
||||
}
|
||||
|
||||
public function test_create_applies_default_color_and_storage_flag(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$payload = $this->validPayload();
|
||||
unset($payload['color'], $payload['is_storage_place']);
|
||||
|
||||
$this->post('/api/institutions', $payload, ['Accept' => 'application/json'])->assertCreated();
|
||||
|
||||
$this->assertDatabaseHas('institutions', [
|
||||
'name' => 'Test Museum',
|
||||
'color' => '#c5cae9',
|
||||
'is_storage_place' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_validates_required_fields(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
|
||||
$this->postJson('/api/institutions', [])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['category_id', 'name', 'abbreviation', 'lat', 'lon', 'logo']);
|
||||
}
|
||||
|
||||
public function test_admin_can_update_an_institution_keeping_the_logo(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create(['logo' => 'institution_logo/keep.jpg', 'name' => 'Old']);
|
||||
|
||||
$payload = $this->validPayload(['name' => 'New Name', 'category_id' => $institution->category_id]);
|
||||
unset($payload['logo']);
|
||||
|
||||
$this->put("/api/institutions/{$institution->uuid}", $payload, ['Accept' => 'application/json'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.name', 'New Name');
|
||||
|
||||
$this->assertSame('institution_logo/keep.jpg', $institution->fresh()->logo);
|
||||
}
|
||||
|
||||
public function test_update_replaces_the_logo_when_a_new_one_is_uploaded(): void
|
||||
{
|
||||
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']);
|
||||
|
||||
$payload = $this->validPayload(['category_id' => $institution->category_id]);
|
||||
|
||||
$this->put("/api/institutions/{$institution->uuid}", $payload, ['Accept' => 'application/json'])->assertOk();
|
||||
|
||||
Storage::disk('public')->assertMissing('institution_logo/old.jpg');
|
||||
Storage::disk('public')->assertExists($institution->fresh()->logo);
|
||||
}
|
||||
|
||||
public function test_admin_can_soft_delete_an_institution(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$this->deleteJson("/api/institutions/{$institution->uuid}")->assertOk();
|
||||
|
||||
$this->assertSoftDeleted($institution);
|
||||
}
|
||||
|
||||
public function test_admin_can_restore_a_trashed_institution(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
$institution->delete();
|
||||
|
||||
$this->postJson("/api/institutions/{$institution->uuid}/restore")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.uuid', $institution->uuid);
|
||||
|
||||
$this->assertNotSoftDeleted($institution);
|
||||
}
|
||||
|
||||
public function test_admin_can_force_delete_an_institution_and_its_logo_and_links(): void
|
||||
{
|
||||
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']);
|
||||
$link = InstitutionLink::factory()->create(['institution_id' => $institution->id]);
|
||||
$institution->delete();
|
||||
|
||||
$this->deleteJson("/api/institutions/{$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');
|
||||
}
|
||||
|
||||
// --- Autorizzazione ------------------------------------------------------
|
||||
|
||||
public function test_a_non_admin_cannot_write(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$this->postJson('/api/institutions', [])->assertForbidden();
|
||||
$this->deleteJson("/api/institutions/{$institution->uuid}")->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_guests_cannot_access_institutions(): void
|
||||
{
|
||||
$this->getJson('/api/institutions')->assertUnauthorized();
|
||||
}
|
||||
}
|
||||
149
backend/tests/Feature/InstitutionLinkControllerTest.php
Normal file
149
backend/tests/Feature/InstitutionLinkControllerTest.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Enums\InstitutionLinkType;
|
||||
use App\Models\Institution;
|
||||
use App\Models\InstitutionLink;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstitutionLinkControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function validPayload(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'url' => 'https://example.org',
|
||||
'type' => InstitutionLinkType::Official->value,
|
||||
'label' => 'Official site',
|
||||
'sort_order' => 1,
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
// --- Lettura (tier.app) --------------------------------------------------
|
||||
|
||||
public function test_index_lists_links_of_an_institution_ordered(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
$second = InstitutionLink::factory()->create(['institution_id' => $institution->id, 'sort_order' => 2]);
|
||||
$first = InstitutionLink::factory()->create(['institution_id' => $institution->id, 'sort_order' => 1]);
|
||||
|
||||
$this->getJson("/api/institutions/{$institution->uuid}/links")
|
||||
->assertOk()
|
||||
->assertJsonCount(2, 'data')
|
||||
->assertJsonPath('data.0.id', $first->id)
|
||||
->assertJsonPath('data.1.id', $second->id);
|
||||
}
|
||||
|
||||
public function test_show_returns_a_single_link(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
$link = InstitutionLink::factory()->create(['institution_id' => $institution->id]);
|
||||
|
||||
$this->getJson("/api/institutions/{$institution->uuid}/links/{$link->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.id', $link->id);
|
||||
}
|
||||
|
||||
public function test_show_returns_not_found_when_the_link_belongs_to_another_institution(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
$otherInstitution = Institution::factory()->create();
|
||||
$link = InstitutionLink::factory()->create(['institution_id' => $otherInstitution->id]);
|
||||
|
||||
$this->getJson("/api/institutions/{$institution->uuid}/links/{$link->id}")->assertNotFound();
|
||||
}
|
||||
|
||||
// --- Scrittura (tier.admin) ---------------------------------------------
|
||||
|
||||
public function test_admin_can_add_a_link_to_an_institution(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$this->postJson("/api/institutions/{$institution->uuid}/links", $this->validPayload())
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.url', 'https://example.org')
|
||||
->assertJsonPath('data.institution_id', $institution->id);
|
||||
|
||||
$this->assertDatabaseHas('institution_links', [
|
||||
'institution_id' => $institution->id,
|
||||
'url' => 'https://example.org',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_defaults_sort_order_to_zero_when_missing(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
$payload = $this->validPayload();
|
||||
unset($payload['sort_order']);
|
||||
|
||||
$this->postJson("/api/institutions/{$institution->uuid}/links", $payload)
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.sort_order', 0);
|
||||
}
|
||||
|
||||
public function test_store_validates_required_fields(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$this->postJson("/api/institutions/{$institution->uuid}/links", [])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['url', 'type']);
|
||||
}
|
||||
|
||||
public function test_admin_can_update_a_link(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
$link = InstitutionLink::factory()->create(['institution_id' => $institution->id]);
|
||||
|
||||
$this->putJson(
|
||||
"/api/institutions/{$institution->uuid}/links/{$link->id}",
|
||||
$this->validPayload(['url' => 'https://updated.example.org'])
|
||||
)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.url', 'https://updated.example.org');
|
||||
|
||||
$this->assertSame('https://updated.example.org', $link->fresh()->url);
|
||||
}
|
||||
|
||||
public function test_admin_can_delete_a_link(): void
|
||||
{
|
||||
$this->actingAs($this->adminUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
$link = InstitutionLink::factory()->create(['institution_id' => $institution->id]);
|
||||
|
||||
$this->deleteJson("/api/institutions/{$institution->uuid}/links/{$link->id}")->assertOk();
|
||||
|
||||
$this->assertDatabaseMissing('institution_links', ['id' => $link->id]);
|
||||
}
|
||||
|
||||
// --- Autorizzazione ------------------------------------------------------
|
||||
|
||||
public function test_a_non_admin_cannot_write(): void
|
||||
{
|
||||
$this->actingAs($this->operationalUser(), 'sanctum');
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$this->postJson("/api/institutions/{$institution->uuid}/links", $this->validPayload())->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_guests_cannot_access_links(): void
|
||||
{
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$this->getJson("/api/institutions/{$institution->uuid}/links")->assertUnauthorized();
|
||||
}
|
||||
}
|
||||
30
backend/tests/Feature/Models/InstitutionLinkTest.php
Normal file
30
backend/tests/Feature/Models/InstitutionLinkTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Models;
|
||||
|
||||
use App\Enums\InstitutionLinkType;
|
||||
use App\Models\Institution;
|
||||
use App\Models\InstitutionLink;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstitutionLinkTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_type_is_cast_to_enum(): void
|
||||
{
|
||||
$link = InstitutionLink::factory()->create(['type' => InstitutionLinkType::Ticketing]);
|
||||
|
||||
$this->assertInstanceOf(InstitutionLinkType::class, $link->fresh()->type);
|
||||
$this->assertSame(InstitutionLinkType::Ticketing, $link->fresh()->type);
|
||||
}
|
||||
|
||||
public function test_it_belongs_to_an_institution(): void
|
||||
{
|
||||
$institution = Institution::factory()->create();
|
||||
$link = InstitutionLink::factory()->create(['institution_id' => $institution->id]);
|
||||
|
||||
$this->assertTrue($link->institution->is($institution));
|
||||
}
|
||||
}
|
||||
99
backend/tests/Feature/Models/InstitutionTest.php
Normal file
99
backend/tests/Feature/Models/InstitutionTest.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Models;
|
||||
|
||||
use App\Models\Institution;
|
||||
use App\Models\InstitutionLink;
|
||||
use App\Models\Lists\InstitutionCategory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InstitutionTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_uuid_is_generated_on_create(): void
|
||||
{
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$this->assertNotEmpty($institution->uuid);
|
||||
$this->assertSame(36, strlen((string) $institution->uuid));
|
||||
}
|
||||
|
||||
public function test_two_institutions_get_distinct_uuids(): void
|
||||
{
|
||||
$a = Institution::factory()->create();
|
||||
$b = Institution::factory()->create();
|
||||
|
||||
$this->assertNotSame($a->uuid, $b->uuid);
|
||||
}
|
||||
|
||||
public function test_a_provided_uuid_is_not_overwritten(): void
|
||||
{
|
||||
$uuid = (string) Str::uuid();
|
||||
|
||||
$institution = Institution::factory()->make();
|
||||
$institution->uuid = $uuid;
|
||||
$institution->save();
|
||||
|
||||
$this->assertSame($uuid, $institution->fresh()->uuid);
|
||||
}
|
||||
|
||||
public function test_route_key_is_the_uuid(): void
|
||||
{
|
||||
$this->assertSame('uuid', (new Institution)->getRouteKeyName());
|
||||
}
|
||||
|
||||
public function test_attributes_are_cast(): void
|
||||
{
|
||||
$institution = Institution::factory()->create([
|
||||
'is_storage_place' => 1,
|
||||
'lat' => '55.700000',
|
||||
'lon' => '13.190000',
|
||||
'legacy_id' => '42',
|
||||
]);
|
||||
|
||||
$fresh = $institution->fresh();
|
||||
|
||||
$this->assertIsBool($fresh->is_storage_place);
|
||||
$this->assertIsFloat($fresh->lat);
|
||||
$this->assertIsFloat($fresh->lon);
|
||||
$this->assertSame(42, $fresh->legacy_id);
|
||||
}
|
||||
|
||||
public function test_it_belongs_to_a_category(): void
|
||||
{
|
||||
$category = InstitutionCategory::factory()->create();
|
||||
$institution = Institution::factory()->create(['category_id' => $category->id]);
|
||||
|
||||
$this->assertTrue($institution->category->is($category));
|
||||
}
|
||||
|
||||
public function test_a_category_has_many_institutions(): void
|
||||
{
|
||||
$category = InstitutionCategory::factory()->create();
|
||||
Institution::factory()->count(2)->create(['category_id' => $category->id]);
|
||||
|
||||
$this->assertCount(2, $category->institutions);
|
||||
}
|
||||
|
||||
public function test_it_has_many_links(): void
|
||||
{
|
||||
$institution = Institution::factory()->create();
|
||||
InstitutionLink::factory()->create(['institution_id' => $institution->id]);
|
||||
|
||||
$this->assertCount(1, $institution->refresh()->links);
|
||||
}
|
||||
|
||||
public function test_it_is_soft_deleted(): void
|
||||
{
|
||||
$institution = Institution::factory()->create();
|
||||
|
||||
$institution->delete();
|
||||
|
||||
$this->assertSoftDeleted($institution);
|
||||
$this->assertSame(0, Institution::count());
|
||||
$this->assertSame(1, Institution::withTrashed()->count());
|
||||
}
|
||||
}
|
||||
27
backend/tests/Unit/InstitutionLinkTypeTest.php
Normal file
27
backend/tests/Unit/InstitutionLinkTypeTest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Enums\InstitutionLinkType;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class InstitutionLinkTypeTest extends TestCase
|
||||
{
|
||||
public function test_every_case_has_a_non_empty_label(): void
|
||||
{
|
||||
foreach (InstitutionLinkType::cases() as $case) {
|
||||
$this->assertNotSame('', $case->label());
|
||||
}
|
||||
}
|
||||
|
||||
public function test_official_case_value_and_label(): void
|
||||
{
|
||||
$this->assertSame('official', InstitutionLinkType::Official->value);
|
||||
$this->assertSame('Official website', InstitutionLinkType::Official->label());
|
||||
}
|
||||
|
||||
public function test_it_is_a_string_backed_enum(): void
|
||||
{
|
||||
$this->assertSame(InstitutionLinkType::Social, InstitutionLinkType::from('social'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user