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>
334 lines
13 KiB
PHP
334 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Jobs\ImportTerritoryPdfFolder;
|
|
use App\Livewire\Settings\XmlExchange;
|
|
use App\Models\Territorio;
|
|
use App\Models\User;
|
|
use App\Services\TerritorioPdfImportState;
|
|
use App\Services\TerritorioThumbnailService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Livewire;
|
|
use Spatie\Permission\Models\Role;
|
|
use Spatie\Permission\PermissionRegistrar;
|
|
use Tests\TestCase;
|
|
use ZipArchive;
|
|
|
|
/**
|
|
* Import dei PDF dei territori: da cartella e da ZIP.
|
|
*
|
|
* Il difetto che costa di piu' qui non fa rumore: un PDF abbinato al territorio
|
|
* sbagliato ne sostituisce la mappa, e ce ne si accorge solo quando qualcuno va
|
|
* in un territorio con la cartina di un altro. Per questo l'abbinamento scarta
|
|
* invece di indovinare, e questi test fissano la regola caso per caso.
|
|
*/
|
|
class ImportPdfTerritoriTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const PDF = "%PDF-1.4\n1 0 obj << /Type /Catalog >> endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n";
|
|
|
|
private User $admin;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Storage::fake('public');
|
|
|
|
// pdftoppm non serve a niente qui, e su un PDF finto fallirebbe comunque.
|
|
$this->mock(TerritorioThumbnailService::class, function ($mock) {
|
|
$mock->shouldReceive('generate')->andReturn(null);
|
|
$mock->shouldReceive('delete');
|
|
});
|
|
|
|
Role::create(['name' => 'amministratore', 'guard_name' => 'web']);
|
|
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
|
|
|
$this->admin = User::create([
|
|
'name' => 'Amministratore',
|
|
'email' => 'admin@example.test',
|
|
'password' => 'password-di-prova',
|
|
]);
|
|
$this->admin->assignRole('amministratore');
|
|
$this->actingAs($this->admin);
|
|
}
|
|
|
|
// ─── Abbinamento nome file → territorio ─────────────────────
|
|
|
|
/**
|
|
* Il caso segnalato: "1-A" conteneva "1", e senza un territorio 1A il PDF
|
|
* finiva sul territorio 1 sostituendone la mappa.
|
|
*/
|
|
public function test_un_file_1_A_non_sostituisce_il_pdf_del_territorio_1(): void
|
|
{
|
|
$uno = $this->territorioConPdf('1');
|
|
|
|
$stato = $this->importa(['1-A.pdf']);
|
|
|
|
$this->assertSame('territori-pdf/originale-1.pdf', $uno->fresh()->pdf_path);
|
|
Storage::disk('public')->assertExists('territori-pdf/originale-1.pdf');
|
|
$this->assertSame(1, $stato['stats']['skipped']);
|
|
$this->assertSame('unknown-suffix', $stato['issues'][0]['type']);
|
|
$this->assertSame(['1'], $stato['issues'][0]['matched_numbers']);
|
|
}
|
|
|
|
/**
|
|
* Prima il vecchio PDF si cancellava prima di scrivere il nuovo: un import
|
|
* sbagliato non si poteva disfare.
|
|
*/
|
|
public function test_il_pdf_sostituito_finisce_nello_storico(): void
|
|
{
|
|
$uno = $this->territorioConPdf('1');
|
|
|
|
$this->importa(['1.pdf']);
|
|
|
|
$uno->refresh();
|
|
$this->assertNotSame('territori-pdf/originale-1.pdf', $uno->pdf_path);
|
|
Storage::disk('public')->assertExists($uno->pdf_path);
|
|
|
|
$versione = \App\Models\TerritorioPdfVersione::where('territorio_id', $uno->id)->sole();
|
|
$this->assertSame('import', $versione->origine);
|
|
$this->assertSame('PDF ORIGINALE', Storage::disk('public')->get($versione->pdf_path));
|
|
}
|
|
|
|
public function test_un_file_1_A_va_al_territorio_1_A_quando_esiste(): void
|
|
{
|
|
$uno = $this->territorioConPdf('1');
|
|
$unoA = Territorio::create(['numero' => '1-A', 'attivo' => true]);
|
|
|
|
$this->importa(['1-A.pdf']);
|
|
|
|
$this->assertNotNull($unoA->fresh()->pdf_path);
|
|
$this->assertSame('territori-pdf/originale-1.pdf', $uno->fresh()->pdf_path);
|
|
}
|
|
|
|
/**
|
|
* "12 B", "12B" e "12-b" sono lo stesso numero, e non e' il 12: la lettera
|
|
* dopo il numero fa parte del numero.
|
|
*/
|
|
public function test_12_B_con_lo_spazio_e_il_territorio_12B_e_non_il_12(): void
|
|
{
|
|
$dodici = $this->territorioConPdf('12');
|
|
$dodiciB = Territorio::create(['numero' => '12B', 'attivo' => true]);
|
|
|
|
$stato = $this->importa(['12 B.pdf']);
|
|
|
|
$this->assertSame(1, $stato['stats']['updated']);
|
|
$this->assertNotNull($dodiciB->fresh()->pdf_path);
|
|
$this->assertSame('territori-pdf/originale-12.pdf', $dodici->fresh()->pdf_path);
|
|
}
|
|
|
|
/**
|
|
* Due territori che si scrivono diversi ma sono lo stesso numero: il file
|
|
* che li nomina non si puo' abbinare con certezza, quindi si scarta.
|
|
*/
|
|
public function test_due_territori_con_lo_stesso_numero_scritto_diverso_rendono_il_file_ambiguo(): void
|
|
{
|
|
$attaccato = Territorio::create(['numero' => '12B', 'attivo' => true]);
|
|
$staccato = Territorio::create(['numero' => '12 B', 'attivo' => true]);
|
|
|
|
$stato = $this->importa(['12b.pdf']);
|
|
|
|
$this->assertNull($attaccato->fresh()->pdf_path);
|
|
$this->assertNull($staccato->fresh()->pdf_path);
|
|
$this->assertSame('ambiguous', $stato['issues'][0]['type']);
|
|
$this->assertEqualsCanonicalizing(['12B', '12 B'], $stato['issues'][0]['matched_numbers']);
|
|
}
|
|
|
|
public function test_due_numeri_di_territori_esistenti_nello_stesso_nome_rendono_il_file_ambiguo(): void
|
|
{
|
|
$tre = Territorio::create(['numero' => '3', 'attivo' => true]);
|
|
$dodici = Territorio::create(['numero' => '12', 'attivo' => true]);
|
|
|
|
$stato = $this->importa(['Zona 3 - Territorio 12.pdf']);
|
|
|
|
$this->assertNull($tre->fresh()->pdf_path);
|
|
$this->assertNull($dodici->fresh()->pdf_path);
|
|
$this->assertSame('ambiguous', $stato['issues'][0]['type']);
|
|
}
|
|
|
|
/** Le forme di sempre devono continuare a funzionare. */
|
|
public function test_zeri_iniziali_testo_intorno_e_numeri_estranei_non_impediscono_l_abbinamento(): void
|
|
{
|
|
$sette = Territorio::create(['numero' => '7', 'attivo' => true]);
|
|
$dodici = Territorio::create(['numero' => '12', 'attivo' => true]);
|
|
|
|
$stato = $this->importa(['Territorio 007 - Via Roma.pdf', '2024_territorio_12.pdf']);
|
|
|
|
$this->assertSame(2, $stato['stats']['updated']);
|
|
$this->assertNotNull($sette->fresh()->pdf_path);
|
|
$this->assertNotNull($dodici->fresh()->pdf_path);
|
|
}
|
|
|
|
public function test_lo_stesso_territorio_scritto_in_due_modi_nella_stessa_cartella_e_un_doppione(): void
|
|
{
|
|
$dodiciB = Territorio::create(['numero' => '12B', 'attivo' => true]);
|
|
|
|
$stato = $this->importa(['12B.pdf', '12 B.pdf']);
|
|
|
|
$this->assertSame(1, $stato['stats']['updated']);
|
|
$this->assertSame('duplicate-in-batch', $stato['issues'][0]['type']);
|
|
$this->assertNotNull($dodiciB->fresh()->pdf_path);
|
|
}
|
|
|
|
// ─── Cartella ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Thumbs.db e simili facevano fallire la validazione `mimes:pdf` di tutta la
|
|
* cartella: nessun PDF importato per colpa di un file che Windows crea da sé.
|
|
*/
|
|
public function test_thumbs_db_e_i_file_nascosti_nella_cartella_non_bloccano_l_import(): void
|
|
{
|
|
Storage::fake('local');
|
|
$uno = Territorio::create(['numero' => '1', 'attivo' => true]);
|
|
|
|
Livewire::test(XmlExchange::class)
|
|
->set('pdfFolder', [
|
|
UploadedFile::fake()->createWithContent('1.pdf', self::PDF),
|
|
UploadedFile::fake()->createWithContent('Thumbs.db', 'non un pdf'),
|
|
UploadedFile::fake()->createWithContent('desktop.ini', '[.ShellClassInfo]'),
|
|
UploadedFile::fake()->createWithContent('.DS_Store', 'non un pdf'),
|
|
])
|
|
->call('importTerritoryPdfFolder')
|
|
->assertHasNoErrors()
|
|
->assertSet('pdfImportStatus', 'completed');
|
|
|
|
$this->assertNotNull($uno->fresh()->pdf_path);
|
|
Storage::disk('public')->assertExists($uno->fresh()->pdf_path);
|
|
}
|
|
|
|
public function test_un_pdf_non_valido_nella_cartella_si_scarta_da_solo(): void
|
|
{
|
|
Storage::fake('local');
|
|
$uno = Territorio::create(['numero' => '1', 'attivo' => true]);
|
|
$due = Territorio::create(['numero' => '2', 'attivo' => true]);
|
|
|
|
$componente = Livewire::test(XmlExchange::class)
|
|
->set('pdfFolder', [
|
|
UploadedFile::fake()->createWithContent('1.pdf', self::PDF),
|
|
// Un'immagine rinominata. Nei test Livewire prende il tipo
|
|
// dichiarato dal file finto, non dal contenuto: va detto.
|
|
UploadedFile::fake()->createWithContent('2.pdf', "\x89PNG\r\n\x1a\n" . str_repeat("\0", 64))->mimeType('image/png'),
|
|
])
|
|
->call('importTerritoryPdfFolder')
|
|
->assertHasNoErrors();
|
|
|
|
$this->assertNotNull($uno->fresh()->pdf_path);
|
|
$this->assertNull($due->fresh()->pdf_path);
|
|
$this->assertSame('invalid-file', $componente->get('pdfImportIssues')[0]['type']);
|
|
}
|
|
|
|
/**
|
|
* Con wire:model la cartella partiva in una richiesta sola, e PHP ne teneva
|
|
* i primi `max_file_uploads` (20) scartando gli altri in silenzio. Il browser
|
|
* la spedisce a lotti: la pagina deve dirgli quanto grandi, e l'input non
|
|
* deve avere wire:model.
|
|
*/
|
|
public function test_la_cartella_parte_a_lotti_dentro_il_limite_di_php(): void
|
|
{
|
|
$this->withoutVite();
|
|
|
|
$lotto = min(20, (int) ini_get('max_file_uploads'));
|
|
|
|
$this->get(route('xml.exchange'))
|
|
->assertOk()
|
|
->assertSee('data-file-per-lotto="' . $lotto . '"', false)
|
|
->assertSee('uploadMultiple(', false)
|
|
->assertDontSee('wire:model="pdfFolder"', false);
|
|
}
|
|
|
|
/** Lotti aggiunti uno dopo l'altro: il server deve importarli tutti. */
|
|
public function test_una_cartella_di_piu_di_venti_pdf_viene_importata_per_intero(): void
|
|
{
|
|
Storage::fake('local');
|
|
|
|
$file = [];
|
|
for ($numero = 1; $numero <= 25; $numero++) {
|
|
Territorio::create(['numero' => (string) $numero, 'attivo' => true]);
|
|
$file[] = UploadedFile::fake()->createWithContent($numero . '.pdf', self::PDF);
|
|
}
|
|
|
|
Livewire::test(XmlExchange::class)
|
|
->set('pdfFolder', $file)
|
|
->call('importTerritoryPdfFolder')
|
|
->assertHasNoErrors();
|
|
|
|
$this->assertSame(25, Territorio::whereNotNull('pdf_path')->count());
|
|
}
|
|
|
|
// ─── ZIP ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Uno ZIP fatto su macOS porta con se' __MACOSX/._1.pdf: estensione giusta,
|
|
* contenuto spazzatura. Arrivando per primo, diventava la mappa del
|
|
* territorio 1 e il PDF vero veniva scartato come doppione.
|
|
*/
|
|
public function test_lo_zip_ignora_file_di_sistema_e_copie_macosx(): void
|
|
{
|
|
$uno = Territorio::create(['numero' => '1', 'attivo' => true]);
|
|
|
|
$zip = $this->zip([
|
|
'__MACOSX/._1.pdf' => 'risorse di macOS, non un PDF',
|
|
'Thumbs.db' => 'non un pdf',
|
|
'territori/1.pdf' => self::PDF,
|
|
]);
|
|
|
|
$this->postJson(route('imports.territori.pdf-zip'), ['pdfZip' => $zip])
|
|
->assertOk();
|
|
|
|
$percorso = $uno->fresh()->pdf_path;
|
|
$this->assertNotNull($percorso);
|
|
$this->assertSame(self::PDF, Storage::disk('public')->get($percorso));
|
|
}
|
|
|
|
// ─── Supporto ───────────────────────────────────────────────
|
|
|
|
private function territorioConPdf(string $numero): Territorio
|
|
{
|
|
$percorso = 'territori-pdf/originale-' . Str::slug($numero) . '.pdf';
|
|
Storage::disk('public')->put($percorso, 'PDF ORIGINALE');
|
|
|
|
return Territorio::create(['numero' => $numero, 'attivo' => true, 'pdf_path' => $percorso]);
|
|
}
|
|
|
|
/**
|
|
* Esegue il job su file con i nomi dati, come se fossero arrivati da una
|
|
* cartella, e restituisce lo stato finale dell'import.
|
|
*/
|
|
private function importa(array $nomi): array
|
|
{
|
|
Storage::fake('local');
|
|
|
|
$importId = (string) Str::uuid();
|
|
$file = [];
|
|
foreach (array_values($nomi) as $indice => $nome) {
|
|
$percorso = 'bulk-territori-imports/' . $importId . '/' . $indice . '.pdf';
|
|
Storage::disk('local')->put($percorso, self::PDF);
|
|
$file[] = ['original_name' => $nome, 'stored_path' => $percorso];
|
|
}
|
|
|
|
app(TerritorioPdfImportState::class)->initialize($importId, count($file));
|
|
ImportTerritoryPdfFolder::dispatchSync($importId, $file, $this->admin->id);
|
|
|
|
return app(TerritorioPdfImportState::class)->get($importId);
|
|
}
|
|
|
|
private function zip(array $voci): UploadedFile
|
|
{
|
|
$percorso = tempnam(sys_get_temp_dir(), 'zip-pdf-');
|
|
$zip = new ZipArchive();
|
|
$zip->open($percorso, ZipArchive::OVERWRITE);
|
|
foreach ($voci as $nome => $contenuto) {
|
|
$zip->addFromString($nome, $contenuto);
|
|
}
|
|
$zip->close();
|
|
|
|
return new UploadedFile($percorso, 'territori.zip', 'application/zip', null, true);
|
|
}
|
|
}
|