117 lines
5.7 KiB
Markdown
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
|
|
```
|