Editor piantine: bozza automatica e pubblicazione con anteprima
- Il disegno è bozza e si salva da solo; i dati del territorio si modificano dall'editor e valgono subito nel gestionale. - Un solo "Pubblica" (immagine + PDF), uguale in editor e gestione: anteprima preparata in coda su disco privato, conferma obbligatoria, rifiutata se nel frattempo è cambiato qualcosa di stampato. - Stati Bozza / Pubblicata / Modifiche da pubblicare, calcolati sull'impronta di confine e dati stampati e sul PDF in uso. - Gestione riscritta: stato, ricerca, filtri, storico paginato. - Editor ridisegnato a tre zone, utilizzabile anche su tablet e telefono. - Avvisi via evento (trait Avvisa + <x-avvisi>). - .gitattributes: script shell sempre LF, altrimenti il container locale su Windows non parte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
286
tests/Feature/PubblicazionePiantinaTest.php
Normal file
286
tests/Feature/PubblicazionePiantinaTest.php
Normal file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\Piantine\Pubblicazione;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\TerritorioMappa;
|
||||
use App\Models\TerritorioPdfVersione;
|
||||
use App\Models\User;
|
||||
use App\Models\Zona;
|
||||
use App\Services\Piantine\PubblicazionePiantina;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
use RuntimeException;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Dalla bozza al PDF ufficiale: l'anteprima non tocca niente, la conferma
|
||||
* pubblica esattamente quello che si è visto, o niente.
|
||||
*/
|
||||
class PubblicazionePiantinaTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const GEOMETRIA = [
|
||||
'type' => 'Polygon',
|
||||
'coordinates' => [[[13.35, 38.11], [13.37, 38.11], [13.37, 38.13], [13.35, 38.11]]],
|
||||
];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutVite();
|
||||
Storage::fake('public');
|
||||
Storage::fake('local');
|
||||
config(['piantine.formato' => 'jpeg']);
|
||||
|
||||
// Un JPEG vero: FPDF lo legge davvero quando compone il PDF.
|
||||
$immagine = imagecreatetruecolor(80, 50);
|
||||
imagefilledrectangle($immagine, 0, 0, 79, 49, imagecolorallocate($immagine, 230, 230, 230));
|
||||
ob_start();
|
||||
imagejpeg($immagine);
|
||||
$jpeg = ob_get_clean();
|
||||
imagedestroy($immagine);
|
||||
|
||||
$this->jpeg = $jpeg;
|
||||
}
|
||||
|
||||
private string $jpeg;
|
||||
|
||||
private function rendererRisponde(): void
|
||||
{
|
||||
Http::fake(['*/render' => Http::response($this->jpeg)]);
|
||||
}
|
||||
|
||||
public function test_l_anteprima_si_prepara_senza_toccare_territorio_ne_piantina(): void
|
||||
{
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
$servizio = app(PubblicazionePiantina::class);
|
||||
|
||||
// In test la coda è sincrona: il job ha già lavorato al ritorno.
|
||||
$token = $servizio->avvia($mappa);
|
||||
|
||||
$this->assertSame(PubblicazionePiantina::PRONTA, $servizio->stato($token)['stato']);
|
||||
$this->assertStringStartsWith('%PDF', file_get_contents($servizio->pdfAnteprima($token)));
|
||||
|
||||
$this->assertNull($territorio->fresh()->pdf_path);
|
||||
$this->assertNull($mappa->fresh()->pubblicata_at);
|
||||
$this->assertNull($mappa->fresh()->image_path);
|
||||
$this->assertSame(TerritorioMappa::STATO_BOZZA, $mappa->fresh()->stato());
|
||||
}
|
||||
|
||||
public function test_il_pdf_dell_anteprima_si_vede_solo_con_il_token_giusto(): void
|
||||
{
|
||||
[, $mappa] = $this->piantinaCollegata();
|
||||
$token = app(PubblicazionePiantina::class)->avvia($mappa);
|
||||
|
||||
$this->actingAs($this->admin())
|
||||
->get(route('piantine.anteprima', $token))
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'application/pdf');
|
||||
|
||||
$this->actingAs($this->admin())
|
||||
->get(route('piantine.anteprima', str_repeat('a', 40)))
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_la_conferma_sostituisce_il_pdf_e_archivia_il_precedente(): void
|
||||
{
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
Storage::disk('public')->put('territori-pdf/vecchio.pdf', '%PDF-vecchio');
|
||||
$territorio->update(['pdf_path' => 'territori-pdf/vecchio.pdf']);
|
||||
|
||||
$servizio = app(PubblicazionePiantina::class);
|
||||
$token = $servizio->avvia($mappa);
|
||||
|
||||
$this->actingAs($this->admin());
|
||||
$servizio->conferma($token);
|
||||
|
||||
$territorio->refresh();
|
||||
$mappa->refresh();
|
||||
|
||||
$this->assertStringStartsWith('territori-pdf/piantina-' . $territorio->id . '-', $territorio->pdf_path);
|
||||
Storage::disk('public')->assertExists($territorio->pdf_path);
|
||||
$this->assertSame(1, TerritorioPdfVersione::where('territorio_id', $territorio->id)->count());
|
||||
|
||||
$this->assertNotNull($mappa->image_path);
|
||||
Storage::disk('public')->assertExists($mappa->image_path);
|
||||
$this->assertSame(TerritorioMappa::STATO_PUBBLICATA, $mappa->stato());
|
||||
$this->assertSame($mappa->geojson, $mappa->geojson_pubblicato);
|
||||
|
||||
// L'anteprima confermata non resta in giro.
|
||||
$this->assertNull($servizio->stato($token));
|
||||
$this->assertSame([], Storage::disk('local')->files('piantine-anteprime'));
|
||||
}
|
||||
|
||||
public function test_se_la_piantina_cambia_dopo_l_anteprima_la_conferma_viene_rifiutata(): void
|
||||
{
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
$servizio = app(PubblicazionePiantina::class);
|
||||
$token = $servizio->avvia($mappa);
|
||||
|
||||
// I confini sono stampati sulla cartolina: cambiarli rende vecchia l'anteprima.
|
||||
$territorio->update(['confini' => 'Nord: via Nuova']);
|
||||
|
||||
try {
|
||||
$servizio->conferma($token);
|
||||
$this->fail('La conferma doveva essere rifiutata.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->assertStringContainsString('cambiat', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->assertNull($territorio->fresh()->pdf_path);
|
||||
$this->assertNull($mappa->fresh()->pubblicata_at);
|
||||
}
|
||||
|
||||
public function test_una_piantina_scollegata_non_si_pubblica(): void
|
||||
{
|
||||
$mappa = TerritorioMappa::create(['geojson' => json_encode(self::GEOMETRIA)]);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
app(PubblicazionePiantina::class)->avvia($mappa);
|
||||
}
|
||||
|
||||
public function test_un_renderer_che_non_risponde_lascia_un_errore_leggibile(): void
|
||||
{
|
||||
Http::fake(['*/render' => Http::response(['error' => 'area troppo estesa'], 413)]);
|
||||
[, $mappa] = $this->piantinaCollegata(renderer: false);
|
||||
|
||||
$servizio = app(PubblicazionePiantina::class);
|
||||
$token = $servizio->avvia($mappa);
|
||||
|
||||
$stato = $servizio->stato($token);
|
||||
$this->assertSame(PubblicazionePiantina::ERRORE, $stato['stato']);
|
||||
$this->assertStringContainsString('area troppo estesa', $stato['messaggio']);
|
||||
$this->assertNull($servizio->pdfAnteprima($token));
|
||||
}
|
||||
|
||||
public function test_annullare_butta_l_anteprima(): void
|
||||
{
|
||||
[, $mappa] = $this->piantinaCollegata();
|
||||
$servizio = app(PubblicazionePiantina::class);
|
||||
$token = $servizio->avvia($mappa);
|
||||
|
||||
$servizio->annulla($token);
|
||||
|
||||
$this->assertNull($servizio->stato($token));
|
||||
$this->assertSame([], Storage::disk('local')->files('piantine-anteprime'));
|
||||
}
|
||||
|
||||
public function test_la_finestra_porta_dalla_richiesta_alla_conferma(): void
|
||||
{
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
|
||||
Livewire::actingAs($this->admin())
|
||||
->test(Pubblicazione::class)
|
||||
->dispatch('pubblica-piantina', mappaId: $mappa->id)
|
||||
->assertSet('fase', PubblicazionePiantina::PRONTA)
|
||||
->assertSee('Anteprima del PDF da pubblicare', false)
|
||||
->call('conferma')
|
||||
->assertSet('mappaId', null)
|
||||
->assertDispatched('piantina-pubblicata', mappaId: $mappa->id);
|
||||
|
||||
$this->assertSame(TerritorioMappa::STATO_PUBBLICATA, $mappa->fresh()->stato());
|
||||
$this->assertNotNull($territorio->fresh()->pdf_path);
|
||||
}
|
||||
|
||||
public function test_chiudere_la_finestra_non_pubblica_niente(): void
|
||||
{
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
|
||||
Livewire::actingAs($this->admin())
|
||||
->test(Pubblicazione::class)
|
||||
->dispatch('pubblica-piantina', mappaId: $mappa->id)
|
||||
->call('chiudi')
|
||||
->assertSet('mappaId', null);
|
||||
|
||||
$this->assertNull($territorio->fresh()->pdf_path);
|
||||
$this->assertSame([], Storage::disk('local')->files('piantine-anteprime'));
|
||||
}
|
||||
|
||||
// ─── Stati del modello ──────────────────────────────────────────────────
|
||||
|
||||
public function test_dopo_la_pubblicazione_ogni_dato_stampato_riporta_la_piantina_da_pubblicare(): void
|
||||
{
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
$servizio = app(PubblicazionePiantina::class);
|
||||
$this->actingAs($this->admin());
|
||||
$servizio->conferma($servizio->avvia($mappa));
|
||||
|
||||
$this->assertSame(TerritorioMappa::STATO_PUBBLICATA, $mappa->fresh()->stato());
|
||||
|
||||
$territorio->update(['zona_id' => Zona::create(['nome' => 'Altra', 'attivo' => true])->id]);
|
||||
$this->assertSame(TerritorioMappa::STATO_MODIFICATA, $mappa->fresh()->stato());
|
||||
}
|
||||
|
||||
public function test_un_pdf_sostituito_per_altre_vie_riporta_la_piantina_da_pubblicare(): void
|
||||
{
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
$servizio = app(PubblicazionePiantina::class);
|
||||
$this->actingAs($this->admin());
|
||||
$servizio->conferma($servizio->avvia($mappa));
|
||||
|
||||
$territorio->update(['pdf_path' => 'territori-pdf/caricato-a-mano.pdf']);
|
||||
|
||||
$this->assertSame(TerritorioMappa::STATO_MODIFICATA, $mappa->fresh()->stato());
|
||||
}
|
||||
|
||||
public function test_motivo_non_pubblicabile_dice_cosa_manca(): void
|
||||
{
|
||||
$senzaDisegno = TerritorioMappa::create(['geojson' => 'rotto']);
|
||||
$this->assertStringContainsString('Disegna', $senzaDisegno->motivoNonPubblicabile());
|
||||
|
||||
$scollegata = TerritorioMappa::create(['geojson' => json_encode(self::GEOMETRIA)]);
|
||||
$this->assertStringContainsString('Collega', $scollegata->motivoNonPubblicabile());
|
||||
|
||||
[$territorio, $mappa] = $this->piantinaCollegata();
|
||||
$this->assertNull($mappa->motivoNonPubblicabile());
|
||||
|
||||
$territorio->delete();
|
||||
$this->assertStringContainsString('cestino', $mappa->fresh()->motivoNonPubblicabile());
|
||||
$this->assertSame('Terr. 7', $mappa->fresh()->etichetta());
|
||||
}
|
||||
|
||||
private function piantinaCollegata(bool $renderer = true): array
|
||||
{
|
||||
if ($renderer) {
|
||||
$this->rendererRisponde();
|
||||
}
|
||||
|
||||
$zona = Zona::firstOrCreate(['nome' => 'Centro'], ['attivo' => true]);
|
||||
$territorio = Territorio::create([
|
||||
'numero' => '7',
|
||||
'attivo' => true,
|
||||
'zona_id' => $zona->id,
|
||||
'confini' => 'Nord: via Roma',
|
||||
]);
|
||||
$mappa = TerritorioMappa::create([
|
||||
'territorio_id' => $territorio->id,
|
||||
'geojson' => json_encode(self::GEOMETRIA),
|
||||
]);
|
||||
|
||||
return [$territorio, $mappa];
|
||||
}
|
||||
|
||||
private function admin(): User
|
||||
{
|
||||
Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
|
||||
Setting::firstOrCreate([], ['congregazione_nome' => 'Congregazione di prova']);
|
||||
|
||||
$utente = User::firstOrCreate(
|
||||
['email' => 'admin@example.test'],
|
||||
['name' => 'Amministratore', 'password' => 'password-di-prova'],
|
||||
);
|
||||
$utente->assignRole('amministratore');
|
||||
|
||||
return $utente;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user