Files
AggloSign/backend/tests/Feature/AdminCampaignsTest.php

236 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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());
}
}