Files
dyncoll-dev/backend/tests/Feature/UserAffiliationControllerTest.php
2026-06-24 16:30:21 +02:00

370 lines
13 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Institution;
use App\Models\Lists\UserPosition;
use App\Models\User;
use App\Models\UserAffiliation;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserAffiliationControllerTest extends TestCase
{
use RefreshDatabase;
private const BASE_ROUTE = '/api/user-affiliations';
private const JSON_HEADERS = ['Accept' => 'application/json'];
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
private function validPayload(array $overrides = []): array
{
return array_merge([
'institution_id' => Institution::factory()->create()->id,
'user_id' => User::factory()->create()->id,
'user_position_id' => UserPosition::factory()->create()->id,
'start_year' => 2020,
'end_year' => null,
], $overrides);
}
// --- Lettura (pubblica, throttle:public) ----------------------------------
public function test_index_lists_affiliations_for_an_operational_user(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
UserAffiliation::factory()->count(2)->create();
$this->getJson(self::BASE_ROUTE)
->assertOk()
->assertJsonStructure(['message', 'data', 'meta' => ['current_page', 'total']])
->assertJsonCount(2, 'data');
}
public function test_index_filters_by_institution_user_position_and_open(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$institution = Institution::factory()->create();
$user = User::factory()->create();
$position = UserPosition::factory()->create();
$open = UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'user_position_id' => $position->id,
'end_year' => null,
]);
UserAffiliation::factory()->create(['end_year' => 2019]);
$this->getJson(self::BASE_ROUTE."?institution_id={$institution->id}")
->assertOk()
->assertJsonCount(1, 'data');
$this->getJson(self::BASE_ROUTE."?user_id={$user->id}")
->assertOk()
->assertJsonCount(1, 'data');
$this->getJson(self::BASE_ROUTE."?user_position_id={$position->id}")
->assertOk()
->assertJsonCount(1, 'data');
$this->getJson(self::BASE_ROUTE.'?open=1')
->assertOk()
->assertJsonPath('data.0.id', $open->id)
->assertJsonCount(1, 'data');
$this->getJson(self::BASE_ROUTE.'?open=0')
->assertOk()
->assertJsonCount(1, 'data');
}
public function test_index_can_list_only_trashed(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$trashed = UserAffiliation::factory()->create();
UserAffiliation::factory()->create();
$trashed->delete();
$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(self::BASE_ROUTE.'?trashed=with')->assertJsonCount(2, 'data');
}
public function test_show_returns_affiliation_with_institution_user_and_position(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$affiliation = UserAffiliation::factory()->create();
$this->getJson(self::BASE_ROUTE."/{$affiliation->id}")
->assertOk()
->assertJsonPath('data.id', $affiliation->id)
->assertJsonStructure(['data' => ['institution', 'user', 'user_position']]);
}
public function test_legacy_fields_are_hidden_from_the_json_output(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$affiliation = UserAffiliation::factory()->create([
'legacy_user_id' => 123,
'legacy_institution_id' => 456,
]);
$this->getJson(self::BASE_ROUTE."/{$affiliation->id}")
->assertOk()
->assertJsonMissingPath('data.legacy_user_id')
->assertJsonMissingPath('data.legacy_institution_id');
}
// --- Scrittura (tier.app, non riservata all'Admin) ------------------------
public function test_operational_user_can_create_an_affiliation(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$this->postJson(self::BASE_ROUTE, $this->validPayload(), self::JSON_HEADERS)
->assertCreated()
->assertJsonStructure(['data' => ['id', 'institution', 'user', 'user_position']]);
$this->assertDatabaseCount('user_affiliations', 1);
}
public function test_create_validates_required_fields(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$this->postJson(self::BASE_ROUTE, [])
->assertStatus(422)
->assertJsonValidationErrors(['institution_id', 'user_id', 'user_position_id', 'start_year']);
}
public function test_create_rejects_a_second_open_affiliation_for_the_same_institution_and_user(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$institution = Institution::factory()->create();
$user = User::factory()->create();
UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]);
$this->postJson(self::BASE_ROUTE, $this->validPayload([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]))
->assertStatus(422)
->assertJsonValidationErrors(['institution_id']);
}
public function test_create_allows_a_new_open_affiliation_after_the_previous_one_was_closed(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$institution = Institution::factory()->create();
$user = User::factory()->create();
UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => 2019,
]);
$this->postJson(self::BASE_ROUTE, $this->validPayload([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]))->assertCreated();
}
public function test_create_allows_a_new_open_affiliation_after_the_previous_one_was_soft_deleted(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$institution = Institution::factory()->create();
$user = User::factory()->create();
$trashed = UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]);
$trashed->delete();
$this->postJson(self::BASE_ROUTE, $this->validPayload([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]))->assertCreated();
}
public function test_create_allows_two_open_affiliations_for_the_same_user_at_different_institutions(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$user = User::factory()->create();
UserAffiliation::factory()->create(['user_id' => $user->id, 'end_year' => null]);
$this->postJson(self::BASE_ROUTE, $this->validPayload(['user_id' => $user->id, 'end_year' => null]))
->assertCreated();
}
public function test_operational_user_can_update_an_affiliation(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$affiliation = UserAffiliation::factory()->create(['start_year' => 2018, 'end_year' => null]);
$payload = $this->validPayload([
'institution_id' => $affiliation->institution_id,
'user_id' => $affiliation->user_id,
'end_year' => 2022,
]);
$this->putJson(self::BASE_ROUTE."/{$affiliation->id}", $payload)
->assertOk()
->assertJsonPath('data.end_year', 2022);
$this->assertSame(2022, $affiliation->fresh()->end_year);
}
public function test_update_allows_keeping_the_affiliation_open(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$affiliation = UserAffiliation::factory()->create(['end_year' => null]);
$payload = $this->validPayload([
'institution_id' => $affiliation->institution_id,
'user_id' => $affiliation->user_id,
'end_year' => null,
]);
$this->putJson(self::BASE_ROUTE."/{$affiliation->id}", $payload)->assertOk();
}
public function test_update_rejects_opening_a_second_affiliation_for_the_same_institution_and_user(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$institution = Institution::factory()->create();
$user = User::factory()->create();
UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]);
$closed = UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => 2019,
]);
$payload = $this->validPayload([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]);
$this->putJson(self::BASE_ROUTE."/{$closed->id}", $payload)
->assertStatus(422)
->assertJsonValidationErrors(['institution_id']);
}
public function test_operational_user_can_soft_delete_an_affiliation(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$affiliation = UserAffiliation::factory()->create();
$this->deleteJson(self::BASE_ROUTE."/{$affiliation->id}")->assertOk();
$this->assertSoftDeleted($affiliation);
}
public function test_operational_user_can_restore_a_trashed_affiliation(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$affiliation = UserAffiliation::factory()->create();
$affiliation->delete();
$this->postJson(self::BASE_ROUTE."/{$affiliation->id}/restore")
->assertOk()
->assertJsonPath('data.id', $affiliation->id);
$this->assertNotSoftDeleted($affiliation);
}
public function test_restore_is_blocked_when_another_open_affiliation_exists_for_the_same_institution_and_user(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$institution = Institution::factory()->create();
$user = User::factory()->create();
$trashed = UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]);
$trashed->delete();
UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]);
$this->postJson(self::BASE_ROUTE."/{$trashed->id}/restore")
->assertStatus(409)
->assertJsonPath('in_use', true);
$this->assertSoftDeleted($trashed);
}
public function test_restore_succeeds_when_the_conflicting_affiliation_was_closed(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$institution = Institution::factory()->create();
$user = User::factory()->create();
$trashed = UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => null,
]);
$trashed->delete();
UserAffiliation::factory()->create([
'institution_id' => $institution->id,
'user_id' => $user->id,
'end_year' => 2019,
]);
$this->postJson(self::BASE_ROUTE."/{$trashed->id}/restore")->assertOk();
$this->assertNotSoftDeleted($trashed);
}
public function test_operational_user_can_force_delete_an_affiliation(): void
{
$this->actingAs($this->operationalUser(), 'sanctum');
$affiliation = UserAffiliation::factory()->create();
$affiliation->delete();
$this->deleteJson(self::BASE_ROUTE."/{$affiliation->id}/force")->assertOk();
$this->assertDatabaseMissing('user_affiliations', ['id' => $affiliation->id]);
}
// --- Autorizzazione ---------------------------------------------------------
public function test_guests_can_read_affiliations(): void
{
UserAffiliation::factory()->create();
$this->getJson(self::BASE_ROUTE)->assertOk();
}
public function test_guests_cannot_write_affiliations(): void
{
$affiliation = UserAffiliation::factory()->create();
$this->postJson(self::BASE_ROUTE, [])->assertUnauthorized();
$this->deleteJson(self::BASE_ROUTE."/{$affiliation->id}")->assertUnauthorized();
}
}