Blocco 2 - Esiti delle azioni Livewire come avvisi (trait Avvisa) invece di flash che comparivano alla pagina dopo. - PDF, miniature e versioni dei territori non più serviti da nginx su /storage: TerritorioFileController con login, permessi e cache privata. - Ripristino tutto o niente: verifica dell'archivio, una transazione con import, piantine, blocco account e scambio dei file annullabile. - Backup formato 2 con piantine, storico PDF e area KML. - Posto unico preso in un passo (Cache::lock) e rispettato anche da import XML e PDF. - Import PDF da cartella: abbinamento corretto (1-A, 12 B, ambigui), caricamento a lotti, file non PDF ignorati, PDF sostituito nello storico. - L'import XML elimina i file dei territori che non usa più; comando territori:file-orfani per quelli accumulati prima. Blocco 3 - Password attuale richiesta al cambio volontario; AuthenticateSession chiude le sessioni aperte altrove. - Niente nomi dei proclamatori in chiaro nel registro attività. - Export XML senza remember_token; chi riceve l'hash dal file deve cambiare password. - Campagne: ultimo giorno incluso, percentuale sui territori distinti. - Coda: retry_after 960 s, stop_grace_period 16m, Redis volatile-lru, impostazioni rilette a ogni job. - Azzeramento che cancella anche piantine, area KML e anteprime. - Pulsanti profilo e menu che non si nascondevano; icona del sito. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
252 lines
11 KiB
PHP
252 lines
11 KiB
PHP
<?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'));
|
|
}
|
|
|
|
/**
|
|
* Le credenziali che arrivano da un file non sono più segrete: chi riceve
|
|
* l'hash dal file deve cambiare password, e il "ricordami" non viaggia.
|
|
*/
|
|
public function test_le_credenziali_del_file_obbligano_a_cambiare_password(): void
|
|
{
|
|
$this->admin->forceFill(['remember_token' => 'token-attuale'])->save();
|
|
$hash = Hash::make('password-del-file');
|
|
$xml = $this->xmlMinimo(
|
|
utenti: '<user legacy_id="9"><name>Amministratore</name><email>admin@example.test</email><password>' . $hash . '</password><remember_token>vecchio</remember_token><roles></roles></user>'
|
|
. '<user legacy_id="10"><name>Altro</name><email>altro@example.test</email><password>' . $hash . '</password><remember_token>rubato</remember_token><roles><role>assistente</role></roles></user>',
|
|
);
|
|
|
|
$this->assertArrayNotHasKey('error', $this->importa($xml));
|
|
|
|
$altro = User::where('email', 'altro@example.test')->sole();
|
|
$this->assertTrue((bool) $altro->password_change_required);
|
|
$this->assertNull($altro->remember_token);
|
|
|
|
// Chi importa resta com'era.
|
|
$admin = $this->admin->fresh();
|
|
$this->assertFalse((bool) $admin->password_change_required);
|
|
$this->assertSame('token-attuale', $admin->getRememberToken());
|
|
}
|
|
|
|
public function test_l_export_non_contiene_i_remember_token(): void
|
|
{
|
|
$this->admin->forceFill(['remember_token' => 'token-segreto'])->save();
|
|
|
|
$xml = (new XmlExchange())->currentXmlBackup();
|
|
|
|
$this->assertStringNotContainsString('remember_token', $xml);
|
|
$this->assertStringNotContainsString('token-segreto', $xml);
|
|
}
|
|
|
|
// ─── 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>';
|
|
}
|
|
}
|