'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: '2025-08-31T22:00:00.000000Z2026-08-30T22:00:00.000000Z',
assegnazioni: '1112026-03-14T23:00:00.000000Z',
);
$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('https://example.org');
$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('');
$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("\n\n\n");
$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: 'Amministratoreadmin@example.test' . $hashVecchio . ''
. 'Altroaltro@example.test' . $hashVecchio . 'assistente',
);
$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: 'Amministratoreadmin@example.test' . $hash . 'vecchio'
. 'Altroaltro@example.test' . $hash . 'rubatoassistente',
);
$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 ''
. '' . $utenti . ''
. 'MarioRossi1'
. '11'
. '' . $anni . ''
. '' . $assegnazioni . ''
. '';
}
}