370 lines
13 KiB
PHP
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();
|
|
}
|
|
}
|