'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()); } }