Files
slim-blog/CONTRIBUTING.md
2026-03-16 11:48:26 +01:00

117 lines
5.7 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` | `AuthService` | `createUser()` (normalisation, unicité via exceptions métier, longueur mdp), `authenticate()`, `changePassword()`, `login/logout/isLoggedIn()` |
| `AuthServiceRateLimitTest` | `AuthService` | `checkRateLimit()` (IP libre, verrouillée, expirée, minimum 1 minute, `deleteExpired()`), `recordFailure()` (constantes MAX_ATTEMPTS/LOCK_MINUTES), `resetRateLimit()` |
| `LoginAttemptRepositoryTest` | `LoginAttemptRepository` | `findByIp()`, `recordFailure()` (INSERT vs UPDATE, compteur, seuil exact, fenêtre temporelle), `resetForIp()`, `deleteExpired()` |
| `PasswordResetServiceTest` | `PasswordResetService` | `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` | `PasswordResetRepository` | `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` | `UserRepository` | `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 métier (`AuthService`, `PasswordResetService`, `PostService`) 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 (`UserRepositoryTest`, etc.) testent l'implémentation concrète avec un mock PDO — c'est intentionnel.
### 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
```