'application/json']; /** * @param array $overrides * @return array */ 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_restore_succeeds_for_a_closed_affiliation_even_when_another_one_is_open(): 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' => 2019, ]); $trashed->delete(); UserAffiliation::factory()->create([ 'institution_id' => $institution->id, 'user_id' => $user->id, 'end_year' => null, ]); $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(); } }