Une plateforme éditoriale multi-rédaction que nous maintenons a introduit une capacité personnalisée valider_article_partenaire, réservée aux rédacteurs en chef et aux administrateurs, pour distinguer la validation d’articles rédigés par des partenaires externes de la publication classique. Un audit de sécurité effectué six mois plus tard a révélé qu’un rôle « contributeur senior », ajouté entre-temps par une autre personne de l’équipe pour un besoin non lié, avait hérité de cette capacité par un mauvais copier-coller de définition de rôle. Aucun test n’avait détecté cette dérive, faute de couverture systématique croisant rôles et capacités.
Le problème n’était pas la capacité elle-même, correctement implémentée au départ, mais l’absence de garde-fou testant explicitement qui peut et qui ne peut pas l’exercer, à chaque évolution future des rôles.
Lister exhaustivement la matrice attendue
Avant d’écrire le moindre test, il faut formaliser noir sur blanc ce qui est attendu — ce tableau devient ensuite la référence pour la génération du test :
| Rôle | valider_article_partenaire |
|---|---|
| administrator | Autorisé |
| editor (rédacteur en chef) | Autorisé |
| author | Refusé |
| contributor | Refusé |
| contributeur_senior (personnalisé) | Refusé |
| subscriber | Refusé |
Générer la matrice avec un data provider
Plutôt que d’écrire six méthodes de test presque identiques, un data provider parcourt la matrice définie ci-dessus et délègue à une seule méthode de test la vérification pour chaque rôle :
public static function matrice_roles_capacite(): array {
return [
'administrator autorisé' => ['administrator', true],
'editor autorisé' => ['editor', true],
'author refusé' => ['author', false],
'contributor refusé' => ['contributor', false],
'contributeur_senior refusé' => ['contributeur_senior', false],
'subscriber refusé' => ['subscriber', false],
];
}
#[DataProvider('matrice_roles_capacite')]
public function test_capacite_valider_article_partenaire(string $role, bool $attendu): void {
$utilisateur_id = $this->factory()->user->create(['role' => $role]);
wp_set_current_user($utilisateur_id);
$this->assertSame($attendu, current_user_can('valider_article_partenaire'));
}

Ce test aurait immédiatement échoué au moment où le rôle « contributeur senior » a hérité par erreur de la capacité, puisque son entrée dans la matrice attend explicitement false.
Couvrir aussi map_meta_cap si la capacité en dépend
Certaines capacités ne s’évaluent pas uniquement sur le rôle global, mais aussi sur le contexte d’un article précis via map_meta_cap — par exemple, un rédacteur en chef ne peut valider que les articles de sa propre rédaction, pas ceux d’une rédaction sœur sur une plateforme multisite. Ce cas mérite une matrice distincte, croisant rôle et contexte d’article, plutôt que d’être mélangé à la vérification globale de capacité :
public function test_redacteur_en_chef_ne_valide_pas_hors_de_sa_redaction(): void {
$redacteur_id = $this->factory()->user->create(['role' => 'editor']);
update_user_meta($redacteur_id, 'redaction_id', 3);
wp_set_current_user($redacteur_id);
$article_autre_redaction = $this->factory()->post->create(['post_type' => 'article_partenaire']);
update_post_meta($article_autre_redaction, 'redaction_id', 7);
$this->assertFalse(
current_user_can('valider_article_partenaire', $article_autre_redaction)
);
}
Ne pas oublier les rôles ajoutés par des extensions tierces
- Un rôle ajouté par une extension de commerce ou de gestion des membres peut hériter silencieusement de capacités du rôle sur lequel il se base (souvent
subscriberouauthor). - La matrice de test doit être révisée à chaque ajout de rôle personnalisé sur le projet, pas seulement à la création initiale de la capacité.
- Un test de garde générique, qui vérifie qu’aucun rôle en dehors de la liste attendue ne possède la capacité, complète utilement la matrice explicite :
public function test_aucun_role_non_liste_ne_possede_la_capacite(): void {
$roles_autorises = ['administrator', 'editor'];
foreach (wp_roles()->roles as $slug_role => $donnees_role) {
if (in_array($slug_role, $roles_autorises, true)) {
continue;
}
$this->assertArrayNotHasKey(
'valider_article_partenaire',
$donnees_role['capabilities'],
"Le rôle $slug_role ne devrait pas avoir cette capacité"
);
}
}
Une capacité personnalisée non testée par matrice de rôles est une porte qui reste ouverte jusqu’au jour où quelqu’un, sans mauvaise intention, ajoute un rôle qui hérite d’un peu trop de permissions.
En résumé
La matrice rôle par action transforme une vérification de sécurité difficile à maintenir de mémoire en une liste explicite, versionnée dans le code, que tout nouveau rôle ajouté au projet doit venir compléter. Ce test ne remplace pas une compréhension fine de map_meta_cap, mais garantit qu’aucune capacité sensible ne se retrouve accordée à un rôle par accident, sans qu’un test ne le signale immédiatement.