++ fix e controlli

This commit is contained in:
2026-07-30 17:05:39 +02:00
parent 712336d652
commit 440d63a6b9
54 changed files with 4921 additions and 1322 deletions

View File

@@ -0,0 +1,264 @@
<?php
namespace Tests\Feature;
use App\Jobs\CreateBackupArchive;
use App\Jobs\ResetApplication;
use App\Livewire\Settings\BackupManager;
use App\Livewire\Settings\ZonaPericolosa;
use App\Models\User;
use App\Services\BackupOperationState;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
use Tests\TestCase;
/**
* L'azzeramento: l'unica operazione irreversibile dell'applicazione.
*
* Questi test esistono perché il flusso è stato spostato da BackupManager a un
* componente suo, per poterlo mettere in fondo alla pagina invece che a metà. Lo
* spostamento non doveva cambiare niente di quello che protegge: i due tempi
* separati dal download, la password richiesta al secondo, il limite ai
* tentativi, il posto unico. Sono esattamente i controlli di cui non ci si
* accorge che si sono rotti finché non servono.
*/
class ZonaPericolosaTest extends TestCase
{
use RefreshDatabase;
private const PASSWORD = 'password-di-prova';
private User $admin;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
Role::create(['name' => 'amministratore', 'guard_name' => 'web']);
app(PermissionRegistrar::class)->forgetCachedPermissions();
$this->admin = User::create([
'name' => 'Amministratore',
'email' => 'admin@example.test',
'password' => self::PASSWORD,
]);
$this->admin->assignRole('amministratore');
$this->actingAs($this->admin);
}
// ─── Primo tempo ────────────────────────────────────────────
public function test_senza_la_frase_esatta_non_parte_niente(): void
{
Livewire::test(ZonaPericolosa::class)
->set('confermaReset', 'azzera')
->call('avviaAzzeramento')
->assertHasErrors('confermaReset');
Queue::assertNothingPushed();
$this->assertNull(app(BackupOperationState::class)->corrente());
}
public function test_la_frase_esatta_avvia_il_backup_preventivo_e_niente_altro(): void
{
Livewire::test(ZonaPericolosa::class)
->set('confermaReset', ZonaPericolosa::CONFERMA_AZZERAMENTO)
->call('avviaAzzeramento')
->assertHasNoErrors();
// Il primo tempo crea solo l'archivio: la cancellazione arriva dopo, e
// solo su conferma umana.
Queue::assertPushed(CreateBackupArchive::class);
Queue::assertNotPushed(ResetApplication::class);
$stato = app(BackupOperationState::class);
$corrente = $stato->corrente();
$this->assertNotNull($corrente, 'L\'azzeramento deve occupare il posto unico.');
$this->assertSame(BackupOperationState::TIPO_RESET, $stato->get($corrente)['tipo']);
}
public function test_la_frase_di_conferma_si_svuota_dopo_l_avvio(): void
{
Livewire::test(ZonaPericolosa::class)
->set('confermaReset', ZonaPericolosa::CONFERMA_AZZERAMENTO)
->call('avviaAzzeramento')
->assertSet('confermaReset', '');
}
public function test_un_backup_gia_in_corso_impedisce_l_azzeramento(): void
{
app(BackupOperationState::class)->initialize('op-backup', BackupOperationState::TIPO_BACKUP, 'Compressione', 10);
Livewire::test(ZonaPericolosa::class)
->set('confermaReset', ZonaPericolosa::CONFERMA_AZZERAMENTO)
->call('avviaAzzeramento');
Queue::assertNotPushed(CreateBackupArchive::class);
$this->assertSame('op-backup', app(BackupOperationState::class)->corrente());
}
// ─── Secondo tempo ──────────────────────────────────────────
/**
* Il controllo che regge tutto il disegno a due tempi: senza la prova che
* l'archivio è stato spedito, la cancellazione non parte. Il server non può
* sapere se il file è *arrivato* — nginx bufferizza la risposta e un
* download annullato è indistinguibile da uno riuscito — ma può almeno
* sapere di averlo mandato.
*/
public function test_senza_archivio_servito_la_conferma_non_cancella_niente(): void
{
$this->operazioneInAttesa(servito: false);
Livewire::test(ZonaPericolosa::class)
->call('confermaAzzeramento', self::PASSWORD)
->assertHasErrors('confermaReset');
Queue::assertNotPushed(ResetApplication::class);
}
public function test_senza_password_la_conferma_non_cancella_niente(): void
{
$this->operazioneInAttesa();
Livewire::test(ZonaPericolosa::class)
->call('confermaAzzeramento', '')
->assertHasErrors('passwordConferma');
Queue::assertNotPushed(ResetApplication::class);
}
public function test_con_la_password_sbagliata_la_conferma_non_cancella_niente(): void
{
$this->operazioneInAttesa();
Livewire::test(ZonaPericolosa::class)
->call('confermaAzzeramento', 'non-e-la-mia-password')
->assertHasErrors('passwordConferma');
Queue::assertNotPushed(ResetApplication::class);
}
public function test_con_la_password_giusta_l_azzeramento_parte(): void
{
$this->operazioneInAttesa();
Livewire::test(ZonaPericolosa::class)
->call('confermaAzzeramento', self::PASSWORD)
->assertHasNoErrors();
Queue::assertPushed(ResetApplication::class);
}
/**
* Senza limite ai tentativi, chiedere la password su una sessione dirottata
* sarebbe solo un rallentamento: si proverebbero le password comuni a
* raffica, e il resto della pagina è già raggiungibile.
*/
public function test_cinque_tentativi_sbagliati_bloccano_l_operazione(): void
{
$this->operazioneInAttesa();
for ($tentativo = 1; $tentativo <= 5; $tentativo++) {
Livewire::test(ZonaPericolosa::class)->call('confermaAzzeramento', 'sbagliata');
}
// Il sesto tentativo è rifiutato anche se la password è quella giusta.
Livewire::test(ZonaPericolosa::class)
->call('confermaAzzeramento', self::PASSWORD)
->assertHasErrors('passwordConferma');
Queue::assertNotPushed(ResetApplication::class);
}
public function test_annullare_libera_il_posto_e_lascia_il_backup_scaricabile(): void
{
$this->operazioneInAttesa();
Livewire::test(ZonaPericolosa::class)->call('annullaAzzeramento');
$stato = app(BackupOperationState::class);
$this->assertNull($stato->corrente(), 'Annullando, il posto deve tornare libero.');
Queue::assertNotPushed(ResetApplication::class);
// Da qui in poi è un backup come un altro: l'archivio resta valido, e la
// zona di backup deve poterlo offrire in download.
$finale = $stato->get('op-reset');
$this->assertSame(BackupOperationState::TIPO_BACKUP, $finale['tipo']);
$this->assertSame('completed', $finale['status']);
}
// ─── Console e proprietà dell'operazione ────────────────────
/**
* Ogni operazione ha un solo componente che la racconta. Senza, backup e
* zona pericolosa spedirebbero entrambi le stesse righe e la console le
* mostrerebbe due volte.
*/
public function test_la_zona_pericolosa_racconta_solo_i_propri_azzeramenti(): void
{
$stato = app(BackupOperationState::class);
$stato->initialize('op-backup', BackupOperationState::TIPO_BACKUP, 'Compressione', 4);
$stato->log('op-backup', 'Riga di un backup, non di un azzeramento.');
Livewire::test(ZonaPericolosa::class)->assertNotDispatched('console-operazione');
Livewire::test(BackupManager::class)->assertDispatched('console-operazione');
}
/**
* Il difetto che questo test presidia: chi non possiede l'operazione polla
* comunque, e consumerebbe per primo il segno "download già inviato" —
* spegnendo l'evento prima che il proprietario lo emetta. L'archivio
* sarebbe pronto e non arriverebbe a nessuno, e per l'azzeramento è pure il
* passo che sblocca la cancellazione.
*/
public function test_il_download_lo_emette_solo_chi_possiede_l_operazione(): void
{
$this->operazioneInAttesa(servito: false, downloadInviato: false);
Livewire::test(BackupManager::class)->assertNotDispatched('scarica-backup');
$this->assertFalse(
app(BackupOperationState::class)->get('op-reset')['result']['download_inviato'] ?? false,
'Il componente che non possiede l\'operazione non deve consumare il segno del download.'
);
Livewire::test(ZonaPericolosa::class)->assertDispatched('scarica-backup');
}
public function test_il_download_si_emette_una_volta_sola(): void
{
$this->operazioneInAttesa(servito: false, downloadInviato: false);
Livewire::test(ZonaPericolosa::class)->assertDispatched('scarica-backup');
Livewire::test(ZonaPericolosa::class)->assertNotDispatched('scarica-backup');
}
// ─── helper ─────────────────────────────────────────────────
/**
* Azzeramento fermo al secondo tempo: l'archivio esiste e, salvo diversa
* indicazione, risulta spedito.
*/
private function operazioneInAttesa(bool $servito = true, bool $downloadInviato = true): void
{
$stato = app(BackupOperationState::class);
$stato->initialize('op-reset', BackupOperationState::TIPO_RESET, 'Compressione', 3);
$stato->markAttesaDownload('op-reset', [
'file' => '/tmp/backup.zip',
'nome_download' => 'backup.zip',
'dimensione' => 1024,
'servito_at' => $servito ? now()->toDateTimeString() : null,
'download_inviato' => $downloadInviato,
]);
}
}