@@ -0,0 +1,235 @@
< ? php
namespace Tests\Feature ;
use App\Models\AuditLog ;
use App\Models\Campaign ;
use App\Models\DirectoryGroup ;
use App\Models\DirectoryUser ;
use App\Models\Template ;
use App\Models\Tenant ;
use App\Models\User ;
use Illuminate\Foundation\Testing\RefreshDatabase ;
use Illuminate\Http\UploadedFile ;
use Illuminate\Support\Carbon ;
use Illuminate\Support\Facades\File ;
use Tests\TestCase ;
class AdminCampaignsTest extends TestCase
{
use RefreshDatabase ;
private Tenant $tenant ;
private DirectoryGroup $group ;
private string $dir ;
// PNG 1× 1 valide
private const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' ;
private const OIDS = [ 'editor' => '22222222-2222-2222-2222-222222222222' , 'admin' => '33333333-3333-3333-3333-333333333333' ];
protected function setUp () : void
{
parent :: setUp ();
$this -> dir = storage_path ( 'framework/testing/img-' . uniqid ());
config ([ 'signature.admin_allowed_cidrs' => [], 'signature.image_path' => $this -> dir , 'signature.timezone' => 'Europe/Paris' ]);
File :: ensureDirectoryExists ( $this -> dir );
file_put_contents ( $this -> dir . '/banniere.png' , base64_decode ( self :: PNG ));
$this -> tenant = Tenant :: create ([ 'name' => 'CABM' , 'entra_tenant_id' => '11111111-1111-1111-1111-111111111111' ]);
$this -> group = DirectoryGroup :: create ([ 'tenant_id' => $this -> tenant -> id , 'entra_oid' => '44444444-4444-4444-4444-444444444444' , 'display_name' => 'signature_mail_directeurs' ]);
}
protected function tearDown () : void
{
File :: deleteDirectory ( $this -> dir );
parent :: tearDown ();
}
private function user ( string $role ) : User
{
return User :: firstOrCreate ([ 'entra_oid' => self :: OIDS [ $role ]], [ 'name' => $role , 'email' => " { $role } @x.fr " , 'tenant_id' => $this -> tenant -> id , 'role' => $role ]);
}
private function payload ( array $over = []) : array
{
return array_merge ([
'name' => 'Encombrants' , 'image_path' => 'banniere.png' , 'alt_text' => 'Collecte des encombrants' ,
'link_url' => 'https://www.beziers-mediterranee.fr/encombrants' ,
'starts_at' => '2026-11-01T08:00' , 'ends_at' => '2026-11-15T18:00' , 'priority' => 20 ,
], $over );
}
private function campaign ( array $over = []) : Campaign
{
return Campaign :: create ( $over + [
'tenant_id' => $this -> tenant -> id , 'name' => 'Encombrants' , 'image_path' => 'banniere.png' , 'alt_text' => 'Collecte' ,
'starts_at' => now () -> subDay (), 'ends_at' => now () -> addDay (), 'priority' => 10 , 'active' => true ,
]);
}
private function agent ( int $n , bool $inGroup = false ) : DirectoryUser
{
$agent = DirectoryUser :: create ([
'tenant_id' => $this -> tenant -> id , 'entra_oid' => sprintf ( '55555555-5555-5555-5555-%012d' , $n ),
'display_name' => " Agent { $n } " , 'mail' => " agent { $n } @beziers-mediterranee.fr " , 'account_enabled' => true ,
]);
if ( $inGroup ) {
$this -> group -> members () -> attach ( $agent -> id );
}
return $agent ;
}
public function test_editeur_cree_une_campagne_ciblee_qui_reste_inactive () : void
{
$this -> actingAs ( $this -> user ( 'editor' ))
-> post ( '/admin/campagnes' , $this -> payload ([ 'groups' => [ $this -> group -> id ], 'active' => 1 ]))
-> assertRedirect ( route ( 'admin.campaigns.index' )) -> assertSessionHasNoErrors ();
$c = Campaign :: firstWhere ( 'name' , 'Encombrants' );
$this -> assertFalse ( $c -> active , 'seul un administrateur active une campagne' );
$this -> assertTrue ( $c -> starts_at -> equalTo ( Carbon :: parse ( '2026-11-01 08:00' , 'Europe/Paris' )), 'dates saisies en heure de Paris' );
$this -> assertTrue ( $c -> ends_at -> equalTo ( Carbon :: parse ( '2026-11-15 18:00' , 'Europe/Paris' )));
$this -> assertSame ( 1 , $c -> width , 'largeur automatique = largeur réelle de l\'image' );
$this -> assertSame ([ $this -> group -> id ], $c -> groups -> pluck ( 'id' ) -> all ());
$this -> assertTrue ( AuditLog :: where ( 'action' , 'campaign.create' ) -> exists ());
$this -> actingAs ( $this -> user ( 'editor' )) -> get ( '/admin/campagnes' ) -> assertOk () -> assertSee ( 'Encombrants' ) -> assertSee ( 'signature_mail_directeurs' );
}
public function test_image_envoyee_depuis_le_formulaire () : void
{
$tmp = tempnam ( sys_get_temp_dir (), 'png' );
file_put_contents ( $tmp , base64_decode ( self :: PNG ));
$this -> actingAs ( $this -> user ( 'admin' )) -> post ( '/admin/campagnes' , $this -> payload ([
'image_path' => null , 'image_file' => new UploadedFile ( $tmp , 'Banniere Noel 2026.png' , 'image/png' , null , true ),
'width' => 450 , 'active' => 1 ,
])) -> assertSessionHasNoErrors ();
$c = Campaign :: firstWhere ( 'name' , 'Encombrants' );
$this -> assertSame ( 'banniere-noel-2026.png' , $c -> image_path );
$this -> assertFileExists ( $this -> dir . '/banniere-noel-2026.png' );
$this -> assertSame ( 450 , $c -> width );
$this -> assertTrue ( $c -> active );
}
public function test_refuse_les_saisies_invalides () : void
{
$admin = $this -> user ( 'admin' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ([ 'ends_at' => '2026-10-01T08:00' ])) -> assertSessionHasErrors ( 'ends_at' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ([ 'link_url' => 'javascript:alert(1)' ])) -> assertSessionHasErrors ( 'link_url' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ([ 'image_path' => 'absente.png' ])) -> assertSessionHasErrors ( 'image_path' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ([ 'image_path' => '../banniere.png' ])) -> assertSessionHasErrors ( 'image_path' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ([ 'image_path' => null ])) -> assertSessionHasErrors ( 'image_path' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ([ 'alt_text' => '' ])) -> assertSessionHasErrors ( 'alt_text' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ([ 'groups' => [ 999 ]])) -> assertSessionHasErrors ( 'groups.0' );
$this -> assertSame ( 0 , Campaign :: count ());
}
public function test_activation_modification_et_suppression_reservees_a_l_admin () : void
{
$c = $this -> campaign ([ 'active' => false ]);
$editor = $this -> user ( 'editor' );
$admin = $this -> user ( 'admin' );
$this -> actingAs ( $editor ) -> post ( " /admin/campagnes/ { $c -> id } /activation " ) -> assertForbidden ();
$this -> actingAs ( $editor ) -> delete ( " /admin/campagnes/ { $c -> id } " ) -> assertForbidden ();
// Inactive : l'éditeur la modifie, sans pouvoir l'activer.
$this -> actingAs ( $editor ) -> put ( " /admin/campagnes/ { $c -> id } " , $this -> payload ([ 'name' => 'Préparée' , 'active' => 1 ])) -> assertSessionHasNoErrors ();
$this -> assertSame ( 'Préparée' , $c -> refresh () -> name );
$this -> assertFalse ( $c -> active );
$this -> actingAs ( $admin ) -> post ( " /admin/campagnes/ { $c -> id } /activation " ) -> assertSessionHasNoErrors ();
$this -> assertTrue ( $c -> refresh () -> active );
// Active : verrouillée pour l'éditeur.
$this -> actingAs ( $editor ) -> put ( " /admin/campagnes/ { $c -> id } " , $this -> payload ([ 'name' => 'Piratée' ])) -> assertForbidden ();
$this -> actingAs ( $editor ) -> get ( " /admin/campagnes/ { $c -> id } " ) -> assertOk () -> assertSee ( 'seul un administrateur peut la modifier' );
$this -> assertSame ( 'Préparée' , $c -> refresh () -> name );
$this -> actingAs ( $admin ) -> put ( " /admin/campagnes/ { $c -> id } " , $this -> payload ([ 'name' => 'Validée' , 'active' => 0 ])) -> assertSessionHasNoErrors ();
$this -> assertSame ( 'Validée' , $c -> refresh () -> name );
$this -> assertFalse ( $c -> active );
$this -> actingAs ( $admin ) -> delete ( " /admin/campagnes/ { $c -> id } " ) -> assertSessionHasNoErrors ();
$this -> assertSame ( 0 , Campaign :: count ());
$this -> assertFileExists ( $this -> dir . '/banniere.png' );
}
public function test_liste_indique_la_portee_reelle_et_les_templates_sans_banniere () : void
{
$this -> agent ( 1 );
$this -> agent ( 2 );
$this -> agent ( 3 , inGroup : true );
$this -> campaign ([ 'name' => 'Générale' , 'priority' => 10 ]);
$this -> campaign ([ 'name' => 'Directeurs' , 'priority' => 50 ]) -> groups () -> attach ( $this -> group -> id );
$this -> campaign ([ 'name' => 'Masquée' , 'priority' => 5 ]);
$this -> campaign ([ 'name' => 'Future' , 'priority' => 100 , 'starts_at' => now () -> addWeek (), 'ends_at' => now () -> addWeeks ( 2 )]);
$this -> campaign ([ 'name' => 'Passée' , 'priority' => 100 , 'starts_at' => now () -> subWeeks ( 2 ), 'ends_at' => now () -> subWeek ()]);
Template :: create ([ 'tenant_id' => $this -> tenant -> id , 'name' => 'Sobre' , 'draft_html_full' => '{{prenom}}' ]) -> publish ();
Template :: create ([ 'tenant_id' => $this -> tenant -> id , 'name' => 'Complet' , 'draft_html_full' => '{{prenom}}{{{banniere}}}' ]) -> publish ();
$this -> actingAs ( $this -> user ( 'admin' )) -> get ( '/admin/campagnes' ) -> assertOk ()
-> assertSeeInOrder ([ 'Directeurs' , 'vue par 1 agent' , 'Générale' , 'vue par 2 agents' , 'Masquée' , 'aucun agent' , 'Future' , 'Programmée' , 'Passée' , 'Terminée' ])
-> assertSee ( 'Sobre' )
-> assertDontSee ( 'Complet' );
}
public function test_largeur_automatique_suit_l_image_et_formulaire_reenregistrable () : void
{
$admin = $this -> user ( 'admin' );
$this -> actingAs ( $admin ) -> post ( '/admin/campagnes' , $this -> payload ()) -> assertSessionHasNoErrors ();
$c = Campaign :: firstWhere ( 'name' , 'Encombrants' );
$this -> assertSame ( 1 , $c -> width );
// Le formulaire d'édition laisse la largeur vide (automatique) : il se réenregistre tel quel.
$this -> actingAs ( $admin ) -> get ( " /admin/campagnes/ { $c -> id } " ) -> assertOk () -> assertSee ( 'min="100" max="800" value=""' , false );
$this -> actingAs ( $admin ) -> put ( " /admin/campagnes/ { $c -> id } " , $this -> payload ([ 'width' => null ])) -> assertSessionHasNoErrors ();
// Une largeur saisie est conservée et réaffichée.
$this -> actingAs ( $admin ) -> put ( " /admin/campagnes/ { $c -> id } " , $this -> payload ([ 'width' => 450 ])) -> assertSessionHasNoErrors ();
$this -> assertSame ( 450 , $c -> refresh () -> width );
$this -> actingAs ( $admin ) -> get ( " /admin/campagnes/ { $c -> id } " ) -> assertSee ( 'value="450"' , false );
}
public function test_messages_d_activation () : void
{
$admin = $this -> user ( 'admin' );
$running = $this -> campaign ([ 'active' => false ]);
$ended = $this -> campaign ([ 'name' => 'Passée' , 'starts_at' => now () -> subWeeks ( 2 ), 'ends_at' => now () -> subWeek ()]);
$this -> actingAs ( $admin ) -> post ( " /admin/campagnes/ { $running -> id } /activation " ) -> assertSessionHas ( 'status' , fn ( $m ) => str_contains ( $m , 'dès le prochain message' ));
$this -> actingAs ( $admin ) -> post ( " /admin/campagnes/ { $running -> id } /activation " ) -> assertSessionHas ( 'status' , fn ( $m ) => str_contains ( $m , 'désactivée' ));
$this -> actingAs ( $admin ) -> post ( " /admin/campagnes/ { $ended -> id } /activation " ) -> assertSessionHas ( 'status' , fn ( $m ) => str_contains ( $m , 'désactivée' ));
$this -> assertFalse ( $ended -> refresh () -> active );
}
public function test_image_endommagee_refusee_avec_un_message () : void
{
$tmp = tempnam ( sys_get_temp_dir (), 'png' );
file_put_contents ( $tmp , substr ( base64_decode ( self :: PNG ), 0 , 20 )); // en-tête PNG tronqué
$this -> actingAs ( $this -> user ( 'admin' )) -> post ( '/admin/campagnes' , $this -> payload ([
'image_path' => null , 'image_file' => new UploadedFile ( $tmp , 'cassee.png' , 'image/png' , null , true ),
])) -> assertSessionHasErrors ( 'image_file' );
$this -> assertSame ( 0 , Campaign :: count ());
}
public function test_cloisonnement_par_tenant () : void
{
$other = Tenant :: create ([ 'name' => 'Commune' , 'entra_tenant_id' => '99999999-9999-9999-9999-999999999999' ]);
$foreign = $this -> campaign ([ 'tenant_id' => $other -> id , 'name' => 'Ailleurs' ]);
$admin = $this -> user ( 'admin' );
$this -> actingAs ( $admin ) -> get ( " /admin/campagnes/ { $foreign -> id } " ) -> assertNotFound ();
$this -> actingAs ( $admin ) -> delete ( " /admin/campagnes/ { $foreign -> id } " ) -> assertNotFound ();
$this -> actingAs ( $admin ) -> get ( '/admin/campagnes' ) -> assertOk () -> assertDontSee ( 'Ailleurs' );
$this -> assertSame ( 1 , Campaign :: count ());
}
}