S-13 sul modulo ufficiale e correzioni all'integrità dei dati
Modulo S-13 - Il PDF scrive sopra il modulo ufficiale (FPDI, matrice convertita in PDF 1.4) invece di ricostruirlo in HTML: 20 righe per foglio come l'originale. - Una riga per ogni territorio attivo; riporto dell'ultimo rientro prima dell'anno; assegnazioni a cavallo d'anno su entrambi i fogli; scelta per data e non per anno registrato; nomi come "Cognome I.". - Logica spostata in App\Services\Registro\ModuloS13. Integrità dei dati - Export/import XML (e quindi backup/ripristino): le date non slittano più di un giorno; i file vecchi in UTC tornano al giorno giusto. - Import XML: rifiuta file estranei o incompleti senza toccare i dati, errore con riga e motivo, territori senza zona importati con il loro storico, caratteri di controllo rimossi in export, chi importa conserva password e ruolo di amministratore. - Assegnazioni: assegnazioneCorrente corretta (filtro dentro ofMany), periodi sovrapposti rifiutati in Assegna e Registro sotto lock, anno di servizio ricavato dalla data, niente date future nel Registro. - Utenti: non si declassa sé stessi né l'ultimo amministratore. - Entrypoint: non sovrascrive più la copia persistente di APP_KEY con un valore diverso; serve APP_KEY_ROTATE=1 per un cambio voluto. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
211
tests/Feature/AssegnazioneSovrapposizioneTest.php
Normal file
211
tests/Feature/AssegnazioneSovrapposizioneTest.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\Assegnazioni\Assegna;
|
||||
use App\Livewire\Registro;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Un territorio è fuori al massimo una volta: nessuna doppia assegnazione
|
||||
* aperta, nessuna uscita retrodatata a prima dell'ultimo rientro, nessun
|
||||
* periodo accavallato dalle correzioni nel Registro.
|
||||
*/
|
||||
class AssegnazioneSovrapposizioneTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
private Territorio $territorio;
|
||||
private Proclamatore $proclamatore;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Carbon::setTestNow('2026-10-09 10:00:00');
|
||||
|
||||
foreach (['settings.manage', 'territori.assign', 'territori.return', 'registro.view'] as $permesso) {
|
||||
Permission::create(['name' => $permesso, 'guard_name' => 'web']);
|
||||
}
|
||||
Role::create(['name' => 'amministratore', 'guard_name' => 'web'])->givePermissionTo(Permission::all());
|
||||
|
||||
$this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password']);
|
||||
$this->admin->assignRole('amministratore');
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
$this->territorio = Territorio::create(['numero' => '5', 'attivo' => true]);
|
||||
$this->proclamatore = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Carbon::setTestNow();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
// ─── L'assegnazione in corso ────────────────────────────────
|
||||
|
||||
/**
|
||||
* Il caso che rendeva il territorio "libero" mentre era fuori: una chiusa
|
||||
* con data più recente dell'aperta.
|
||||
*/
|
||||
public function test_l_assegnazione_in_corso_si_trova_anche_con_una_chiusa_piu_recente(): void
|
||||
{
|
||||
$aperta = $this->assegna('2026-08-20');
|
||||
$this->assegna('2026-09-01', '2026-09-15');
|
||||
|
||||
$this->assertTrue($this->territorio->fresh()->assegnazioneCorrente?->is($aperta));
|
||||
}
|
||||
|
||||
// ─── Il controllo di sovrapposizione ────────────────────────
|
||||
|
||||
public function test_rientro_e_nuova_uscita_nello_stesso_giorno_non_si_sovrappongono(): void
|
||||
{
|
||||
$this->assegna('2026-09-01', '2026-09-20');
|
||||
|
||||
$this->assertNull(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-09-20')));
|
||||
}
|
||||
|
||||
public function test_un_uscita_prima_dell_ultimo_rientro_si_sovrappone(): void
|
||||
{
|
||||
$chiusa = $this->assegna('2026-09-01', '2026-09-20');
|
||||
|
||||
$this->assertTrue(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-09-10'))?->is($chiusa));
|
||||
}
|
||||
|
||||
public function test_con_un_assegnazione_aperta_ogni_nuova_uscita_si_sovrappone(): void
|
||||
{
|
||||
$aperta = $this->assegna('2026-09-01');
|
||||
|
||||
$this->assertTrue(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-10-01'))?->is($aperta));
|
||||
}
|
||||
|
||||
public function test_un_periodo_chiuso_inserito_prima_di_tutti_non_si_sovrappone(): void
|
||||
{
|
||||
$this->assegna('2026-09-01');
|
||||
|
||||
$this->assertNull(Assegnazione::sovrappostaA($this->territorio->id, Carbon::parse('2026-06-01'), Carbon::parse('2026-07-01')));
|
||||
}
|
||||
|
||||
// ─── Pagina Assegna ─────────────────────────────────────────
|
||||
|
||||
public function test_assegna_rifiuta_un_uscita_retrodatata_a_prima_dell_ultimo_rientro(): void
|
||||
{
|
||||
$this->assegna('2026-09-01', '2026-09-20');
|
||||
|
||||
Livewire::test(Assegna::class, ['territorioId' => $this->territorio->id])
|
||||
->set('proclamatore_id', $this->proclamatore->id)
|
||||
->set('assigned_at', '2026-09-10')
|
||||
->call('save')
|
||||
->assertHasErrors('assigned_at');
|
||||
|
||||
$this->assertSame(1, Assegnazione::count());
|
||||
}
|
||||
|
||||
public function test_assegna_accetta_l_uscita_dal_giorno_del_rientro(): void
|
||||
{
|
||||
$this->assegna('2026-09-01', '2026-09-20');
|
||||
|
||||
Livewire::test(Assegna::class, ['territorioId' => $this->territorio->id])
|
||||
->set('proclamatore_id', $this->proclamatore->id)
|
||||
->set('assigned_at', '2026-09-20')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSame(2, Assegnazione::count());
|
||||
}
|
||||
|
||||
// ─── Registro (correzioni dell'amministratore) ──────────────
|
||||
|
||||
public function test_il_registro_ricava_l_anno_di_servizio_dalla_data(): void
|
||||
{
|
||||
Livewire::test(Registro::class)
|
||||
->call('openCreate')
|
||||
->set('form_territorio_id', (string) $this->territorio->id)
|
||||
->set('form_proclamatore_id', (string) $this->proclamatore->id)
|
||||
->set('form_assigned_at', '2025-08-10')
|
||||
->set('form_returned_at', '2025-09-05')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSame('2024-2025', Assegnazione::sole()->annoTeocratico->label);
|
||||
}
|
||||
|
||||
public function test_il_registro_non_crea_una_seconda_assegnazione_aperta(): void
|
||||
{
|
||||
$this->assegna('2026-09-01');
|
||||
|
||||
Livewire::test(Registro::class)
|
||||
->call('openCreate')
|
||||
->set('form_territorio_id', (string) $this->territorio->id)
|
||||
->set('form_proclamatore_id', (string) $this->proclamatore->id)
|
||||
->set('form_assigned_at', '2026-10-01')
|
||||
->call('save')
|
||||
->assertHasErrors('form_assigned_at');
|
||||
|
||||
$this->assertSame(1, Assegnazione::count());
|
||||
}
|
||||
|
||||
public function test_il_registro_non_riapre_un_assegnazione_chiusa_se_il_territorio_e_fuori(): void
|
||||
{
|
||||
$chiusa = $this->assegna('2026-08-01', '2026-08-20');
|
||||
$this->assegna('2026-09-01');
|
||||
|
||||
Livewire::test(Registro::class)
|
||||
->call('openEdit', $chiusa->id)
|
||||
->set('form_returned_at', '')
|
||||
->call('save')
|
||||
->assertHasErrors('form_assigned_at');
|
||||
|
||||
$this->assertNotNull($chiusa->fresh()->returned_at);
|
||||
}
|
||||
|
||||
public function test_il_registro_rifiuta_le_date_future(): void
|
||||
{
|
||||
Livewire::test(Registro::class)
|
||||
->call('openCreate')
|
||||
->set('form_territorio_id', (string) $this->territorio->id)
|
||||
->set('form_proclamatore_id', (string) $this->proclamatore->id)
|
||||
->set('form_assigned_at', '2026-11-01')
|
||||
->call('save')
|
||||
->assertHasErrors('form_assigned_at');
|
||||
}
|
||||
|
||||
public function test_il_registro_permette_di_correggere_la_stessa_assegnazione(): void
|
||||
{
|
||||
$aperta = $this->assegna('2026-09-01');
|
||||
|
||||
Livewire::test(Registro::class)
|
||||
->call('openEdit', $aperta->id)
|
||||
->set('form_assigned_at', '2026-09-03')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSame('2026-09-03', $aperta->fresh()->assigned_at->toDateString());
|
||||
}
|
||||
|
||||
// ─── helper ─────────────────────────────────────────────────
|
||||
|
||||
private function assegna(string $uscita, ?string $rientro = null): Assegnazione
|
||||
{
|
||||
return Assegnazione::create([
|
||||
'territorio_id' => $this->territorio->id,
|
||||
'proclamatore_id' => $this->proclamatore->id,
|
||||
'anno_teocratico_id' => AnnoTeocratico::perData(Carbon::parse($uscita))->id,
|
||||
'assigned_at' => $uscita,
|
||||
'returned_at' => $rientro,
|
||||
'created_by' => $this->admin->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,23 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\Registro;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use App\Services\Registro\ModuloS13;
|
||||
use Illuminate\Support\Collection;
|
||||
use ReflectionMethod;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Modulo S-13-I: il ciclo da quattro gruppi "Assegnato a" e la colonna
|
||||
* "Restituito l'ultima volta il".
|
||||
* Modulo S-13-I: il ciclo da quattro gruppi "Assegnato a", la colonna
|
||||
* "Restituito l'ultima volta il" e le assegnazioni a cavallo d'anno.
|
||||
*
|
||||
* E' la regola meno intuitiva del Registro — la quinta assegnazione non si
|
||||
* aggiunge alle quattro precedenti, le sostituisce — e finisce su un modulo che
|
||||
* viene firmato e archiviato: se sbaglia, sbaglia su carta.
|
||||
*
|
||||
* righeS13() e' privata di proposito (non e' un'azione Livewire); qui la si
|
||||
* chiama in riflessione perche' e' il calcolo, non il PDF, quello da proteggere.
|
||||
*/
|
||||
class RegistroS13Test extends TestCase
|
||||
{
|
||||
@@ -68,7 +64,7 @@ class RegistroS13Test extends TestCase
|
||||
$this->assertSame('', $riga['riporto']);
|
||||
$this->assertCount(4, $riga['assegnazioni']);
|
||||
$this->assertSame(
|
||||
['nome' => 'Rossi Mario', 'assegnato_il' => '10/09/2025', 'restituito_il' => '15/10/2025'],
|
||||
['nome' => 'Rossi M.', 'assegnato_il' => '10/09/2025', 'restituito_il' => '15/10/2025'],
|
||||
$riga['assegnazioni'][0]
|
||||
);
|
||||
|
||||
@@ -82,6 +78,19 @@ class RegistroS13Test extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_il_nome_e_cognome_piu_iniziale_del_nome(): void
|
||||
{
|
||||
$proclamatore = Proclamatore::create([
|
||||
'nome' => 'élena Maria',
|
||||
'cognome' => "Dell'Acqua Bianchi",
|
||||
'attivo' => true,
|
||||
]);
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-09-10')->update(['proclamatore_id' => $proclamatore->id]);
|
||||
|
||||
$this->assertSame("Dell'Acqua Bianchi É.", $this->righe()->sole()['assegnazioni'][0]['nome']);
|
||||
}
|
||||
|
||||
public function test_un_assegnazione_ancora_aperta_ha_la_data_di_rientro_vuota(): void
|
||||
{
|
||||
$territorio = $this->territorio('1');
|
||||
@@ -255,17 +264,11 @@ class RegistroS13Test extends TestCase
|
||||
$this->assertSame(['1'], $this->righe()->pluck('numero')->all());
|
||||
}
|
||||
|
||||
public function test_le_assegnazioni_di_un_altro_anno_di_servizio_non_compaiono(): void
|
||||
public function test_le_assegnazioni_chiuse_di_un_altro_anno_di_servizio_non_compaiono(): void
|
||||
{
|
||||
$altroAnno = AnnoTeocratico::create([
|
||||
'label' => '2024-2025',
|
||||
'start_date' => '2024-09-01',
|
||||
'end_date' => '2025-08-31',
|
||||
]);
|
||||
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-09-01', '2025-09-20');
|
||||
$this->assegna($territorio, '2025-01-10', '2025-02-10', $altroAnno);
|
||||
$this->assegna($territorio, '2025-01-10', '2025-02-10', $this->annoPrecedente());
|
||||
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
@@ -275,11 +278,168 @@ class RegistroS13Test extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_un_territorio_senza_assegnazioni_nell_anno_non_produce_una_riga(): void
|
||||
/**
|
||||
* Il registro serve a vedere anche i territori fermi: una riga vuota con
|
||||
* la data dell'ultimo rientro dice da quanto non escono.
|
||||
*/
|
||||
public function test_un_territorio_senza_assegnazioni_nell_anno_ha_comunque_la_sua_riga(): void
|
||||
{
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-03-01', '2025-04-12', $this->annoPrecedente());
|
||||
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
$this->assertSame('1', $riga['numero']);
|
||||
$this->assertSame('12/04/2025', $riga['riporto']);
|
||||
$this->assertSame(['', '', '', ''], array_column($riga['assegnazioni'], 'nome'));
|
||||
}
|
||||
|
||||
public function test_un_territorio_mai_assegnato_ha_la_riga_vuota(): void
|
||||
{
|
||||
$this->territorio('1');
|
||||
|
||||
$this->assertTrue($this->righe()->isEmpty());
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
$this->assertSame('', $riga['riporto']);
|
||||
$this->assertSame(['', '', '', ''], array_column($riga['assegnazioni'], 'assegnato_il'));
|
||||
}
|
||||
|
||||
// ─── Il riporto a inizio anno ───────────────────────────────
|
||||
|
||||
/**
|
||||
* La nota del modulo: "quando si inizia un nuovo foglio, registrare in
|
||||
* questa colonna l'ultima data in cui e' stato restituito ogni territorio".
|
||||
*/
|
||||
public function test_nel_primo_ciclo_il_riporto_e_l_ultimo_rientro_prima_dell_anno(): void
|
||||
{
|
||||
$precedente = $this->annoPrecedente();
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-01-10', '2025-02-10', $precedente);
|
||||
$this->assegna($territorio, '2025-05-02', '2025-06-30', $precedente);
|
||||
$this->assegna($territorio, '2025-10-01', '2025-10-20');
|
||||
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
$this->assertSame('30/06/2025', $riga['riporto']);
|
||||
$this->assertSame('01/10/2025', $riga['assegnazioni'][0]['assegnato_il']);
|
||||
}
|
||||
|
||||
// ─── A cavallo d'anno ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Uscito ad agosto, rientrato a ottobre: sul foglio nuovo occupa il primo
|
||||
* gruppo con entrambe le date, e il riporto e' il rientro precedente.
|
||||
*/
|
||||
public function test_l_assegnazione_a_cavallo_apre_il_foglio_nuovo_con_entrambe_le_date(): void
|
||||
{
|
||||
$precedente = $this->annoPrecedente();
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-05-02', '2025-06-30', $precedente);
|
||||
$this->assegna($territorio, '2025-08-10', '2025-10-05', $precedente);
|
||||
$this->assegna($territorio, '2025-11-01', '2025-11-20');
|
||||
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
$this->assertSame('30/06/2025', $riga['riporto']);
|
||||
$this->assertSame(
|
||||
['nome' => 'Rossi M.', 'assegnato_il' => '10/08/2025', 'restituito_il' => '05/10/2025'],
|
||||
$riga['assegnazioni'][0]
|
||||
);
|
||||
$this->assertSame('01/11/2025', $riga['assegnazioni'][1]['assegnato_il']);
|
||||
}
|
||||
|
||||
public function test_sul_foglio_vecchio_l_assegnazione_a_cavallo_resta_senza_rientro(): void
|
||||
{
|
||||
$precedente = $this->annoPrecedente();
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-08-10', '2025-10-05', $precedente);
|
||||
|
||||
$riga = $this->righe($precedente)->sole();
|
||||
|
||||
$this->assertSame('10/08/2025', $riga['assegnazioni'][0]['assegnato_il']);
|
||||
$this->assertSame('', $riga['assegnazioni'][0]['restituito_il']);
|
||||
}
|
||||
|
||||
public function test_un_assegnazione_ancora_fuori_da_un_anno_precedente_compare_sul_foglio_nuovo(): void
|
||||
{
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-07-01', null, $this->annoPrecedente());
|
||||
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
$this->assertSame('01/07/2025', $riga['assegnazioni'][0]['assegnato_il']);
|
||||
$this->assertSame('', $riga['assegnazioni'][0]['restituito_il']);
|
||||
}
|
||||
|
||||
/** L'assegnazione a cavallo conta nel ciclo: con altre quattro nell'anno, la riga ricomincia. */
|
||||
public function test_l_assegnazione_a_cavallo_conta_nel_ciclo_da_quattro(): void
|
||||
{
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-08-10', '2025-09-15', $this->annoPrecedente());
|
||||
$this->assegna($territorio, '2025-10-01', '2025-10-20');
|
||||
$this->assegna($territorio, '2025-11-01', '2025-11-20');
|
||||
$this->assegna($territorio, '2025-12-01', '2025-12-24');
|
||||
$this->assegna($territorio, '2026-01-05');
|
||||
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
$this->assertSame('24/12/2025', $riga['riporto']);
|
||||
$this->assertSame(['05/01/2026', '', '', ''], array_column($riga['assegnazioni'], 'assegnato_il'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Il foglio sceglie per data, non per anno_teocratico_id: una data
|
||||
* corretta a mano nel Registro o un import legacy possono lasciare l'anno
|
||||
* registrato incoerente con il giorno di uscita.
|
||||
*/
|
||||
public function test_conta_la_data_di_uscita_non_l_anno_registrato(): void
|
||||
{
|
||||
$precedente = $this->annoPrecedente();
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2025-10-01', '2025-10-20', $precedente);
|
||||
|
||||
$this->assertSame('01/10/2025', $this->righe()->sole()['assegnazioni'][0]['assegnato_il']);
|
||||
$this->assertSame('', $this->righe($precedente)->sole()['assegnazioni'][0]['assegnato_il']);
|
||||
}
|
||||
|
||||
public function test_l_ultimo_giorno_dell_anno_appartiene_al_foglio(): void
|
||||
{
|
||||
$territorio = $this->territorio('1');
|
||||
$this->assegna($territorio, '2026-08-31', '2026-08-31');
|
||||
|
||||
$riga = $this->righe()->sole();
|
||||
|
||||
$this->assertSame('31/08/2026', $riga['assegnazioni'][0]['assegnato_il']);
|
||||
$this->assertSame('31/08/2026', $riga['assegnazioni'][0]['restituito_il']);
|
||||
}
|
||||
|
||||
// ─── Impaginazione e PDF ────────────────────────────────────
|
||||
|
||||
public function test_ogni_foglio_ha_venti_righe_come_il_modulo(): void
|
||||
{
|
||||
foreach (range(1, 21) as $numero) {
|
||||
$this->territorio((string) $numero);
|
||||
}
|
||||
|
||||
$pagine = (new ModuloS13())->pagine($this->righe());
|
||||
|
||||
$this->assertCount(2, $pagine);
|
||||
$this->assertCount(20, $pagine[0]);
|
||||
$this->assertCount(20, $pagine[1]);
|
||||
$this->assertSame('21', $pagine[1][0]['numero']);
|
||||
$this->assertSame('', $pagine[1][1]['numero']);
|
||||
}
|
||||
|
||||
public function test_il_pdf_e_il_modulo_ufficiale_con_una_pagina_per_foglio(): void
|
||||
{
|
||||
foreach (range(1, 21) as $numero) {
|
||||
$this->assegna($this->territorio((string) $numero), '2025-09-01');
|
||||
}
|
||||
|
||||
$pdf = (new ModuloS13())->genera($this->anno);
|
||||
|
||||
$this->assertStringStartsWith('%PDF-', $pdf);
|
||||
$this->assertSame(2, preg_match_all('#/Type /Page\b(?!s)#', $pdf));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,8 +486,15 @@ class RegistroS13Test extends TestCase
|
||||
*/
|
||||
private function righe(?AnnoTeocratico $anno = null): Collection
|
||||
{
|
||||
$metodo = new ReflectionMethod(Registro::class, 'righeS13');
|
||||
return (new ModuloS13())->righe($anno ?? $this->anno);
|
||||
}
|
||||
|
||||
return $metodo->invoke(new Registro(), $anno ?? $this->anno);
|
||||
private function annoPrecedente(): AnnoTeocratico
|
||||
{
|
||||
return AnnoTeocratico::create([
|
||||
'label' => '2024-2025',
|
||||
'start_date' => '2024-09-01',
|
||||
'end_date' => '2025-08-31',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
78
tests/Feature/UltimoAmministratoreTest.php
Normal file
78
tests/Feature/UltimoAmministratoreTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\Settings\UsersIndex;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
/** L'installazione non deve mai restare senza un amministratore. */
|
||||
class UltimoAmministratoreTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Role::create(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||
Role::create(['name' => 'assistente', 'guard_name' => 'web']);
|
||||
|
||||
$this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password']);
|
||||
$this->admin->assignRole('amministratore');
|
||||
$this->actingAs($this->admin);
|
||||
}
|
||||
|
||||
public function test_non_si_toglie_a_se_stessi_il_ruolo_di_amministratore(): void
|
||||
{
|
||||
// Anche con un secondo amministratore: il declassamento di sé stessi
|
||||
// va chiesto a un altro.
|
||||
User::create(['name' => 'Altro', 'email' => 'altro@example.test', 'password' => 'password'])
|
||||
->assignRole('amministratore');
|
||||
|
||||
$this->modificaRuolo($this->admin, 'assistente')->assertHasErrors('editSelectedRole');
|
||||
|
||||
$this->assertTrue($this->admin->fresh()->hasRole('amministratore'));
|
||||
}
|
||||
|
||||
public function test_non_si_declassa_l_ultimo_amministratore(): void
|
||||
{
|
||||
// Chi modifica non è l'amministratore stesso, così la regola su sé
|
||||
// stessi non entra in gioco: si prova solo quella sull'ultimo. Il
|
||||
// permesso della pagina sta nel middleware della rotta, che
|
||||
// Livewire::test non attraversa.
|
||||
$operatore = User::create(['name' => 'Operatore', 'email' => 'op@example.test', 'password' => 'password']);
|
||||
$operatore->assignRole('assistente');
|
||||
$this->actingAs($operatore);
|
||||
|
||||
$this->modificaRuolo($this->admin, 'assistente')->assertHasErrors('editSelectedRole');
|
||||
|
||||
$this->assertTrue($this->admin->fresh()->hasRole('amministratore'));
|
||||
}
|
||||
|
||||
public function test_un_amministratore_ne_declassa_un_altro_se_non_e_l_ultimo(): void
|
||||
{
|
||||
$altro = User::create(['name' => 'Altro', 'email' => 'altro@example.test', 'password' => 'password']);
|
||||
$altro->assignRole('amministratore');
|
||||
|
||||
$this->modificaRuolo($altro, 'assistente')->assertHasNoErrors();
|
||||
|
||||
$this->assertTrue($altro->fresh()->hasRole('assistente'));
|
||||
$this->assertFalse($altro->fresh()->hasRole('amministratore'));
|
||||
}
|
||||
|
||||
private function modificaRuolo(User $utente, string $ruolo)
|
||||
{
|
||||
return Livewire::test(UsersIndex::class)
|
||||
->set('editingUserId', $utente->id)
|
||||
->set('editName', $utente->name)
|
||||
->set('editEmail', $utente->email)
|
||||
->set('editSelectedRole', $ruolo)
|
||||
->call('updateUser');
|
||||
}
|
||||
}
|
||||
216
tests/Feature/XmlExchangeImportTest.php
Normal file
216
tests/Feature/XmlExchangeImportTest.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\Settings\XmlExchange;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Campagna;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\User;
|
||||
use App\Models\Zona;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Export e import XML: lo stesso percorso del data.xml dei backup, quindi
|
||||
* ogni difetto qui è un difetto del ripristino.
|
||||
*
|
||||
* L'applicazione gira su Europe/Rome (default di config/app.php, nessun
|
||||
* override in phpunit.xml): è la condizione in cui le date slittavano.
|
||||
*/
|
||||
class XmlExchangeImportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Role::create(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||
Role::create(['name' => 'assistente', 'guard_name' => 'web']);
|
||||
|
||||
$this->admin = User::create([
|
||||
'name' => 'Amministratore',
|
||||
'email' => 'admin@example.test',
|
||||
'password' => 'password-attuale',
|
||||
]);
|
||||
$this->admin->assignRole('amministratore');
|
||||
$this->actingAs($this->admin);
|
||||
}
|
||||
|
||||
// ─── Round-trip ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Il difetto che spostava ogni data indietro di un giorno a ogni giro di
|
||||
* backup e ripristino: toArray() le serializzava in UTC.
|
||||
*/
|
||||
public function test_export_e_reimport_conservano_le_date_esatte(): void
|
||||
{
|
||||
$anno = AnnoTeocratico::create(['label' => '2025-2026', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31']);
|
||||
Campagna::create(['descrizione' => 'Commemorazione', 'start_date' => '2026-03-01', 'end_date' => '2026-03-31']);
|
||||
$territorio = Territorio::create(['numero' => '7', 'attivo' => true]);
|
||||
$proclamatore = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]);
|
||||
Assegnazione::create([
|
||||
'territorio_id' => $territorio->id,
|
||||
'proclamatore_id' => $proclamatore->id,
|
||||
'anno_teocratico_id' => $anno->id,
|
||||
'assigned_at' => '2026-03-15',
|
||||
'returned_at' => '2026-04-01',
|
||||
'created_by' => $this->admin->id,
|
||||
]);
|
||||
|
||||
$this->reimporta();
|
||||
|
||||
$assegnazione = Assegnazione::sole();
|
||||
$this->assertSame('2026-03-15', $assegnazione->assigned_at->toDateString());
|
||||
$this->assertSame('2026-04-01', $assegnazione->returned_at->toDateString());
|
||||
|
||||
$anno = AnnoTeocratico::sole();
|
||||
$this->assertSame('2025-09-01', $anno->start_date->toDateString());
|
||||
$this->assertSame('2026-08-31', $anno->end_date->toDateString());
|
||||
|
||||
$campagna = Campagna::sole();
|
||||
$this->assertSame('2026-03-01', $campagna->start_date->toDateString());
|
||||
$this->assertSame('2026-03-31', $campagna->end_date->toDateString());
|
||||
}
|
||||
|
||||
/** I backup fatti prima della correzione hanno le date in UTC: tornano giuste. */
|
||||
public function test_un_backup_vecchio_con_date_in_utc_viene_riportato_al_giorno_giusto(): void
|
||||
{
|
||||
$xml = $this->xmlMinimo(
|
||||
anni: '<anno legacy_id="1"><label>2025-2026</label><start_date>2025-08-31T22:00:00.000000Z</start_date><end_date>2026-08-30T22:00:00.000000Z</end_date></anno>',
|
||||
assegnazioni: '<assegnazione legacy_id="1"><legacy_territorio_id>1</legacy_territorio_id><legacy_proclamatore_id>1</legacy_proclamatore_id><legacy_anno_id>1</legacy_anno_id><assigned_at>2026-03-14T23:00:00.000000Z</assigned_at><returned_at></returned_at></assegnazione>',
|
||||
);
|
||||
|
||||
$this->assertArrayNotHasKey('error', $this->importa($xml));
|
||||
|
||||
$this->assertSame('2026-03-15', Assegnazione::sole()->assigned_at->toDateString());
|
||||
$this->assertSame('2025-09-01', AnnoTeocratico::sole()->start_date->toDateString());
|
||||
$this->assertSame('2026-08-31', AnnoTeocratico::sole()->end_date->toDateString());
|
||||
}
|
||||
|
||||
/** Un carattere di controllo in una nota rendeva l'intero file illeggibile. */
|
||||
public function test_un_carattere_di_controllo_in_una_nota_non_rompe_il_file(): void
|
||||
{
|
||||
Territorio::create(['numero' => '1', 'attivo' => true, 'note' => "Citofono\x0Brotto\x01"]);
|
||||
|
||||
$xml = (new XmlExchange())->currentXmlBackup();
|
||||
|
||||
$this->assertNotFalse(simplexml_load_string($xml));
|
||||
$this->reimporta($xml);
|
||||
$this->assertSame('Citofonorotto', Territorio::sole()->note);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un territorio resta senza zona quando la sua zona viene eliminata: al
|
||||
* reimport veniva scartato insieme a tutte le sue assegnazioni.
|
||||
*/
|
||||
public function test_un_territorio_senza_zona_viene_importato_con_il_suo_storico(): void
|
||||
{
|
||||
$anno = AnnoTeocratico::create(['label' => '2025-2026', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31']);
|
||||
$territorio = Territorio::create(['numero' => '3', 'attivo' => true, 'zona_id' => null]);
|
||||
$proclamatore = Proclamatore::create(['nome' => 'Anna', 'cognome' => 'Bianchi', 'attivo' => true]);
|
||||
Assegnazione::create([
|
||||
'territorio_id' => $territorio->id,
|
||||
'proclamatore_id' => $proclamatore->id,
|
||||
'anno_teocratico_id' => $anno->id,
|
||||
'assigned_at' => '2025-10-01',
|
||||
'created_by' => $this->admin->id,
|
||||
]);
|
||||
|
||||
$this->reimporta();
|
||||
|
||||
$this->assertNull(Territorio::sole()->zona_id);
|
||||
$this->assertSame(1, Assegnazione::count());
|
||||
}
|
||||
|
||||
// ─── File da rifiutare ──────────────────────────────────────
|
||||
|
||||
public function test_un_xml_estraneo_viene_rifiutato_senza_toccare_i_dati(): void
|
||||
{
|
||||
Territorio::create(['numero' => '1', 'attivo' => true]);
|
||||
|
||||
$esito = $this->importa('<?xml version="1.0"?><urlset><url><loc>https://example.org</loc></url></urlset>');
|
||||
|
||||
$this->assertStringContainsString('non è un export di TerManager2', $esito['error']);
|
||||
$this->assertSame(1, Territorio::count());
|
||||
}
|
||||
|
||||
public function test_un_export_senza_le_sezioni_principali_viene_rifiutato(): void
|
||||
{
|
||||
Territorio::create(['numero' => '1', 'attivo' => true]);
|
||||
|
||||
$esito = $this->importa('<?xml version="1.0"?><termanager2><settings/></termanager2>');
|
||||
|
||||
$this->assertStringContainsString('mancano le sezioni', $esito['error']);
|
||||
$this->assertSame(1, Territorio::count());
|
||||
}
|
||||
|
||||
public function test_un_file_malformato_indica_riga_e_motivo(): void
|
||||
{
|
||||
$esito = $this->importa("<?xml version=\"1.0\"?>\n<termanager2>\n<territori>\n</termanager2>");
|
||||
|
||||
$this->assertStringContainsString('riga', $esito['error']);
|
||||
}
|
||||
|
||||
// ─── Chi importa non resta fuori ────────────────────────────
|
||||
|
||||
/**
|
||||
* Il file può contenere l'account di chi importa con una password vecchia
|
||||
* e senza ruoli: l'amministratore che lo carica non deve perdere né l'una
|
||||
* né gli altri.
|
||||
*/
|
||||
public function test_chi_importa_conserva_password_e_ruolo_di_amministratore(): void
|
||||
{
|
||||
$hashVecchio = Hash::make('password-di-un-anno-fa');
|
||||
$xml = $this->xmlMinimo(
|
||||
utenti: '<user legacy_id="9"><name>Amministratore</name><email>admin@example.test</email><password>' . $hashVecchio . '</password><roles></roles></user>'
|
||||
. '<user legacy_id="10"><name>Altro</name><email>altro@example.test</email><password>' . $hashVecchio . '</password><roles><role>assistente</role></roles></user>',
|
||||
);
|
||||
|
||||
$this->assertArrayNotHasKey('error', $this->importa($xml));
|
||||
|
||||
$admin = $this->admin->fresh();
|
||||
$this->assertTrue(Hash::check('password-attuale', $admin->password));
|
||||
$this->assertTrue($admin->hasRole('amministratore'));
|
||||
|
||||
// Gli altri account invece prendono i dati del file.
|
||||
$altro = User::where('email', 'altro@example.test')->sole();
|
||||
$this->assertTrue(Hash::check('password-di-un-anno-fa', $altro->password));
|
||||
$this->assertTrue($altro->hasRole('assistente'));
|
||||
}
|
||||
|
||||
// ─── helper ─────────────────────────────────────────────────
|
||||
|
||||
private function reimporta(?string $xml = null): void
|
||||
{
|
||||
$xml ??= (new XmlExchange())->currentXmlBackup();
|
||||
|
||||
$esito = $this->importa($xml);
|
||||
|
||||
$this->assertArrayNotHasKey('error', $esito, $esito['error'] ?? '');
|
||||
$this->assertSame([], $esito['issues'], json_encode($esito['issues']));
|
||||
}
|
||||
|
||||
private function importa(string $xml): array
|
||||
{
|
||||
return (new XmlExchange())->importXmlFromContent($xml);
|
||||
}
|
||||
|
||||
private function xmlMinimo(string $utenti = '', string $anni = '', string $assegnazioni = ''): string
|
||||
{
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><termanager2 version="1.0">'
|
||||
. '<settings/><users>' . $utenti . '</users><zones/><tipologie/>'
|
||||
. '<proclamatori><proclamatore legacy_id="1"><nome>Mario</nome><cognome>Rossi</cognome><attivo>1</attivo></proclamatore></proclamatori>'
|
||||
. '<territori><territorio legacy_id="1"><numero>1</numero><legacy_zona_id></legacy_zona_id><legacy_tipologia_id></legacy_tipologia_id><attivo>1</attivo></territorio></territori>'
|
||||
. '<anni_teocratici>' . $anni . '</anni_teocratici><campagne/>'
|
||||
. '<assegnazioni>' . $assegnazioni . '</assegnazioni>'
|
||||
. '</termanager2>';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user