Integration Baniere dans le backend

This commit is contained in:
mrKamoo
2026-10-02 12:45:11 +02:00
parent 1b91590cca
commit 429dd9de5e
20 changed files with 1170 additions and 84 deletions
@@ -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());
}
}
+27
View File
@@ -59,6 +59,14 @@ class AdminImagesTest extends TestCase
$this->actingAs($editor)->get('/admin/images')->assertOk()->assertSee('logo-cabm-2.png');
}
public function test_extension_deduite_du_contenu_et_non_du_nom_envoye(): void
{
$this->actingAs($this->user('editor'))->post('/admin/images', ['image' => $this->png('photo.gif')])->assertSessionHasNoErrors();
$this->assertFileExists($this->dir.'/photo.png');
$this->assertFileDoesNotExist($this->dir.'/photo.gif');
}
public function test_refuse_un_faux_png_et_le_svg(): void
{
$tmp = tempnam(sys_get_temp_dir(), 'x');
@@ -85,4 +93,23 @@ class AdminImagesTest extends TestCase
$this->actingAs($admin)->delete('/admin/images/logo.png')->assertSessionHasNoErrors();
$this->assertFileDoesNotExist($this->dir.'/logo.png');
}
public function test_image_utilisee_par_une_campagne_ou_une_autre_collectivite_non_supprimable(): void
{
file_put_contents($this->dir.'/banniere.png', base64_decode(self::PNG));
$admin = $this->user('admin');
$other = Tenant::create(['name' => 'Commune', 'entra_tenant_id' => '99999999-9999-9999-9999-999999999999']);
$campaign = \App\Models\Campaign::create(['tenant_id' => $other->id, 'name' => 'Ailleurs', 'image_path' => 'banniere.png',
'alt_text' => 'x', 'starts_at' => now(), 'ends_at' => now()->addDay()]);
$this->actingAs($admin)->delete('/admin/images/banniere.png')->assertSessionHasErrors('image');
$this->assertFileExists($this->dir.'/banniere.png');
$campaign->update(['tenant_id' => $this->tenant->id]);
$this->actingAs($admin)->delete('/admin/images/banniere.png')->assertSessionHasErrors('image');
$campaign->delete();
$this->actingAs($admin)->delete('/admin/images/banniere.png')->assertSessionHasNoErrors();
$this->assertFileDoesNotExist($this->dir.'/banniere.png');
}
}
@@ -115,4 +115,34 @@ class SignatureApiTest extends TestCase
$this->assertStringContainsString('/img/campagnes/enc.png', $html);
$this->assertStringContainsString('alt="Collecte"', $html);
}
public function test_une_seule_banniere_selon_priorite_ciblage_et_periode(): void
{
$base = ['tenant_id' => $this->tenant->id, 'alt_text' => 'x', 'starts_at' => now()->subDay(), 'ends_at' => now()->addDay()];
$group = DirectoryGroup::create(['tenant_id' => $this->tenant->id, 'entra_oid' => '33333333-3333-3333-3333-333333333333', 'display_name' => 'signature_mail_directeurs']);
Campaign::create($base + ['name' => 'Générale', 'image_path' => 'generale.png', 'priority' => 10]);
Campaign::create($base + ['name' => 'Directeurs', 'image_path' => 'directeurs.png', 'priority' => 50, 'width' => 450,
'link_url' => 'https://www.beziers-mediterranee.fr/voeux?a=1&b=2'])->groups()->attach($group->id);
// Jamais servies, malgré une priorité supérieure : inactive, pas commencée, terminée.
Campaign::create($base + ['name' => 'Inactive', 'image_path' => 'inactive.png', 'priority' => 100, 'active' => false]);
Campaign::create(['name' => 'Future', 'image_path' => 'future.png', 'priority' => 100, 'starts_at' => now()->addHour()] + $base);
Campaign::create(['name' => 'Passée', 'image_path' => 'passee.png', 'priority' => 100, 'ends_at' => now()->subHour()] + $base);
// Hors du groupe ciblé : campagne générale.
$html = $this->signature()->assertOk()->json('html');
$this->assertStringContainsString('/img/generale.png', $html);
$this->assertStringNotContainsString('directeurs.png', $html);
// Dans le groupe : la campagne ciblée, plus prioritaire, remplace la générale.
$group->members()->attach($this->agent->id);
$html = $this->signature()->json('html');
$this->assertStringContainsString('/img/directeurs.png', $html);
$this->assertStringContainsString('width="450"', $html);
$this->assertStringContainsString('<a href="https://www.beziers-mediterranee.fr/voeux?a=1&amp;b=2"', $html);
$this->assertSame(1, substr_count($html, '<img'));
// Pas de bannière dans la signature courte des réponses.
$this->signature(['context' => 'reply'])->assertJson(['html' => 'd Jean']);
}
}
@@ -0,0 +1,39 @@
<?php
namespace Tests\Unit;
use App\Signature\CampaignSelector;
use PHPUnit\Framework\TestCase;
class CampaignSelectorTest extends TestCase
{
private array $campaigns = [
['id' => 1, 'priority' => 10, 'group_ids' => []], // générale
['id' => 2, 'priority' => 50, 'group_ids' => [200]], // directeurs
['id' => 3, 'priority' => 50, 'group_ids' => [300, 400]], // même priorité, plus récente
['id' => 4, 'priority' => 10, 'group_ids' => []], // générale plus récente
];
public function test_campagne_generale_pour_un_agent_hors_groupe(): void
{
$this->assertSame(1, (new CampaignSelector)->select($this->campaigns, []));
$this->assertSame(1, (new CampaignSelector)->select($this->campaigns, [999]));
}
public function test_campagne_ciblee_prioritaire_remplace_la_generale(): void
{
$this->assertSame(2, (new CampaignSelector)->select($this->campaigns, [200]));
$this->assertSame(3, (new CampaignSelector)->select($this->campaigns, [400]));
}
public function test_egalite_departagee_par_anciennete(): void
{
$this->assertSame(2, (new CampaignSelector)->select($this->campaigns, [400, 200]));
}
public function test_aucune_banniere_sans_campagne_applicable(): void
{
$this->assertNull((new CampaignSelector)->select([], [200]));
$this->assertNull((new CampaignSelector)->select([['id' => 9, 'priority' => 0, 'group_ids' => [5]]], [200]));
}
}