This commit is contained in:
mrKamoo
2026-09-30 16:27:32 +02:00
commit 310724708f
147 changed files with 21327 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace Tests\Feature;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Graph\DirectorySync;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminSyncTest extends TestCase
{
use RefreshDatabase;
private Tenant $tenant;
protected function setUp(): void
{
parent::setUp();
config(['signature.admin_allowed_cidrs' => []]);
$this->tenant = Tenant::create(['name' => 'CABM', 'entra_tenant_id' => '11111111-1111-1111-1111-111111111111']);
// Pas d'appel Graph en test : la synchro est simulée.
$this->app->instance(DirectorySync::class, new class extends DirectorySync {
public array $calls = [];
public function run(Tenant $tenant, bool $full = false): array
{
$this->calls[] = $full;
return ['users' => 3, 'removed' => 1, 'groups' => 2, 'memberships' => 5];
}
});
}
private function user(string $role): User
{
return User::create(['name' => $role, 'email' => "{$role}@x.fr", 'tenant_id' => $this->tenant->id, 'entra_oid' => '22222222-2222-2222-2222-222222222222', 'role' => $role]);
}
public function test_admin_lance_une_synchro_complete(): void
{
$this->actingAs($this->user('admin'))->from('/admin')->post('/admin/synchro', ['full' => '1'])
->assertRedirect('/admin')
->assertSessionHas('status', 'Synchronisation complète terminée : 3 agents mis à jour, 1 retirés, 2 groupes, 5 appartenances.');
$this->assertSame([true], app(DirectorySync::class)->calls);
}
public function test_editeur_ne_peut_pas_synchroniser(): void
{
$this->actingAs($this->user('editor'))->post('/admin/synchro')->assertForbidden();
}
public function test_bouton_visible_sur_le_tableau_de_bord(): void
{
$this->actingAs($this->user('admin'))->get('/admin')->assertOk()->assertSee('Synchroniser maintenant');
}
}
@@ -0,0 +1,135 @@
<?php
namespace Tests\Feature;
use App\Models\Assignment;
use App\Models\AuditLog;
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 Tests\TestCase;
class AdminTemplatesTest extends TestCase
{
use RefreshDatabase;
private Tenant $tenant;
private DirectoryUser $agent;
protected function setUp(): void
{
parent::setUp();
config(['signature.admin_allowed_cidrs' => []]);
$this->tenant = Tenant::create(['name' => 'CABM', 'entra_tenant_id' => '11111111-1111-1111-1111-111111111111']);
$this->agent = DirectoryUser::create([
'tenant_id' => $this->tenant->id, 'entra_oid' => '22222222-2222-2222-2222-222222222222',
'display_name' => 'Jean Martin', 'given_name' => 'Jean', 'surname' => 'Martin',
'mail' => 'j.martin@beziers-mediterranee.fr', 'account_enabled' => true,
]);
}
private function user(string $role): User
{
return User::create([
'name' => $role, 'email' => "{$role}@beziers-mediterranee.fr", 'tenant_id' => $this->tenant->id,
'entra_oid' => '22222222-2222-2222-2222-222222222222', 'role' => $role,
]);
}
private function payload(array $over = []): array
{
return array_merge(['name' => 'Directeurs', 'draft_html_full' => '<b>{{prenom}}</b>', 'draft_html_short' => '{{nom}}'], $over);
}
public function test_agent_sans_role_refuse(): void
{
$this->actingAs($this->user(''))->get('/admin/templates')->assertForbidden();
}
public function test_editeur_cree_et_modifie_un_brouillon_nettoye_mais_ne_publie_pas(): void
{
$editor = $this->user('editor');
$this->actingAs($editor)->post('/admin/templates', $this->payload([
'draft_html_full' => '<b>{{prenom}}</b><script>alert(1)</script>',
]))->assertRedirect();
$t = Template::firstWhere('name', 'Directeurs');
$this->assertSame('<b>{{prenom}}</b>', $t->draft_html_full);
$this->assertNull($t->html_full, 'rien n\'est publié tant qu\'un admin ne publie pas');
$this->actingAs($editor)->post("/admin/templates/{$t->id}/publish")->assertForbidden();
$this->actingAs($editor)->get('/admin/affectations')->assertForbidden();
$this->assertTrue(AuditLog::where('action', 'template.create')->exists());
}
public function test_admin_publie_versionne_et_restaure(): void
{
$admin = $this->user('admin');
$t = Template::create(['tenant_id' => $this->tenant->id] + $this->payload());
$this->actingAs($admin)->post("/admin/templates/{$t->id}/publish")->assertRedirect();
$t->refresh();
$this->assertSame(1, $t->published_version);
$this->assertSame('<b>{{prenom}}</b>', $t->html_full);
$this->actingAs($admin)->put("/admin/templates/{$t->id}", $this->payload(['draft_html_full' => 'v2']))->assertRedirect();
$this->assertSame('<b>{{prenom}}</b>', $t->fresh()->html_full, 'le brouillon ne modifie pas le publié');
$version = $t->versions()->first();
$this->actingAs($admin)->post("/admin/templates/{$t->id}/versions/{$version->id}/restore")->assertRedirect();
$this->assertSame('<b>{{prenom}}</b>', $t->fresh()->draft_html_full);
}
public function test_apercu_rend_le_brouillon_pour_un_agent(): void
{
$this->actingAs($this->user('editor'))
->postJson('/admin/templates/preview', ['html' => 'Bonjour {{prenom}} <img src=x onerror=alert(1)>', 'variant' => 'full', 'agent_id' => $this->agent->id])
->assertOk()
->assertJson(['html' => 'Bonjour Jean <img src=x>', 'agent' => 'Jean Martin — j.martin@beziers-mediterranee.fr']);
}
public function test_cloisonnement_entre_tenants(): void
{
$other = Tenant::create(['name' => 'Commune', 'entra_tenant_id' => '99999999-9999-9999-9999-999999999999']);
$foreign = Template::create(['tenant_id' => $other->id, 'name' => 'Autre', 'draft_html_full' => 'x']);
$this->actingAs($this->user('superadmin'))->get("/admin/templates/{$foreign->id}")->assertNotFound();
}
public function test_affectations_crud_et_template_par_defaut(): void
{
$admin = $this->user('admin');
$t = Template::create(['tenant_id' => $this->tenant->id] + $this->payload());
$t->publish();
$group = DirectoryGroup::create(['tenant_id' => $this->tenant->id, 'entra_oid' => '33333333-3333-3333-3333-333333333333', 'display_name' => 'signature_mail_directeurs']);
$this->actingAs($admin)->post('/admin/affectations', ['template_id' => $t->id, 'directory_group_id' => $group->id, 'priority' => 50])->assertRedirect();
$this->actingAs($admin)->post('/admin/affectations', ['template_id' => $t->id, 'directory_group_id' => $group->id, 'priority' => 60])
->assertSessionHasErrors('directory_group_id');
$a = Assignment::first();
$this->actingAs($admin)->put("/admin/affectations/{$a->id}", ['priority' => 100])->assertRedirect();
$this->assertSame(100, $a->fresh()->priority);
$this->actingAs($admin)->post("/admin/templates/{$t->id}/default")->assertRedirect();
$this->assertTrue($t->fresh()->is_default);
$this->actingAs($admin)->delete("/admin/templates/{$t->id}")->assertStatus(422);
$this->actingAs($admin)->delete("/admin/affectations/{$a->id}")->assertRedirect();
$this->assertSame(0, Assignment::count());
}
public function test_pages_principales_s_affichent(): void
{
$admin = $this->user('superadmin');
$t = Template::create(['tenant_id' => $this->tenant->id] + $this->payload());
foreach (['/admin', '/admin/templates', '/admin/templates/create', "/admin/templates/{$t->id}", '/admin/affectations', '/admin/agents', "/admin/agents/{$this->agent->id}", '/me'] as $url) {
$this->actingAs($admin)->get($url)->assertOk();
}
$this->actingAs($admin)->getJson('/admin/agents/recherche?q=mar')->assertOk()->assertJsonFragment(['id' => $this->agent->id]);
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_la_racine_redirige_vers_le_portail_agent(): void
{
$this->get('/')->assertRedirect('/me');
}
public function test_le_portail_exige_une_connexion(): void
{
$this->get('/me')->assertRedirect('/login');
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
namespace Tests\Feature;
use App\Models\Assignment;
use App\Models\Campaign;
use App\Models\DirectoryGroup;
use App\Models\DirectoryUser;
use App\Models\EditableField;
use App\Models\SharedMailbox;
use App\Models\Template;
use App\Models\Tenant;
use App\Models\UserOverride;
use App\Models\UserPreference;
use App\Services\Auth\EntraTokenValidator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use RuntimeException;
use Tests\TestCase;
class SignatureApiTest extends TestCase
{
use RefreshDatabase;
private Tenant $tenant;
private DirectoryUser $agent;
protected function setUp(): void
{
parent::setUp();
$this->tenant = Tenant::create(['name' => 'CABM', 'entra_tenant_id' => '11111111-1111-1111-1111-111111111111']);
$this->agent = DirectoryUser::create([
'tenant_id' => $this->tenant->id, 'entra_oid' => '22222222-2222-2222-2222-222222222222',
'given_name' => 'Jean', 'surname' => 'Martin', 'job_title' => 'Technicien',
'mobile_phone' => '06 00 00 00 00', 'mail' => 'j.martin@beziers-mediterranee.fr', 'account_enabled' => true,
]);
$default = Template::create(['tenant_id' => $this->tenant->id, 'name' => 'Défaut', 'is_default' => true,
'draft_html_full' => 'D {{prenom}}{{#if mobile}} {{mobile}}{{/if}}{{{banniere}}}', 'draft_html_short' => 'd {{prenom}}']);
$default->publish();
// Le validateur de jeton est remplacé : on teste ici la logique métier, pas la cryptographie.
$this->app->instance(EntraTokenValidator::class, new class extends EntraTokenValidator {
public function validate(string $jwt): array
{
if ($jwt !== 'ok') {
throw new RuntimeException('bad');
}
return ['tid' => '11111111-1111-1111-1111-111111111111', 'oid' => '22222222-2222-2222-2222-222222222222'];
}
});
}
private function signature(array $q = [], string $token = 'ok')
{
return $this->withToken($token)->getJson('/api/v1/signature?'.http_build_query($q));
}
public function test_refuse_sans_jeton_valide(): void
{
$this->getJson('/api/v1/signature')->assertStatus(401);
$this->signature([], 'bad')->assertStatus(401);
}
public function test_template_par_defaut_puis_groupe_prioritaire(): void
{
$this->signature()->assertOk()->assertJson(['html' => 'D Jean 06 00 00 00 00', 'variant' => 'full', 'template' => 'Défaut']);
$group = DirectoryGroup::create(['tenant_id' => $this->tenant->id, 'entra_oid' => '33333333-3333-3333-3333-333333333333', 'display_name' => 'signature_mail_directeurs']);
$group->members()->attach($this->agent->id);
$dir = Template::create(['tenant_id' => $this->tenant->id, 'name' => 'Directeurs', 'draft_html_full' => 'DIR {{nom}}']);
$dir->publish();
Assignment::create(['tenant_id' => $this->tenant->id, 'template_id' => $dir->id, 'directory_group_id' => $group->id, 'priority' => 100]);
$this->signature()->assertJson(['html' => 'DIR Martin', 'template' => 'Directeurs']);
}
public function test_variante_reponse_selon_preference(): void
{
$this->signature(['context' => 'reply'])->assertJson(['html' => 'd Jean', 'variant' => 'short']);
UserPreference::create(['directory_user_id' => $this->agent->id, 'reply_variant' => 'none']);
$this->signature(['context' => 'reply'])->assertJson(['html' => '', 'variant' => 'none']);
}
public function test_surcharge_agent_uniquement_si_champ_ouvert(): void
{
UserOverride::create(['directory_user_id' => $this->agent->id, 'field' => 'mobile', 'value' => '07 11 11 11 11']);
$this->signature()->assertJson(['html' => 'D Jean 06 00 00 00 00']);
EditableField::create(['tenant_id' => $this->tenant->id, 'field' => 'mobile', 'label' => 'Mobile', 'enabled' => true]);
$this->signature()->assertJson(['html' => 'D Jean 07 11 11 11 11']);
}
public function test_boite_partagee_avec_ou_sans_expediteur(): void
{
$tpl = Template::create(['tenant_id' => $this->tenant->id, 'name' => 'Accueil', 'draft_html_full' => '{{boite_nom}}{{#if prenom}} / {{prenom}}{{/if}}']);
$tpl->publish();
$box = SharedMailbox::create(['tenant_id' => $this->tenant->id, 'address' => 'accueil@beziers-mediterranee.fr',
'display_name' => 'Accueil', 'template_id' => $tpl->id, 'include_sender' => false]);
$this->signature(['mailbox' => 'accueil@beziers-mediterranee.fr'])->assertJson(['html' => 'Accueil']);
$box->update(['include_sender' => true]);
$this->signature(['mailbox' => 'accueil@beziers-mediterranee.fr'])->assertJson(['html' => 'Accueil / Jean']);
}
public function test_banniere_de_campagne_active_et_ciblee(): void
{
Campaign::create(['tenant_id' => $this->tenant->id, 'name' => 'Encombrants', 'image_path' => 'campagnes/enc.png',
'alt_text' => 'Collecte', 'starts_at' => now()->subDay(), 'ends_at' => now()->addDay()]);
$html = $this->signature()->assertOk()->json('html');
$this->assertStringContainsString('/img/campagnes/enc.png', $html);
$this->assertStringContainsString('alt="Collecte"', $html);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Tests\Unit;
use App\Signature\TemplateRenderer;
use PHPUnit\Framework\TestCase;
class TemplateRendererTest extends TestCase
{
private TemplateRenderer $r;
protected function setUp(): void
{
$this->r = new TemplateRenderer;
}
public function test_substitue_et_echappe_les_variables(): void
{
$this->assertSame('Bonjour Jean &lt;b&gt;', $this->r->render('Bonjour {{prenom}}', ['prenom' => 'Jean <b>']));
}
public function test_valeur_brute_avec_triple_accolade(): void
{
$this->assertSame('<img src="x">', $this->r->render('{{{banniere}}}', ['banniere' => '<img src="x">']));
}
public function test_bloc_if_masque_si_vide(): void
{
$tpl = 'A{{#if mobile}} Mob. {{mobile}}{{/if}}B';
$this->assertSame('AB', $this->r->render($tpl, ['mobile' => ' ']));
$this->assertSame('A Mob. 06B', $this->r->render($tpl, ['mobile' => '06']));
}
public function test_blocs_imbriques_et_unless(): void
{
$tpl = '{{#if adresse}}{{adresse}}{{#if ville}}, {{ville}}{{/if}}{{/if}}{{#unless adresse}}-{{/unless}}';
$this->assertSame('1 rue X, Béziers', $this->r->render($tpl, ['adresse' => '1 rue X', 'ville' => 'Béziers']));
$this->assertSame('1 rue X', $this->r->render($tpl, ['adresse' => '1 rue X']));
$this->assertSame('-', $this->r->render($tpl, []));
}
public function test_variable_inconnue_vide_et_casse_ignoree(): void
{
$this->assertSame('[]', $this->r->render('[{{Inconnue}}]', []));
$this->assertSame('x', $this->r->render('{{PRENOM}}', ['prenom' => 'x']));
}
public function test_liste_des_variables(): void
{
$vars = $this->r->variables('{{prenom}} {{#if mobile}}{{mobile}}{{/if}} {{{banniere}}}');
sort($vars);
$this->assertSame(['banniere', 'mobile', 'prenom'], $vars);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Tests\Unit;
use App\Signature\TemplateSanitizer;
use PHPUnit\Framework\TestCase;
class TemplateSanitizerTest extends TestCase
{
private TemplateSanitizer $s;
protected function setUp(): void
{
$this->s = new TemplateSanitizer;
}
public function test_retire_scripts_et_balises_actives(): void
{
$out = $this->s->clean('<p>A</p><script>alert(1)</script><iframe src="x"></iframe><form><input></form><b>B</b>');
$this->assertSame('<p>A</p><b>B</b>', $out);
}
public function test_retire_gestionnaires_evenements(): void
{
$this->assertSame('<img src="a.png">', $this->s->clean('<img src="a.png" onerror="alert(1)">'));
$this->assertSame('<a href="https://x">x</a>', $this->s->clean("<a href=\"https://x\" onClick='y()'>x</a>"));
}
public function test_retire_url_javascript_mais_garde_https_mailto_tel(): void
{
$this->assertSame('<a>x</a>', $this->s->clean('<a href="javascript:alert(1)">x</a>'));
$this->assertSame('<a>x</a>', $this->s->clean('<a href=" JaVa&#115;cript:alert(1)">x</a>'));
$ok = '<a href="mailto:a@b.fr">m</a><a href="tel:+33400">t</a><img src="https://s/img/l.png">';
$this->assertSame($ok, $this->s->clean($ok));
}
public function test_conserve_marqueurs_de_template_entre_lignes_de_tableau(): void
{
$tpl = '<table><tr><td>{{prenom}}</td></tr>{{#if banniere}}<tr><td>{{{banniere}}}</td></tr>{{/if}}</table>';
$this->assertSame($tpl, $this->s->clean($tpl));
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Unit;
use App\Signature\TemplateSelector;
use PHPUnit\Framework\TestCase;
class TemplateSelectorTest extends TestCase
{
private array $assignments = [
['id' => 1, 'template_id' => 10, 'group_id' => 100, 'priority' => 10], // agents
['id' => 2, 'template_id' => 20, 'group_id' => 200, 'priority' => 100], // directeurs
['id' => 3, 'template_id' => 30, 'group_id' => 300, 'priority' => 100], // même priorité, plus récent
];
public function test_priorite_la_plus_haute_gagne(): void
{
$this->assertSame(20, (new TemplateSelector)->select($this->assignments, [100, 200], 99));
}
public function test_egalite_departagee_par_anciennete(): void
{
$this->assertSame(20, (new TemplateSelector)->select($this->assignments, [300, 200], 99));
}
public function test_template_par_defaut_hors_groupe(): void
{
$this->assertSame(99, (new TemplateSelector)->select($this->assignments, [555], 99));
$this->assertNull((new TemplateSelector)->select($this->assignments, [], null));
}
}