Revisione blocchi 2 e 3: file protetti, ripristino atomico, import sicuri
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>
This commit is contained in:
268
tests/Feature/PostoUnicoImportTest.php
Normal file
268
tests/Feature/PostoUnicoImportTest.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\CreateBackupArchive;
|
||||
use App\Jobs\ImportTerritoryPdfFolder;
|
||||
use App\Jobs\RestoreBackupArchive;
|
||||
use App\Livewire\Settings\BackupManager;
|
||||
use App\Livewire\Settings\XmlExchange;
|
||||
use App\Livewire\Settings\ZonaPericolosa;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\User;
|
||||
use App\Services\BackupOperationState;
|
||||
use App\Services\TerritorioPdfImportDispatcher;
|
||||
use App\Services\TerritorioThumbnailService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
use Tests\TestCase;
|
||||
use ZipArchive;
|
||||
|
||||
/**
|
||||
* Il posto unico vale anche per gli import.
|
||||
*
|
||||
* Backup, ripristino e azzeramento si escludevano a vicenda, ma l'import XML e
|
||||
* l'import PDF passavano accanto al blocco: un import XML durante un ripristino
|
||||
* riscriveva le stesse tabelle, un import PDF durante un backup cambiava i file
|
||||
* mentre l'archivio li copiava. Qui si verifica il blocco nelle due direzioni e
|
||||
* che il posto si liberi anche quando l'import fallisce.
|
||||
*/
|
||||
class PostoUnicoImportTest 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');
|
||||
Storage::fake('local');
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
// ─── Un'operazione di backup blocca gli import ──────────────
|
||||
|
||||
public function test_l_import_pdf_da_cartella_non_parte_durante_un_ripristino(): void
|
||||
{
|
||||
Queue::fake();
|
||||
app(BackupOperationState::class)->initialize('op-restore', BackupOperationState::TIPO_RESTORE, 'Estrazione', 10);
|
||||
Territorio::create(['numero' => '1', 'attivo' => true]);
|
||||
|
||||
Livewire::test(XmlExchange::class)
|
||||
->set('pdfFolder', [UploadedFile::fake()->createWithContent('1.pdf', self::PDF)])
|
||||
->call('importTerritoryPdfFolder')
|
||||
->assertDispatched('avviso', fn ($nome, $parametri) => $parametri['tipo'] === 'errore'
|
||||
&& str_contains($parametri['testo'], 'ripristino'));
|
||||
|
||||
Queue::assertNotPushed(ImportTerritoryPdfFolder::class);
|
||||
$this->assertSame('op-restore', app(BackupOperationState::class)->corrente());
|
||||
}
|
||||
|
||||
public function test_il_controllo_prima_di_caricare_la_cartella_avvisa_se_il_posto_e_occupato(): void
|
||||
{
|
||||
app(BackupOperationState::class)->initialize('op-backup', BackupOperationState::TIPO_BACKUP, 'Compressione', 10);
|
||||
|
||||
Livewire::test(XmlExchange::class)
|
||||
->call('cartellaPuoPartire')
|
||||
->assertReturned(false)
|
||||
->assertDispatched('avviso', fn ($nome, $parametri) => $parametri['tipo'] === 'errore');
|
||||
}
|
||||
|
||||
public function test_l_import_zip_non_parte_durante_un_azzeramento(): void
|
||||
{
|
||||
Queue::fake();
|
||||
app(BackupOperationState::class)->initialize('op-reset', BackupOperationState::TIPO_RESET, 'Backup preventivo', 3);
|
||||
|
||||
$this->postJson(route('imports.territori.pdf-zip'), ['pdfZip' => $this->zip(['1.pdf' => self::PDF])])
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment(['message' => app(BackupOperationState::class)->messaggioOccupato('op-reset')]);
|
||||
|
||||
Queue::assertNotPushed(ImportTerritoryPdfFolder::class);
|
||||
$this->assertSame('op-reset', app(BackupOperationState::class)->corrente());
|
||||
}
|
||||
|
||||
public function test_l_import_xml_non_parte_durante_un_backup_e_non_tocca_i_dati(): void
|
||||
{
|
||||
app(BackupOperationState::class)->initialize('op-backup', BackupOperationState::TIPO_BACKUP, 'Compressione', 10);
|
||||
Territorio::create(['numero' => '99', 'attivo' => true]);
|
||||
|
||||
$this->post(route('xml.import-xml'), ['xmlImport' => $this->xml('1')])
|
||||
->assertSessionHasErrors('xmlImport');
|
||||
|
||||
$this->assertSame(['99'], Territorio::pluck('numero')->all());
|
||||
$this->assertSame('op-backup', app(BackupOperationState::class)->corrente());
|
||||
}
|
||||
|
||||
// ─── Un import blocca le operazioni di backup ───────────────
|
||||
|
||||
public function test_un_import_pdf_in_coda_blocca_backup_ripristino_e_azzeramento(): void
|
||||
{
|
||||
Queue::fake();
|
||||
Territorio::create(['numero' => '1', 'attivo' => true]);
|
||||
|
||||
Livewire::test(XmlExchange::class)
|
||||
->set('pdfFolder', [UploadedFile::fake()->createWithContent('1.pdf', self::PDF)])
|
||||
->call('importTerritoryPdfFolder');
|
||||
|
||||
Queue::assertPushed(ImportTerritoryPdfFolder::class);
|
||||
$stato = app(BackupOperationState::class);
|
||||
$import = $stato->corrente();
|
||||
$this->assertNotNull($import, 'L\'import PDF deve occupare il posto unico finche\' il job non finisce.');
|
||||
$this->assertSame(BackupOperationState::TIPO_IMPORT_PDF, $stato->get($import)['tipo']);
|
||||
|
||||
Livewire::test(BackupManager::class)
|
||||
->call('avviaBackup')
|
||||
->assertSet('erroreOperazione', $stato->messaggioOccupato($import));
|
||||
Queue::assertNotPushed(CreateBackupArchive::class);
|
||||
|
||||
Livewire::test(ZonaPericolosa::class)
|
||||
->set('confermaReset', ZonaPericolosa::CONFERMA_AZZERAMENTO)
|
||||
->call('avviaAzzeramento');
|
||||
Queue::assertNotPushed(CreateBackupArchive::class);
|
||||
|
||||
$this->postJson(route('backup.restore'), [
|
||||
'backupZip' => $this->zip(['manifest.json' => '{}']),
|
||||
])->assertStatus(409);
|
||||
Queue::assertNotPushed(RestoreBackupArchive::class);
|
||||
|
||||
$this->assertSame($import, $stato->corrente());
|
||||
}
|
||||
|
||||
public function test_un_secondo_import_non_parte_mentre_il_primo_e_in_coda(): void
|
||||
{
|
||||
Queue::fake();
|
||||
Territorio::create(['numero' => '1', 'attivo' => true]);
|
||||
|
||||
Livewire::test(XmlExchange::class)
|
||||
->set('pdfFolder', [UploadedFile::fake()->createWithContent('1.pdf', self::PDF)])
|
||||
->call('importTerritoryPdfFolder');
|
||||
|
||||
$this->post(route('xml.import-xml'), ['xmlImport' => $this->xml('1')])
|
||||
->assertSessionHasErrors('xmlImport');
|
||||
|
||||
Queue::assertPushed(ImportTerritoryPdfFolder::class, 1);
|
||||
}
|
||||
|
||||
// ─── Il posto si libera ─────────────────────────────────────
|
||||
|
||||
public function test_l_import_pdf_libera_il_posto_quando_finisce(): void
|
||||
{
|
||||
$uno = Territorio::create(['numero' => '1', 'attivo' => true]);
|
||||
|
||||
Livewire::test(XmlExchange::class)
|
||||
->set('pdfFolder', [UploadedFile::fake()->createWithContent('1.pdf', self::PDF)])
|
||||
->call('importTerritoryPdfFolder')
|
||||
->assertSet('pdfImportStatus', 'completed');
|
||||
|
||||
$this->assertNotNull($uno->fresh()->pdf_path);
|
||||
$this->assertNull(app(BackupOperationState::class)->corrente());
|
||||
}
|
||||
|
||||
/**
|
||||
* failed() e' quello che Laravel chiama quando il worker uccide il job per
|
||||
* timeout: handle() non arriva a liberare niente.
|
||||
*/
|
||||
public function test_un_import_pdf_ucciso_dal_worker_libera_il_posto(): void
|
||||
{
|
||||
app(TerritorioPdfImportDispatcher::class)->occupaPosto('import-1');
|
||||
$this->assertSame('import-1', app(BackupOperationState::class)->corrente());
|
||||
|
||||
(new ImportTerritoryPdfFolder('import-1', [], $this->admin->id))
|
||||
->failed(new \RuntimeException('Job has timed out.'));
|
||||
|
||||
$this->assertNull(app(BackupOperationState::class)->corrente());
|
||||
}
|
||||
|
||||
public function test_l_import_xml_libera_il_posto_quando_riesce_e_quando_fallisce(): void
|
||||
{
|
||||
$this->post(route('xml.import-xml'), ['xmlImport' => $this->xml('1')])
|
||||
->assertSessionHasNoErrors();
|
||||
$this->assertNull(app(BackupOperationState::class)->corrente());
|
||||
|
||||
$this->post(route('xml.import-xml'), [
|
||||
'xmlImport' => UploadedFile::fake()->createWithContent('rotto.xml', '<?xml version="1.0"?><sitemap/>'),
|
||||
])->assertSessionHasErrors('xmlImport');
|
||||
$this->assertNull(app(BackupOperationState::class)->corrente());
|
||||
}
|
||||
|
||||
public function test_uno_zip_senza_pdf_libera_il_posto(): void
|
||||
{
|
||||
// L'estrazione legge lo ZIP da storage_path(), non dal disco finto.
|
||||
Storage::forgetDisk('local');
|
||||
|
||||
$this->postJson(route('imports.territori.pdf-zip'), ['pdfZip' => $this->zip(['Thumbs.db' => 'x', '.DS_Store' => 'x'])])
|
||||
->assertStatus(422)
|
||||
->assertJsonFragment(['message' => 'Lo ZIP non contiene file PDF validi.']);
|
||||
|
||||
$this->assertNull(app(BackupOperationState::class)->corrente());
|
||||
}
|
||||
|
||||
// ─── Presa del posto ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* La presa e' un passo solo: chi arriva secondo trova il posto occupato
|
||||
* anche se il primo non ha ancora finito di avviare il proprio lavoro.
|
||||
*/
|
||||
public function test_chi_arriva_secondo_trova_il_posto_occupato(): void
|
||||
{
|
||||
$stato = app(BackupOperationState::class);
|
||||
|
||||
$this->assertNull($stato->occupa('primo', BackupOperationState::TIPO_IMPORT_PDF, 'In coda'));
|
||||
$this->assertSame('primo', $stato->occupa('secondo', BackupOperationState::TIPO_RESTORE, 'In coda'));
|
||||
$this->assertNull($stato->get('secondo'), 'Chi non prende il posto non deve lasciare un\'operazione fantasma.');
|
||||
|
||||
$stato->markCompleted('primo');
|
||||
$this->assertNull($stato->occupa('terzo', BackupOperationState::TIPO_BACKUP, 'In coda'));
|
||||
}
|
||||
|
||||
// ─── Supporto ───────────────────────────────────────────────
|
||||
|
||||
private function xml(string $numero): UploadedFile
|
||||
{
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><termanager2 version="1.0">'
|
||||
. '<settings/><users/><zones/><tipologie/>'
|
||||
. '<proclamatori/>'
|
||||
. '<territori><territorio legacy_id="1"><numero>' . $numero . '</numero><attivo>1</attivo></territorio></territori>'
|
||||
. '<anni_teocratici/><campagne/><assegnazioni/>'
|
||||
. '</termanager2>';
|
||||
|
||||
return UploadedFile::fake()->createWithContent('export.xml', $xml);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user