withoutVite(); Storage::fake('public'); $this->seed(RolesAndPermissionsSeeder::class); Setting::firstOrCreate([], ['congregazione_nome' => 'Congregazione di prova']); AnnoTeocratico::create(['label' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-08-31']); $this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password-di-prova']); $this->admin->assignRole('amministratore'); $this->actingAs($this->admin); } private function proclamatore(string $cognome, string $nome = 'Test'): Proclamatore { return Proclamatore::create(['nome' => $nome, 'cognome' => $cognome, 'attivo' => true]); } private function territorio(string $numero, bool $pdf = true): Territorio { if ($pdf) { Storage::disk('public')->put("territori-pdf/t{$numero}.pdf", '%PDF-1.4'); } return Territorio::create(['numero' => $numero, 'attivo' => true, 'pdf_path' => $pdf ? "territori-pdf/t{$numero}.pdf" : null]); } /** Gruppo "Zisa": sorvegliante Rossi, membri Verdi e Bianchi, pacchetto 12, 14, 35. */ private function gruppo(): array { $rossi = $this->proclamatore('Rossi', 'Mario'); $verdi = $this->proclamatore('Verdi', 'Anna'); $bianchi = $this->proclamatore('Bianchi', 'Luca'); [$gruppo, $password] = app(GruppiServizio::class)->crea('Zisa', $rossi, 'mario@example.test', [$verdi->id, $bianchi->id]); $territori = collect(['12', '14', '35'])->map(fn ($n) => $this->territorio($n)); app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, $territori->pluck('id')->all()); $territori = $territori->map->fresh(); $sorvegliante = User::where('email', 'mario@example.test')->sole(); // Il cambio password al primo accesso ha i suoi test: qui si lavora. // E l'email non confermata, come nella realtà (TestCase le conferma). $sorvegliante->forceFill(['password_change_required' => false, 'email_verified_at' => null])->save(); return compact('gruppo', 'password', 'rossi', 'verdi', 'bianchi', 'territori', 'sorvegliante'); } // ─── Creazione ────────────────────────────────────────────── public function test_creare_un_gruppo_da_un_utenza_limitata_al_sorvegliante(): void { $rossi = $this->proclamatore('Rossi', 'Mario'); $pagina = Livewire::test(GruppiIndex::class) ->set('nome', 'Zisa') ->set('sorveglianteId', $rossi->id) ->set('email', 'mario@example.test') ->call('crea') ->assertHasNoErrors(); $gruppo = GruppoServizio::sole(); $utente = User::where('email', 'mario@example.test')->sole(); $this->assertSame($rossi->id, $gruppo->sorvegliante_id); $this->assertSame($gruppo->id, $rossi->fresh()->gruppo_servizio_id, 'Il sorvegliante è anche membro'); $this->assertSame($rossi->id, $utente->proclamatore_id); $this->assertTrue($utente->hasRole('sorvegliante')); $this->assertTrue($utente->password_change_required); // Nessuna conferma dell'email per il sorvegliante (TestCase conferma // gli utenti creati: qui si toglie). $utente->forceFill(['email_verified_at' => null])->save(); $this->assertTrue($utente->fresh()->hasVerifiedEmail()); // La password temporanea si vede una volta, ed è quella vera. $password = $pagina->get('passwordTemporanea'); $this->assertNotEmpty($password); $pagina->assertSee($password); $this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $utente->password)); } public function test_un_proclamatore_sta_in_un_solo_gruppo(): void { ['gruppo' => $zisa, 'verdi' => $verdi] = $this->gruppo(); [$noce] = app(GruppiServizio::class)->crea('Noce', $this->proclamatore('Neri'), 'neri@example.test'); $this->expectException(InvalidArgumentException::class); app(GruppiServizio::class)->aggiungiMembri($noce, [$verdi->id]); } public function test_il_ruolo_sorvegliante_non_si_da_dalla_pagina_utenti(): void { ['sorvegliante' => $sorvegliante] = $this->gruppo(); $utenti = Livewire::test(UsersIndex::class); $this->assertNotContains('sorvegliante', $utenti->get('availableRoles')); $utenti->call('startEdit', $sorvegliante->id)->assertSet('editingUserId', null); $utenti->call('deleteUser', $sorvegliante->id); $this->assertNotNull($sorvegliante->fresh()); } // ─── Pacchetto ────────────────────────────────────────────── public function test_il_pacchetto_non_e_un_assegnazione_e_non_e_in_reparto(): void { ['territori' => $territori] = $this->gruppo(); $libero = $this->territorio('99'); $this->assertSame(0, Assegnazione::count(), 'Il pacchetto non crea assegnazioni: medie e statistiche non cambiano'); $this->assertSame([$libero->id], Territorio::inReparto()->pluck('id')->all()); $this->assertSame('in_gruppo', $territori[0]->fresh()->stato); $this->assertSame('In gruppo Zisa', $territori[0]->fresh()->stato_etichetta); } public function test_dalla_pagina_assegna_un_territorio_del_pacchetto_non_si_assegna(): void { ['territori' => $territori] = $this->gruppo(); Livewire::test(Assegna::class) ->set('territorio_id', $territori[0]->id) ->set('proclamatore_id', $this->proclamatore('Neri')->id) ->call('save') ->assertHasErrors('territorio_id'); $this->assertSame(0, Assegnazione::count()); } public function test_un_territorio_assegnato_non_entra_nel_pacchetto(): void { ['gruppo' => $gruppo] = $this->gruppo(); $t = $this->territorio('50'); Assegnazione::create([ 'territorio_id' => $t->id, 'proclamatore_id' => $this->proclamatore('Neri')->id, 'anno_teocratico_id' => AnnoTeocratico::sole()->id, 'assigned_at' => now(), 'created_by' => $this->admin->id, ]); $this->expectException(InvalidArgumentException::class); app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, [$t->id]); } // ─── Accesso del sorvegliante ─────────────────────────────── public function test_il_sorvegliante_vede_solo_il_suo_gruppo_e_il_profilo(): void { ['sorvegliante' => $sorvegliante] = $this->gruppo(); $this->flushSession(); $this->actingAs($sorvegliante); $this->get(route('dashboard'))->assertRedirect(route('mio-gruppo')); $this->get(route('mio-gruppo'))->assertOk()->assertSee('Gruppo Zisa')->assertSee('N° 12'); $this->get(route('profile'))->assertOk(); foreach (['territori.index', 'proclamatori.index', 'registro.index', 'campagne.index', 'assegnazioni.assegna', 'users.index', 'gruppi.index', 'privacy'] as $rotta) { $this->get(route($rotta))->assertForbidden(); } } public function test_l_amministratore_non_ha_la_voce_il_mio_gruppo(): void { $this->gruppo(); // Passa ogni @can (Gate::before), ma un gruppo suo non ce l'ha. $this->get(route('dashboard'))->assertOk()->assertDontSee('Il mio gruppo')->assertSee('Gruppi di servizio'); } public function test_il_sorvegliante_entra_senza_conferma_dell_email_e_va_al_suo_gruppo(): void { $this->gruppo(); $this->flushSession(); auth()->logout(); $password = 'una-password-nuova-1'; User::where('email', 'mario@example.test')->sole()->forceFill(['password' => $password])->save(); $this->post(route('login.store'), ['email' => 'mario@example.test', 'password' => $password]) ->assertRedirect(route('mio-gruppo')); } // ─── Il sorvegliante al lavoro ────────────────────────────── public function test_il_sorvegliante_assegna_a_un_membro_e_nasce_un_assegnazione_normale(): void { ['sorvegliante' => $s, 'verdi' => $verdi, 'territori' => $territori, 'gruppo' => $gruppo] = $this->gruppo(); $this->flushSession(); Livewire::actingAs($s)->test(MioGruppo::class) ->call('apriAssegnazione', $territori[0]->id) ->set('membroId', $verdi->id) ->call('assegna') ->assertHasNoErrors() // Come dalla pagina Assegna: si apre l'invio del link. ->assertSee('Hai inviato il link?'); $a = Assegnazione::sole(); $this->assertSame($verdi->id, $a->proclamatore_id); $this->assertSame($gruppo->id, $a->gruppo_servizio_id, 'Tracciato che viene dal gruppo'); $this->assertTrue($a->link_da_inviare); $this->assertSame($gruppo->id, $territori[0]->fresh()->gruppo_servizio_id, 'Resta nel pacchetto finché non rientra'); $this->assertSame('assegnato', $territori[0]->fresh()->stato); } public function test_il_sorvegliante_puo_tenere_un_territorio_per_se(): void { ['sorvegliante' => $s, 'rossi' => $rossi, 'territori' => $territori] = $this->gruppo(); Livewire::actingAs($s)->test(MioGruppo::class) ->call('apriAssegnazione', $territori[1]->id) ->set('membroId', $rossi->id) ->call('assegna') ->assertHasNoErrors(); $this->assertSame($rossi->id, Assegnazione::sole()->proclamatore_id); } public function test_solo_ai_membri_e_solo_i_territori_del_pacchetto(): void { ['sorvegliante' => $s, 'territori' => $territori] = $this->gruppo(); $estraneo = $this->proclamatore('Neri'); $fuori = $this->territorio('77'); Livewire::actingAs($s)->test(MioGruppo::class) ->call('apriAssegnazione', $territori[0]->id) ->set('membroId', $estraneo->id) ->call('assegna') ->assertHasErrors('membroId'); Livewire::actingAs($s)->test(MioGruppo::class) ->call('apriAssegnazione', $fuori->id) ->assertForbidden(); $this->assertSame(0, Assegnazione::count()); } public function test_il_rientro_lo_registra_il_sorvegliante_e_il_territorio_torna_in_reparto(): void { ['sorvegliante' => $s, 'verdi' => $verdi, 'territori' => $territori, 'gruppo' => $gruppo] = $this->gruppo(); $a = app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now()->subDays(3)); $this->flushSession(); $this->actingAs($s)->get(route('assegnazioni.rientra', $a))->assertOk(); Livewire::actingAs($s)->test(Rientra::class, ['assegnazione' => $a]) ->call('save') ->assertRedirect(route('mio-gruppo')); $this->assertNotNull($a->fresh()->returned_at); $this->assertNull($territori[0]->fresh()->gruppo_servizio_id, 'Esce dal pacchetto'); $this->assertSame('in_reparto', $territori[0]->fresh()->stato); } public function test_il_sorvegliante_non_registra_rientri_di_altri(): void { ['sorvegliante' => $s] = $this->gruppo(); $altro = Assegnazione::create([ 'territorio_id' => $this->territorio('80')->id, 'proclamatore_id' => $this->proclamatore('Neri')->id, 'anno_teocratico_id' => AnnoTeocratico::sole()->id, 'assigned_at' => now(), 'created_by' => $this->admin->id, ]); $this->flushSession(); $this->actingAs($s)->get(route('assegnazioni.rientra', $altro))->assertForbidden(); } public function test_il_sorvegliante_rimanda_in_reparto_quelli_non_dati(): void { ['sorvegliante' => $s, 'territori' => $territori] = $this->gruppo(); Livewire::actingAs($s)->test(MioGruppo::class)->call('rimanda', $territori[2]->id); $this->assertNull($territori[2]->fresh()->gruppo_servizio_id); $this->assertSame('in_reparto', $territori[2]->fresh()->stato); } public function test_la_piantina_del_pacchetto_si_apre_solo_per_il_proprio_gruppo(): void { ['sorvegliante' => $s, 'territori' => $territori] = $this->gruppo(); $fuori = $this->territorio('88'); $this->flushSession(); $this->actingAs($s)->get(route('mio-gruppo.pdf', $territori[0]))->assertOk(); $this->actingAs($s)->get(route('mio-gruppo.pdf', $fuori))->assertForbidden(); } // ─── Scioglimento ─────────────────────────────────────────── public function test_togliere_il_ruolo_scioglie_il_gruppo(): void { ['gruppo' => $gruppo, 'sorvegliante' => $s, 'verdi' => $verdi, 'territori' => $territori] = $this->gruppo(); $data = app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now()); Livewire::test(GruppoShow::class, ['gruppo' => $gruppo])->call('sciogli'); $gruppo->refresh(); $this->assertNotNull($gruppo->sciolto_at); // I non dati tornano in reparto; quello dato resta a Verdi, assegnazione normale. $this->assertNull($territori[1]->fresh()->gruppo_servizio_id); $this->assertNull($territori[2]->fresh()->gruppo_servizio_id); $this->assertNull($data->fresh()->returned_at); $this->assertSame($gruppo->id, $data->fresh()->gruppo_servizio_id, 'Lo storico continua a dirlo'); // Membri liberi, utenza disattivata e senza ruolo. $this->assertSame(0, Proclamatore::where('gruppo_servizio_id', $gruppo->id)->count()); $this->assertTrue($s->fresh()->isDisattivato()); $this->assertFalse($s->fresh()->hasRole('sorvegliante')); // Per ripartire serve un gruppo nuovo. $this->expectException(InvalidArgumentException::class); app(GruppiServizio::class)->aggiungiAlPacchetto($gruppo, [$territori[1]->id]); } public function test_un_utenza_disattivata_non_entra_e_viene_buttata_fuori(): void { ['gruppo' => $gruppo, 'sorvegliante' => $s] = $this->gruppo(); $password = 'una-password-nuova-1'; $s->forceFill(['password' => $password])->save(); app(GruppiServizio::class)->sciogli($gruppo); $this->flushSession(); auth()->logout(); $this->post(route('login.store'), ['email' => 'mario@example.test', 'password' => $password]) ->assertSessionHasErrors('email'); $this->assertGuest(); $this->actingAs($s->fresh())->get(route('profile'))->assertRedirect(route('login')); } public function test_l_ex_sorvegliante_puo_guidare_un_gruppo_nuovo_con_la_stessa_utenza(): void { ['gruppo' => $vecchio, 'rossi' => $rossi, 'sorvegliante' => $s] = $this->gruppo(); app(GruppiServizio::class)->sciogli($vecchio); $pagina = Livewire::test(GruppiIndex::class); $this->assertContains((string) $rossi->id, collect($pagina->viewData('opzioniSorveglianti'))->pluck('valore')->all(), 'Torna tra i candidati'); [$nuovo, $password] = app(GruppiServizio::class)->crea('Zisa 2', $rossi->fresh(), 'mario.nuovo@example.test'); $utente = $s->fresh(); $this->assertSame(1, User::where('proclamatore_id', $rossi->id)->count(), 'Un proclamatore, un\'utenza'); $this->assertFalse($utente->isDisattivato()); $this->assertTrue($utente->hasRole('sorvegliante')); $this->assertSame('mario.nuovo@example.test', $utente->email); $this->assertTrue($utente->password_change_required); $this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $utente->password)); $this->assertSame($nuovo->id, $utente->gruppoSorvegliato()?->id); } // ─── Proclamatori: cestino, anonimizzazione, registro ─────── public function test_il_sorvegliante_di_un_gruppo_attivo_non_si_cestina_ne_si_anonimizza(): void { ['rossi' => $rossi, 'verdi' => $verdi] = $this->gruppo(); Livewire::test(\App\Livewire\Proclamatori\ProclamatoreIndex::class) ->call('deleteProclamatore', $rossi->id) ->call('anonimizza', $rossi->id); $this->assertFalse($rossi->fresh()->trashed()); $this->assertSame('Rossi', $rossi->fresh()->cognome); // Un membro qualsiasi sì, e lascia il gruppo. Livewire::test(\App\Livewire\Proclamatori\ProclamatoreIndex::class)->call('deleteProclamatore', $verdi->id); $this->assertTrue($verdi->fresh()->trashed()); $this->assertNull($verdi->fresh()->gruppo_servizio_id); } public function test_anonimizzare_un_ex_sorvegliante_anonimizza_anche_la_sua_utenza(): void { ['gruppo' => $gruppo, 'rossi' => $rossi, 'sorvegliante' => $s] = $this->gruppo(); app(GruppiServizio::class)->sciogli($gruppo); Livewire::test(\App\Livewire\Proclamatori\ProclamatoreIndex::class)->call('anonimizza', $rossi->id); $utente = $s->fresh(); $this->assertSame('Anonimo', $utente->name); $this->assertStringEndsWith('@anonimo.invalid', $utente->email); $this->assertTrue($utente->isDisattivato()); $this->assertCount(0, $utente->roles); } public function test_nel_registro_delle_attivita_l_utenza_del_sorvegliante_non_ha_nome_ne_email(): void { ['sorvegliante' => $s] = $this->gruppo(); $s->update(['email' => 'mario.rossi@example.test']); $voci = \Spatie\Activitylog\Models\Activity::where('subject_type', User::class)->where('subject_id', $s->id)->get(); $this->assertNotEmpty($voci); foreach ($voci as $voce) { $json = json_encode($voce->properties); $this->assertStringNotContainsString('Rossi', $json); $this->assertStringNotContainsString('mario', $json); } } // ─── Backup e azzeramento ─────────────────────────────────── public function test_l_azzeramento_toglie_anche_gruppi_e_utenza_del_sorvegliante(): void { ['gruppo' => $gruppo, 'verdi' => $verdi, 'territori' => $territori, 'sorvegliante' => $s] = $this->gruppo(); app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now()); $stato = app(\App\Services\BackupOperationState::class); $stato->initialize('azzeramento-gruppi', 'azzeramento', 'In attesa'); (new \App\Jobs\ResetApplication('azzeramento-gruppi', $this->admin->id))->handle($stato, app(\App\Services\BackupArchive::class)); $this->assertSame('completed', $stato->get('azzeramento-gruppi')['status'] ?? null, json_encode($stato->get('azzeramento-gruppi'))); $this->assertSame(0, GruppoServizio::count()); $this->assertNull(User::find($s->id)); } public function test_l_xml_conserva_gruppi_membri_pacchetto_e_utenza(): void { ['gruppo' => $gruppo, 'verdi' => $verdi, 'territori' => $territori] = $this->gruppo(); app(GruppiServizio::class)->assegna($gruppo, $territori[0], $verdi, now()); $xml = (new XmlExchange())->currentXmlBackup(); $this->assertArrayNotHasKey('error', (new XmlExchange())->importXmlFromContent($xml)); $g = GruppoServizio::sole(); $this->assertSame('Zisa', $g->nome); $this->assertSame('Rossi Mario', $g->sorvegliante->nome_completo); $this->assertEqualsCanonicalizing(['Rossi Mario', 'Verdi Anna', 'Bianchi Luca'], $g->membri->map->nome_completo->all()); $this->assertEqualsCanonicalizing(['12', '14', '35'], $g->territori->pluck('numero')->all()); $this->assertSame($g->id, Assegnazione::sole()->gruppo_servizio_id); $this->assertSame($g->sorvegliante_id, User::where('email', 'mario@example.test')->sole()->proclamatore_id); } }