Elenco con solo numero e zona (territori attivi e quelli ancora fuori), mostrato solo per la riconsegna; il server accetta solo un numero dell'elenco. Privacy, README e HANDOFF aggiornati. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
429 lines
19 KiB
PHP
429 lines
19 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Console\Commands\PuliziaRichieste;
|
|
use App\Livewire\Assegnazioni\Assegna;
|
|
use App\Livewire\Assegnazioni\Rientra;
|
|
use App\Livewire\Home;
|
|
use App\Livewire\Richieste\ModuloRichiesta;
|
|
use App\Mail\ModuloS13Mail;
|
|
use App\Models\AnnoTeocratico;
|
|
use App\Models\Assegnazione;
|
|
use App\Models\Proclamatore;
|
|
use App\Models\Richiesta;
|
|
use App\Models\Setting;
|
|
use App\Models\Territorio;
|
|
use App\Models\User;
|
|
use App\Models\Zona;
|
|
use App\Notifications\RichiestaRicevuta;
|
|
use App\Services\RichiesteServizio;
|
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
|
use Illuminate\Console\Scheduling\Schedule;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Livewire\Livewire;
|
|
use Spatie\Activitylog\Models\Activity;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Dal documento delle attività per la versione 2 (21 ottobre 2026):
|
|
* - il modulo pubblico di richiesta (nuovo territorio o riconsegna), che
|
|
* amministratore e assistenti confermano dalla Home portandolo sui moduli
|
|
* di assegnazione e di rientro già esistenti;
|
|
* - il segno "cartaceo" sull'assegnazione e l'elenco dei cartacei fuori;
|
|
* - il modulo S-13 per email agli amministratori l'ultima domenica del mese.
|
|
*/
|
|
class RichiesteTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $admin;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutVite();
|
|
$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->travelTo(Carbon::parse('2026-10-21 10:00'));
|
|
|
|
$this->admin = $this->utente('Admin', 'admin@example.test', 'amministratore');
|
|
RateLimiter::clear('richiesta-pubblica:' . sha1('127.0.0.1'));
|
|
}
|
|
|
|
private function utente(string $nome, string $email, string $ruolo): User
|
|
{
|
|
$utente = User::create(['name' => $nome, 'email' => $email, 'password' => 'password-di-prova']);
|
|
$utente->assignRole($ruolo);
|
|
|
|
return $utente;
|
|
}
|
|
|
|
private function proclamatore(string $cognome, string $nome): Proclamatore
|
|
{
|
|
return Proclamatore::create(['nome' => $nome, 'cognome' => $cognome, 'attivo' => true]);
|
|
}
|
|
|
|
private function territorio(string $numero): Territorio
|
|
{
|
|
return Territorio::create(['numero' => $numero, 'attivo' => true]);
|
|
}
|
|
|
|
private function assegna(Territorio $t, Proclamatore $p, string $dal, bool $cartaceo = false): Assegnazione
|
|
{
|
|
return Assegnazione::create([
|
|
'territorio_id' => $t->id,
|
|
'proclamatore_id' => $p->id,
|
|
'anno_teocratico_id' => AnnoTeocratico::perData(Carbon::parse($dal))->id,
|
|
'assigned_at' => $dal,
|
|
'created_by' => $this->admin->id,
|
|
'cartaceo' => $cartaceo,
|
|
]);
|
|
}
|
|
|
|
/** Il modulo compilato da una persona: aperto, qualche secondo, inviato. */
|
|
private function compila(array $campi): \Livewire\Features\SupportTesting\Testable
|
|
{
|
|
$modulo = Livewire::test(ModuloRichiesta::class);
|
|
foreach ($campi as $campo => $valore) {
|
|
$modulo->set($campo, $valore);
|
|
}
|
|
$this->travel(ModuloRichiesta::SECONDI_MINIMI + 2)->seconds();
|
|
|
|
return $modulo->call('invia');
|
|
}
|
|
|
|
private function postaFunzionante(): void
|
|
{
|
|
Setting::instance()->update(['mail_host' => 'smtp.esempio.it']);
|
|
Setting::flushCache();
|
|
}
|
|
|
|
// ─── Il modulo pubblico ─────────────────────────────────────
|
|
|
|
public function test_il_modulo_si_apre_senza_login_e_non_mostra_dati(): void
|
|
{
|
|
$this->proclamatore('Rossi', 'Mario');
|
|
auth()->logout();
|
|
|
|
$this->get(route('richiesta'))
|
|
->assertOk()
|
|
->assertSee('Invia la richiesta')
|
|
->assertDontSee('Rossi');
|
|
}
|
|
|
|
public function test_una_richiesta_si_salva_cifrata_e_avvisa_chi_la_gestisce(): void
|
|
{
|
|
Notification::fake();
|
|
$this->postaFunzionante();
|
|
$assistente = $this->utente('Assistente', 'assistente@example.test', 'assistente');
|
|
$operatore = $this->utente('Operatore', 'operatore@example.test', 'operatore');
|
|
$nonConfermato = $this->utente('Altro', 'altro@example.test', 'amministratore');
|
|
$nonConfermato->forceFill(['email_verified_at' => null])->save();
|
|
auth()->logout();
|
|
|
|
$this->compila(['tipo' => Richiesta::NUOVO, 'nome' => 'Mario Rossi', 'note' => 'Vicino a casa'])
|
|
->assertHasNoErrors()
|
|
->assertSet('inviata', true);
|
|
|
|
$r = Richiesta::sole();
|
|
$this->assertSame('Mario Rossi', $r->nome);
|
|
$this->assertSame(Richiesta::APERTA, $r->stato);
|
|
$this->assertNull($r->numero_territorio);
|
|
$grezzo = DB::table('richieste')->value('nome');
|
|
$this->assertStringNotContainsString('Rossi', $grezzo, 'Il nome nel database è cifrato');
|
|
$this->assertStringNotContainsString('Rossi', Activity::all()->toJson(), 'Il nome non va nel registro');
|
|
|
|
Notification::assertSentTo([$this->admin, $assistente], RichiestaRicevuta::class);
|
|
Notification::assertNotSentTo([$operatore, $nonConfermato], RichiestaRicevuta::class);
|
|
}
|
|
|
|
public function test_senza_posta_la_richiesta_resta_comunque(): void
|
|
{
|
|
Notification::fake();
|
|
$this->territorio('12');
|
|
auth()->logout();
|
|
|
|
$this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Mario Rossi', 'numero' => '12'])->assertHasNoErrors();
|
|
|
|
$this->assertSame('12', Richiesta::sole()->numero_territorio);
|
|
Notification::assertNothingSent();
|
|
}
|
|
|
|
public function test_la_riconsegna_vuole_il_numero_del_territorio(): void
|
|
{
|
|
$this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Mario Rossi'])->assertHasErrors('numero');
|
|
$this->compila(['tipo' => Richiesta::NUOVO, 'nome' => 'Ma'])->assertHasErrors('nome');
|
|
|
|
$this->assertSame(0, Richiesta::count());
|
|
}
|
|
|
|
public function test_nella_riconsegna_il_territorio_si_sceglie_dall_elenco(): void
|
|
{
|
|
$zona = Zona::create(['nome' => 'Centro storico']);
|
|
Territorio::create(['numero' => '12', 'zona_id' => $zona->id, 'attivo' => true]);
|
|
Territorio::create(['numero' => '3B', 'attivo' => true]);
|
|
$fuori = Territorio::create(['numero' => '40', 'attivo' => true]);
|
|
$this->assegna($fuori, $this->proclamatore('Verdi', 'Anna'), '2026-10-01');
|
|
$fuori->update(['attivo' => false]);
|
|
Territorio::create(['numero' => '77', 'attivo' => false]);
|
|
auth()->logout();
|
|
|
|
// L'elenco c'è solo nella riconsegna: numero e zona, nient'altro.
|
|
$modulo = Livewire::test(ModuloRichiesta::class)
|
|
->assertViewHas('opzioniTerritori', [])
|
|
->set('tipo', Richiesta::RICONSEGNA)
|
|
->assertSee('Centro storico')
|
|
->assertDontSee('Verdi')
|
|
->assertDontSee('assegnat');
|
|
$this->assertSame(['3B', '12', '40'], array_column($modulo->viewData('opzioniTerritori'), 'valore'),
|
|
'In ordine di numero; c\'è anche quello disattivato ma ancora fuori, non quello disattivato e rientrato');
|
|
|
|
$this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Anna Verdi', 'numero' => '99'])->assertHasErrors('numero');
|
|
$this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Anna Verdi', 'numero' => '77'])->assertHasErrors('numero');
|
|
$this->assertSame(0, Richiesta::count());
|
|
|
|
$this->compila(['tipo' => Richiesta::RICONSEGNA, 'nome' => 'Anna Verdi', 'numero' => '3b'])->assertHasNoErrors();
|
|
$this->assertSame('3b', Richiesta::sole()->numero_territorio);
|
|
}
|
|
|
|
public function test_lo_spam_non_si_salva_e_non_se_ne_accorge(): void
|
|
{
|
|
// Il campo trappola riempito.
|
|
$this->compila(['nome' => 'Spam Bot', 'sito' => 'https://spam.example'])->assertSet('inviata', true);
|
|
|
|
// Inviato subito dopo l'apertura.
|
|
Livewire::test(ModuloRichiesta::class)->set('nome', 'Spam Bot')->call('invia')->assertSet('inviata', true);
|
|
|
|
$this->assertSame(0, Richiesta::count());
|
|
}
|
|
|
|
public function test_al_massimo_cinque_richieste_all_ora_dallo_stesso_indirizzo(): void
|
|
{
|
|
for ($i = 1; $i <= ModuloRichiesta::MASSIMO_ORA; $i++) {
|
|
$this->compila(['nome' => "Mario Rossi {$i}"])->assertHasNoErrors();
|
|
}
|
|
$this->compila(['nome' => 'Mario Rossi 6'])->assertHasErrors('nome');
|
|
|
|
$this->assertSame(ModuloRichiesta::MASSIMO_ORA, Richiesta::count());
|
|
}
|
|
|
|
// ─── I nomi simili ──────────────────────────────────────────
|
|
|
|
public function test_i_nomi_simili_trovano_il_proclamatore_anche_scritto_male(): void
|
|
{
|
|
$rossi = $this->proclamatore('Rossi', 'Mario');
|
|
$rossini = $this->proclamatore('Rossini', 'Maria');
|
|
$this->proclamatore('Bianchi', 'Luca');
|
|
$servizio = app(RichiesteServizio::class);
|
|
|
|
$this->assertSame([$rossi->id, $rossini->id], $servizio->nomiSimili('mario rossi')->pluck('id')->all());
|
|
$this->assertSame($rossi->id, $servizio->nomiSimili('Rosi Mario')->first()?->id, 'Una lettera in meno');
|
|
$this->assertSame($rossi->id, $servizio->nomiSimili('Màrio Rossi')->first()?->id, 'Accenti');
|
|
$this->assertTrue($servizio->nomiSimili('Giuseppe Verdi')->isEmpty());
|
|
}
|
|
|
|
// ─── La Home ────────────────────────────────────────────────
|
|
|
|
public function test_la_home_mostra_le_richieste_con_i_nomi_simili_e_i_collegamenti(): void
|
|
{
|
|
$rossi = $this->proclamatore('Rossi', 'Mario');
|
|
$nuova = app(RichiesteServizio::class)->registra(Richiesta::NUOVO, 'Mario Rosi', null, 'Anche cartaceo');
|
|
$t = $this->territorio('12');
|
|
$a = $this->assegna($t, $this->proclamatore('Verdi', 'Anna'), '2026-10-01');
|
|
$riconsegna = app(RichiesteServizio::class)->registra(Richiesta::RICONSEGNA, 'Anna Verdi', '12', null);
|
|
app(RichiesteServizio::class)->registra(Richiesta::RICONSEGNA, 'Luca Neri', '99', null);
|
|
|
|
Livewire::actingAs($this->admin)->test(Home::class)
|
|
->assertSee('Richieste (3)')
|
|
->assertSee('Mario Rosi')
|
|
->assertSee('Anche cartaceo')
|
|
->assertSee(route('assegnazioni.assegna', ['proclamatore' => $rossi->id, 'richiesta' => $nuova->id, 'dalla_home' => 1]))
|
|
->assertSee('Nessuno di questi: apri il modulo')
|
|
->assertSee(route('assegnazioni.rientra', ['assegnazione' => $a->id, 'richiesta' => $riconsegna->id]))
|
|
->assertSee('Il territorio N° 99 non risulta assegnato.');
|
|
}
|
|
|
|
public function test_solo_chi_ha_il_permesso_vede_e_scarta_le_richieste(): void
|
|
{
|
|
$r = app(RichiesteServizio::class)->registra(Richiesta::NUOVO, 'Mario Rossi', null, null);
|
|
$operatore = $this->utente('Operatore', 'operatore@example.test', 'operatore');
|
|
|
|
Livewire::actingAs($operatore)->test(Home::class)
|
|
->assertDontSee('Mario Rossi')
|
|
->call('scartaRichiesta', $r->id)
|
|
->assertForbidden();
|
|
$this->assertTrue($r->fresh()->isAperta());
|
|
|
|
$assistente = $this->utente('Assistente', 'assistente@example.test', 'assistente');
|
|
Livewire::actingAs($assistente)->test(Home::class)
|
|
->assertSee('Mario Rossi')
|
|
->call('scartaRichiesta', $r->id)
|
|
->assertDontSee('Mario Rossi');
|
|
|
|
$r->refresh();
|
|
$this->assertSame(Richiesta::SCARTATA, $r->stato);
|
|
$this->assertSame($assistente->id, $r->gestita_da);
|
|
}
|
|
|
|
// ─── Dalla richiesta ai moduli esistenti ────────────────────
|
|
|
|
public function test_dalla_richiesta_il_modulo_di_assegnazione_e_precompilato_e_la_chiude(): void
|
|
{
|
|
$rossi = $this->proclamatore('Rossi', 'Mario');
|
|
$t = $this->territorio('7');
|
|
$r = app(RichiesteServizio::class)->registra(Richiesta::NUOVO, 'Mario Rossi', null, null);
|
|
|
|
Livewire::withQueryParams(['proclamatore' => $rossi->id, 'richiesta' => $r->id, 'dalla_home' => 1])
|
|
->actingAs($this->admin)
|
|
->test(Assegna::class)
|
|
->assertSet('proclamatore_id', $rossi->id)
|
|
->assertSet('richiestaId', $r->id)
|
|
->assertSee('Mario Rossi')
|
|
->set('territorio_id', $t->id)
|
|
->set('cartaceo', true)
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$a = Assegnazione::sole();
|
|
$this->assertTrue($a->cartaceo);
|
|
$r->refresh();
|
|
$this->assertSame(Richiesta::EVASA, $r->stato);
|
|
$this->assertSame($a->id, $r->assegnazione_id);
|
|
$this->assertSame($this->admin->id, $r->gestita_da);
|
|
}
|
|
|
|
public function test_senza_permesso_o_con_una_richiesta_chiusa_il_modulo_la_ignora(): void
|
|
{
|
|
$r = app(RichiesteServizio::class)->registra(Richiesta::NUOVO, 'Mario Rossi', null, null);
|
|
$operatore = $this->utente('Operatore', 'operatore@example.test', 'operatore');
|
|
|
|
Livewire::withQueryParams(['richiesta' => $r->id])->actingAs($operatore)
|
|
->test(Assegna::class)->assertSet('richiestaId', null);
|
|
|
|
$r->scarta($this->admin);
|
|
Livewire::withQueryParams(['richiesta' => $r->id])->actingAs($this->admin)
|
|
->test(Assegna::class)->assertSet('richiestaId', null);
|
|
}
|
|
|
|
public function test_dalla_riconsegna_il_rientro_la_chiude(): void
|
|
{
|
|
$t = $this->territorio('12');
|
|
$a = $this->assegna($t, $this->proclamatore('Verdi', 'Anna'), '2026-10-01');
|
|
$r = app(RichiesteServizio::class)->registra(Richiesta::RICONSEGNA, 'Anna Verdi', '12', null);
|
|
|
|
Livewire::withQueryParams(['richiesta' => $r->id])->actingAs($this->admin)
|
|
->test(Rientra::class, ['assegnazione' => $a])
|
|
->assertSee('Riconsegna annunciata da')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$this->assertNotNull($a->fresh()->returned_at);
|
|
$r->refresh();
|
|
$this->assertSame(Richiesta::EVASA, $r->stato);
|
|
$this->assertSame($a->id, $r->assegnazione_id);
|
|
}
|
|
|
|
public function test_le_richieste_gestite_si_cancellano_dopo_novanta_giorni(): void
|
|
{
|
|
$servizio = app(RichiesteServizio::class);
|
|
$vecchia = $servizio->registra(Richiesta::NUOVO, 'Vecchia Gestita', null, null);
|
|
$vecchia->scarta($this->admin);
|
|
$aperta = $servizio->registra(Richiesta::NUOVO, 'Vecchia Aperta', null, null);
|
|
|
|
$this->travel(PuliziaRichieste::GIORNI - 1)->days();
|
|
$recente = $servizio->registra(Richiesta::NUOVO, 'Recente', null, null);
|
|
$recente->scarta($this->admin);
|
|
|
|
$this->travel(2)->days();
|
|
$this->artisan('richieste:pulizia')->assertSuccessful();
|
|
|
|
$this->assertModelMissing($vecchia);
|
|
$this->assertModelExists($aperta);
|
|
$this->assertModelExists($recente);
|
|
}
|
|
|
|
// ─── Cartacei ───────────────────────────────────────────────
|
|
|
|
public function test_l_elenco_dei_cartacei_mostra_quelli_fuori_e_a_chi(): void
|
|
{
|
|
$rossi = $this->proclamatore('Rossi', 'Mario');
|
|
$this->assegna($this->territorio('21'), $rossi, '2026-10-01', cartaceo: true);
|
|
$this->assegna($this->territorio('22'), $rossi, '2026-10-01');
|
|
$rientrato = $this->assegna($this->territorio('23'), $rossi, '2026-09-01', cartaceo: true);
|
|
$rientrato->update(['returned_at' => '2026-10-01']);
|
|
|
|
$this->actingAs($this->admin)->get(route('assegnazioni.cartacei'))
|
|
->assertOk()
|
|
->assertSee('N° 21')
|
|
->assertSee('Rossi Mario')
|
|
->assertDontSee('N° 22')
|
|
->assertDontSee('N° 23');
|
|
|
|
// Nell'elenco degli assegnati, il segno solo dove serve.
|
|
$html = $this->actingAs($this->admin)->get(route('assegnazioni.assegna'))->getContent();
|
|
$this->assertStringContainsString('assegnazioni.elenco-assegnati', $html);
|
|
Livewire::withoutLazyLoading()->actingAs($this->admin)->test(\App\Livewire\Assegnazioni\ElencoAssegnati::class)
|
|
->assertSeeHtml('<span class="tm-cartaceo">cartaceo</span>');
|
|
|
|
// Cambio di utente nello stesso test: la sessione dell'admin va
|
|
// svuotata, o la protezione della sessione lo manda al login (302).
|
|
$this->flushSession();
|
|
$sorvegliante = $this->utente('Sorvegliante', 'sorvegliante@example.test', 'sorvegliante');
|
|
$this->actingAs($sorvegliante)->get(route('assegnazioni.cartacei'))->assertForbidden();
|
|
}
|
|
|
|
// ─── Modulo S-13 per email ──────────────────────────────────
|
|
|
|
public function test_l_s13_va_agli_amministratori_con_l_email_confermata(): void
|
|
{
|
|
Mail::fake();
|
|
$this->postaFunzionante();
|
|
$assistente = $this->utente('Assistente', 'assistente@example.test', 'assistente');
|
|
$nonConfermato = $this->utente('Altro', 'altro@example.test', 'amministratore');
|
|
$nonConfermato->forceFill(['email_verified_at' => null])->save();
|
|
|
|
$this->artisan('registro:invia-s13')->assertSuccessful();
|
|
|
|
Mail::assertSent(ModuloS13Mail::class, fn (ModuloS13Mail $m) => $m->hasTo('admin@example.test')
|
|
&& collect($m->attachments())->contains(fn ($a) => $a->as === 'S-13-registro-assegnazioni-2026-2027.pdf' && $a->mime === 'application/pdf'));
|
|
Mail::assertSent(ModuloS13Mail::class, 1);
|
|
Mail::assertNotSent(ModuloS13Mail::class, fn (ModuloS13Mail $m) => $m->hasTo($assistente->email) || $m->hasTo($nonConfermato->email));
|
|
$this->assertSame(1, Activity::where('description', 's13_inviato')->sole()->properties['destinatari']);
|
|
}
|
|
|
|
public function test_senza_posta_l_s13_non_parte(): void
|
|
{
|
|
Mail::fake();
|
|
|
|
$this->artisan('registro:invia-s13')->assertSuccessful();
|
|
|
|
Mail::assertNothingSent();
|
|
}
|
|
|
|
public function test_l_s13_parte_solo_l_ultima_domenica_del_mese(): void
|
|
{
|
|
$evento = collect(app(Schedule::class)->events())
|
|
->first(fn ($e) => str_contains((string) $e->command, 'registro:invia-s13'));
|
|
$this->assertNotNull($evento);
|
|
|
|
$parte = function (string $quando) use ($evento) {
|
|
$this->travelTo(Carbon::parse($quando, config('app.timezone')));
|
|
|
|
return $evento->isDue($this->app) && $evento->filtersPass($this->app);
|
|
};
|
|
|
|
$this->assertTrue($parte('2026-10-25 08:00'), 'Ultima domenica di ottobre');
|
|
$this->assertFalse($parte('2026-10-18 08:00'), 'Penultima domenica');
|
|
$this->assertFalse($parte('2026-10-24 08:00'), 'Sabato');
|
|
$this->assertTrue($parte('2026-11-29 08:00'), 'Ultima domenica di novembre');
|
|
$this->assertTrue($parte('2027-01-31 08:00'), 'Domenica 31');
|
|
}
|
|
}
|