Le piantine non dipendono piu' da un territorio esistente: territorio_id e' nullable, la chiave esterna e' nullOnDelete invece di cascadeOnDelete, e `nome` fa da etichetta finche' il collegamento manca. Si disegna guardando le strade e si decide dopo di chi e' il disegno; se il territorio non c'e', lo si crea dall'editor con numero, zona, tipologia e confini — zona e tipologia comprese, create sul posto quando mancano. Il cambio di cascata chiude anche una perdita di dati silenziosa: ogni import XML svuota `territori` in blocco e finora si portava via tutte le piantine. Il disegno passa su /piantine/editor, a schermo intero con telaio proprio: su una mappa larga un terzo di schermo un confine si sbaglia, e finisce stampato. /piantine resta la gestione — area, generazione, collegamenti, storico. Cartolina S-12 reimpaginata: pagina 1 il modulo con la Localita' (che ora e' il nome della zona, non un campo duplicato) e i confini scritti nello spazio centrale, con il corpo del testo che si adatta alla lunghezza; pagina 2 la piantina sola al 98%. Aggiunta l'anteprima, che compone il PDF al volo senza toccare pdf_path ne' lo storico. Immagine: JPEG invece di PNG (0,9 MB contro 5 per territorio), composta alle tessere dello zoom piu' alto e ridotta con Lanczos, nelle proporzioni della pagina cosi' riempie il foglio invece di lasciare fasce bianche. Il tratto del confine e' espresso in pixel dell'immagine finale: si vede il confine e si legge la via che ci passa sotto. L'attribuzione OSM si dimensiona con l'immagine, come la ODbL richiede. Rimossa la rigenerazione massiva: job, stato in cache e interfaccia. Sistema visivo ripassato su tutta l'applicazione: le scale gray e indigo di Tailwind sono ridefinite nei token, cosi' le 37 viste si riallineano insieme invece di essere riscritte una per una. Grigi caldi, un accento solo con quattro significati, carattere servito dall'applicazione e non da Google Fonts, via il gradiente della testata. Aggiunto il link "salta al contenuto", che mancava. Corretto un guasto d'ambiente che bloccava TUTTI gli upload in sviluppo: dalla 1.26 il fastcgi_params di nginx passa HTTP_HOST come $host, che perde la porta, e ogni URL firmato smetteva di validare — l'endpoint di upload di Livewire rispondeva 401. 294 test verdi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
213 lines
7.5 KiB
PHP
213 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Models\Territorio;
|
|
use App\Models\TerritorioMappa;
|
|
use App\Models\TerritorioPdfVersione;
|
|
use App\Services\Piantine\PiantinaPdfService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use RuntimeException;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Composizione del PDF sul modulo S-12 e giro completo dello storico:
|
|
* sostituzione, ripristino, e i casi in cui manca qualcosa.
|
|
*/
|
|
class PiantinaPdfServiceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Storage::fake('public');
|
|
Storage::fake('local');
|
|
}
|
|
|
|
/** Un territorio con immagine della piantina gia' renderizzata. */
|
|
private function territorioConImmagine(): array
|
|
{
|
|
$territorio = Territorio::create([
|
|
'numero' => '7',
|
|
'attivo' => true,
|
|
'confini' => "Nord: via Roma\nSud: via Milano",
|
|
]);
|
|
|
|
$mappa = TerritorioMappa::create([
|
|
'territorio_id' => $territorio->id,
|
|
'geojson' => '{"type":"Polygon","coordinates":[[[1,1],[2,1],[2,2],[1,1]]]}',
|
|
'image_path' => 'territori-mappe/territorio-' . $territorio->id . '.png',
|
|
'rendered_at' => now(),
|
|
]);
|
|
|
|
// Un PNG vero, piccolo: FPDF lo legge davvero, un finto no.
|
|
$immagine = imagecreatetruecolor(80, 50);
|
|
imagefilledrectangle($immagine, 0, 0, 79, 49, imagecolorallocate($immagine, 230, 230, 230));
|
|
ob_start();
|
|
imagepng($immagine);
|
|
Storage::disk('public')->put($mappa->image_path, ob_get_clean());
|
|
imagedestroy($immagine);
|
|
|
|
return [$territorio, $mappa];
|
|
}
|
|
|
|
private function servizio(): PiantinaPdfService
|
|
{
|
|
return app(PiantinaPdfService::class);
|
|
}
|
|
|
|
public function test_genera_produce_un_pdf_di_due_pagine(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
|
|
$pdf = $this->servizio()->genera($territorio, $mappa);
|
|
|
|
$this->assertStringStartsWith('%PDF', $pdf);
|
|
// FPDF conta le pagine nel catalogo: /Count 2.
|
|
$this->assertStringContainsString('/Count 2', $pdf);
|
|
}
|
|
|
|
/**
|
|
* La "Località" del modulo è il nome della zona: un dato solo, in un posto
|
|
* solo, che si aggiorna quando la congregazione riorganizza il lavoro.
|
|
*/
|
|
public function test_la_localita_stampata_e_il_nome_della_zona(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
$zona = \App\Models\Zona::create(['nome' => 'Palermo Montepellegrino', 'attivo' => true]);
|
|
$territorio->update(['zona_id' => $zona->id]);
|
|
|
|
$testo = $this->testoDel($this->servizio()->genera($territorio->fresh(), $mappa));
|
|
|
|
$this->assertStringContainsString('Palermo Montepellegrino', $testo);
|
|
}
|
|
|
|
public function test_senza_zona_l_intestazione_resta_vuota_senza_rompere_nulla(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
|
|
$pdf = $this->servizio()->genera($territorio, $mappa);
|
|
|
|
$this->assertStringStartsWith('%PDF', $pdf);
|
|
}
|
|
|
|
/** I confini vanno sulla cartolina, non su una pagina a parte. */
|
|
public function test_i_confini_finiscono_nel_pdf(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
|
|
$testo = $this->testoDel($this->servizio()->genera($territorio, $mappa));
|
|
|
|
$this->assertStringContainsString('Nord: via Roma', $testo);
|
|
$this->assertStringContainsString('Sud: via Milano', $testo);
|
|
}
|
|
|
|
public function test_senza_confini_il_pdf_lo_dichiara(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
$territorio->update(['confini' => null]);
|
|
|
|
$testo = $this->testoDel($this->servizio()->genera($territorio->refresh(), $mappa));
|
|
|
|
$this->assertStringContainsString('Confini non ancora descritti', $testo);
|
|
}
|
|
|
|
/**
|
|
* Il testo scritto da FPDF vive dentro i flussi compressi delle pagine:
|
|
* per leggerlo vanno estratti e decompressi.
|
|
*/
|
|
private function testoDel(string $pdf): string
|
|
{
|
|
$testo = '';
|
|
|
|
if (preg_match_all('/stream\r?\n(.*?)\r?\nendstream/s', $pdf, $trovati)) {
|
|
foreach ($trovati[1] as $flusso) {
|
|
$decompresso = @gzuncompress($flusso);
|
|
if ($decompresso !== false) {
|
|
$testo .= $decompresso;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $testo;
|
|
}
|
|
|
|
public function test_genera_senza_immagine_si_ferma_con_un_messaggio_chiaro(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
Storage::disk('public')->delete($mappa->image_path);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
$this->servizio()->genera($territorio, $mappa);
|
|
}
|
|
|
|
public function test_sostituisci_archivia_il_pdf_precedente_nello_storico(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
Storage::disk('public')->put('territori-pdf/vecchio.pdf', '%PDF-vecchio');
|
|
$territorio->update(['pdf_path' => 'territori-pdf/vecchio.pdf']);
|
|
|
|
$this->servizio()->sostituisci($territorio, $mappa);
|
|
|
|
$territorio->refresh();
|
|
$this->assertNotSame('territori-pdf/vecchio.pdf', $territorio->pdf_path);
|
|
Storage::disk('public')->assertExists($territorio->pdf_path);
|
|
Storage::disk('public')->assertMissing('territori-pdf/vecchio.pdf');
|
|
|
|
$versione = TerritorioPdfVersione::sole();
|
|
$this->assertSame($territorio->id, $versione->territorio_id);
|
|
Storage::disk('public')->assertExists($versione->pdf_path);
|
|
$this->assertSame('%PDF-vecchio', Storage::disk('public')->get($versione->pdf_path));
|
|
}
|
|
|
|
public function test_sostituisci_senza_pdf_precedente_non_crea_versioni(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
|
|
$this->servizio()->sostituisci($territorio, $mappa);
|
|
|
|
$this->assertNotNull($territorio->refresh()->pdf_path);
|
|
$this->assertSame(0, TerritorioPdfVersione::count());
|
|
}
|
|
|
|
public function test_ripristina_rimette_in_vigore_la_versione_e_archivia_l_attuale(): void
|
|
{
|
|
[$territorio, $mappa] = $this->territorioConImmagine();
|
|
Storage::disk('public')->put('territori-pdf/vecchio.pdf', '%PDF-vecchio');
|
|
$territorio->update(['pdf_path' => 'territori-pdf/vecchio.pdf']);
|
|
|
|
$this->servizio()->sostituisci($territorio, $mappa);
|
|
$versione = TerritorioPdfVersione::sole();
|
|
|
|
$this->servizio()->ripristina($versione);
|
|
|
|
$territorio->refresh();
|
|
$this->assertSame('%PDF-vecchio', Storage::disk('public')->get($territorio->pdf_path));
|
|
|
|
// La versione usata e' sparita; al suo posto c'e' quella del PDF che
|
|
// era in vigore, quindi il ripristino resta a sua volta reversibile.
|
|
$this->assertSame(0, TerritorioPdfVersione::whereKey($versione->id)->count());
|
|
$this->assertSame(1, TerritorioPdfVersione::count());
|
|
$this->assertSame('ripristino', TerritorioPdfVersione::sole()->origine);
|
|
}
|
|
|
|
public function test_ripristina_con_file_sparito_si_ferma_con_un_messaggio_chiaro(): void
|
|
{
|
|
[$territorio] = $this->territorioConImmagine();
|
|
|
|
$versione = TerritorioPdfVersione::create([
|
|
'territorio_id' => $territorio->id,
|
|
'pdf_path' => 'territori-pdf-versioni/inesistente.pdf',
|
|
'origine' => 'sostituzione',
|
|
'sostituito_il' => now(),
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
$this->servizio()->ripristina($versione);
|
|
}
|
|
}
|