Files
slim-blog/CONTRIBUTING.md
2026-03-16 16:58:54 +01:00

118 lines
6.0 KiB
Markdown

# Contribuer au projet
## Prérequis
Les mêmes que pour le développement (voir [README](README.md)), plus :
- PHP 8.4.1+ avec l'extension `dom` (requise par HTMLPurifier et PHPUnit)
## Lancer les tests
```bash
composer install
vendor/bin/phpunit
```
Avec rapport de couverture (nécessite Xdebug ou PCOV) :
```bash
vendor/bin/phpunit --coverage-text
```
## Analyse statique (PHPStan)
```bash
vendor/bin/phpstan analyse
```
Cette commande utilise `phpstan.neon` qui définit le niveau 8 et les chemins analysés. PDO est nativement connu de PHPStan — aucun stub supplémentaire n'est nécessaire.
PHPStan est configuré au niveau 8, le plus strict. L'exécuter avant toute Pull Request garantit la cohérence des types et détecte les erreurs avant l'exécution.
## Suite de tests
Les tests sont dans `tests/`, organisés en miroir de `src/`.
### `tests/Auth/`
| Fichier | Classe testée | Ce qui est vérifié |
|--------------------------------|---------------------------|---------------------|
| `AuthServiceTest` | `AuthApplicationService` | `createUser()` (normalisation, unicité via exceptions métier, longueur mdp), `authenticate()`, `changePassword()`, `login/logout/isLoggedIn()` |
| `AuthServiceRateLimitTest` | `AuthApplicationService` | `checkRateLimit()` (IP libre, verrouillée, expirée, minimum 1 minute, `deleteExpired()`), `recordFailure()` (constantes MAX_ATTEMPTS/LOCK_MINUTES), `resetRateLimit()` |
| `LoginAttemptRepositoryTest` | `PdoLoginAttemptRepository` | `findByIp()`, `recordFailure()` (INSERT vs UPDATE, compteur, seuil exact, fenêtre temporelle), `resetForIp()`, `deleteExpired()` |
| `PasswordResetServiceTest` | `PasswordResetApplicationService` | `requestReset()` (email inconnu silencieux, invalidation, création, envoi, URL), `validateToken()` (inexistant, expiré, valide), `resetPassword()` (token invalide, mdp trop court via `WeakPasswordException`, mise à jour + consommation) |
| `PasswordResetRepositoryTest` | `PdoPasswordResetRepository` | `create()`, `findActiveByHash()` (filtre `used_at = null`), `invalidateByUserId()` et `markAsUsed()` (jamais de `delete`) |
### `tests/User/`
| Fichier | Classe testée | Ce qui est vérifié |
|-----------------------|------------------|---------------------|
| `UserTest` | `User` | Construction, validation (username, email, hash, rôle), limites min/max, `fromArray()` |
| `UserRepositoryTest` | `PdoUserRepository` | `findAll/ById/ByUsername/ByEmail()`, `create()`, `updatePassword()`, `delete()` |
### `tests/Shared/`
| Fichier | Classe testée | Ce qui est vérifié |
|-----------------------|------------------|---------------------|
| `SessionManagerTest` | `SessionManager` / `SessionManagerInterface` | `isAuthenticated()`, `getUserId()`, rôles, écriture `$_SESSION`, `destroy()` |
| `HtmlSanitizerTest` | `HtmlSanitizer` | Balises autorisées conservées, XSS supprimé (`<script>`, handlers JS, `javascript:`, `data:`, `<iframe>`, `<form>`), CSS (`text-align` conservé, reste supprimé) |
| `MigratorTest` | `Migrator` | Création de la table `migrations`, idempotence de `run()`, non-rejeu des migrations déjà appliquées, enregistrement en table, `syncFtsIndex()` (indexation des articles absents, absence de doublons) |
| `SeederTest` | `Seeder` | Insertion du compte admin quand absent, idempotence (pas d'INSERT si le compte existe), normalisation username/email, hachage bcrypt, format `created_at` |
## Conventions
### Mocks
Les services applicatifs (`AuthApplicationService`, `PasswordResetApplicationService`, `PostApplicationService`) utilisent les **interfaces** comme type des dépendances. Mocker l'interface plutôt que la classe concrète :
```php
// ✅ Correct
$repo = $this->createMock(UserRepositoryInterface::class);
// ❌ À éviter
$repo = $this->createMock(UserRepository::class);
```
Les tests de repositories (`PdoUserRepository`, etc.) testent l'implémentation concrète avec un mock PDO — c'est intentionnel.
Ils doivent vérifier l'intention générale des requêtes et les valeurs retournées, sans figer inutilement chaque détail interne (noms exacts de placeholders, méthode PDO précise utilisée quand cela n'apporte rien, etc.).
### Exceptions métier
Les erreurs métier doivent lever l'exception la plus spécifique disponible :
| Situation | Exception | Namespace |
|------------------------------------|-----------------------------------|------------------------------|
| Nom d'utilisateur déjà pris | `DuplicateUsernameException` | `App\User\Exception` |
| Email déjà utilisé | `DuplicateEmailException` | `App\User\Exception` |
| Mot de passe trop court | `WeakPasswordException` | `App\User\Exception` |
| Entité introuvable en base | `NotFoundException` | `App\Shared\Exception` |
### Ajouter un test
- **Nommage** : `test` + description camelCase français (`testCreateUserNomDejaUtilise`)
- **Structure** : Arrange / Act / Assert, une assertion logique par test
- **Isolation** : dépendances toujours mockées via leur interface
- **PHPDoc** : chaque méthode de test est documentée avec une ligne décrivant le comportement attendu
### Ajouter un domaine
Voir la section *Domaines PHP* dans [Architecture](docs/ARCHITECTURE.md). Lors de l'ajout d'un domaine, créer systématiquement :
1. L'entité (`NomDomaine/NomEntite.php`)
2. L'interface du dépôt (`NomDomaine/NomEntiteRepositoryInterface.php`)
3. L'implémentation du dépôt (`NomDomaine/NomEntiteRepository.php implements NomEntiteRepositoryInterface`)
4. Les exceptions métier si nécessaires (`NomDomaine/Exception/`)
5. Les tests dans `tests/NomDomaine/`
## Formatage du code
```bash
vendor/bin/php-cs-fixer fix
```
Prévisualiser sans appliquer :
```bash
vendor/bin/php-cs-fixer fix --dry-run
```