diff --git a/.gitignore b/.gitignore index f7ea1d3..f1b8e9a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ public/assets/ database/*.sqlite database/*.sqlite-shm database/*.sqlite-wal +database/.provision.lock # ============================================ # Cache & Logs diff --git a/src/Auth/PasswordResetRepositoryInterface.php b/src/Auth/PasswordResetRepositoryInterface.php index 9aedd54..ac065df 100644 --- a/src/Auth/PasswordResetRepositoryInterface.php +++ b/src/Auth/PasswordResetRepositoryInterface.php @@ -8,14 +8,14 @@ interface PasswordResetRepositoryInterface public function create(int $userId, string $tokenHash, string $expiresAt): void; /** - * Consomme atomiquement un token non utilisé et non expiré. - * - * L'implémentation doit effectuer l'opération en une seule étape SQL - * afin d'éviter les courses entre lecture et écriture. - * - * @param string $tokenHash Hash SHA-256 du token de reset - * @param string $usedAt Horodatage de consommation au format SQL - * @return array|null Les données du token consommé, ou null si le token est invalide, expiré ou déjà utilisé - */ -public function consumeActiveToken(string $tokenHash, string $usedAt): ?array; + * @return array|null + */ + public function findActiveByHash(string $tokenHash): ?array; + + public function invalidateByUserId(int $userId): void; + + /** + * @return array|null + */ + public function consumeActiveToken(string $tokenHash, string $usedAt): ?array; } diff --git a/tests/Auth/AccountControllerTest.php b/tests/Auth/AccountControllerTest.php index 2171de0..0114dd0 100644 --- a/tests/Auth/AccountControllerTest.php +++ b/tests/Auth/AccountControllerTest.php @@ -18,6 +18,7 @@ use Tests\ControllerTestCase; * mots de passe non identiques, mot de passe faible, mot de passe actuel * incorrect, erreur inattendue et succès. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class AccountControllerTest extends ControllerTestCase { /** @var \Slim\Views\Twig&MockObject */ diff --git a/tests/Auth/AuthControllerTest.php b/tests/Auth/AuthControllerTest.php index b4336fe..7c0c7c3 100644 --- a/tests/Auth/AuthControllerTest.php +++ b/tests/Auth/AuthControllerTest.php @@ -18,6 +18,7 @@ use Tests\ControllerTestCase; * AuthService, FlashService et Twig sont mockés — aucune session PHP, * aucune base de données, aucun serveur HTTP n'est requis. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class AuthControllerTest extends ControllerTestCase { /** @var \Slim\Views\Twig&MockObject */ diff --git a/tests/Auth/AuthServiceRateLimitTest.php b/tests/Auth/AuthServiceRateLimitTest.php index 6ce49b0..66d3d52 100644 --- a/tests/Auth/AuthServiceRateLimitTest.php +++ b/tests/Auth/AuthServiceRateLimitTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; * - MAX_ATTEMPTS = 5 : nombre d'échecs avant verrouillage * - LOCK_MINUTES = 15 : durée du verrouillage en minutes */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class AuthServiceRateLimitTest extends TestCase { /** @var UserRepositoryInterface&MockObject */ diff --git a/tests/Auth/AuthServiceTest.php b/tests/Auth/AuthServiceTest.php index 17a51e7..6d34d46 100644 --- a/tests/Auth/AuthServiceTest.php +++ b/tests/Auth/AuthServiceTest.php @@ -21,6 +21,7 @@ use PHPUnit\Framework\TestCase; * Les dépendances sont remplacées par des mocks via leurs interfaces pour * isoler le service. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class AuthServiceTest extends TestCase { /** @var UserRepositoryInterface&MockObject */ @@ -58,7 +59,7 @@ final class AuthServiceTest extends TestCase $password = 'motdepasse1'; $user = $this->makeUser('alice', 'alice@example.com', $password); - $this->userRepository->method('findByUsername')->with('alice')->willReturn($user); + $this->userRepository->expects($this->once())->method('findByUsername')->with('alice')->willReturn($user); $result = $this->service->authenticate('alice', $password); @@ -73,7 +74,7 @@ final class AuthServiceTest extends TestCase $password = 'motdepasse1'; $user = $this->makeUser('alice', 'alice@example.com', $password); - $this->userRepository->method('findByUsername')->with('alice')->willReturn($user); + $this->userRepository->expects($this->once())->method('findByUsername')->with('alice')->willReturn($user); $result = $this->service->authenticate('ALICE', $password); @@ -116,7 +117,7 @@ final class AuthServiceTest extends TestCase $password = 'ancienmdp1'; $user = $this->makeUser('alice', 'alice@example.com', $password, 1); - $this->userRepository->method('findById')->with(1)->willReturn($user); + $this->userRepository->expects($this->once())->method('findById')->with(1)->willReturn($user); $this->userRepository->expects($this->once())->method('updatePassword')->with(1); $this->service->changePassword(1, $password, 'nouveaumdp1'); diff --git a/tests/Auth/LoginAttemptRepositoryTest.php b/tests/Auth/LoginAttemptRepositoryTest.php index ebb7d6b..c3c5a9e 100644 --- a/tests/Auth/LoginAttemptRepositoryTest.php +++ b/tests/Auth/LoginAttemptRepositoryTest.php @@ -24,6 +24,7 @@ use PHPUnit\Framework\TestCase; * PDO et PDOStatement sont mockés pour isoler complètement * le dépôt de la base de données. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class LoginAttemptRepositoryTest extends TestCase { /** @var PDO&MockObject */ diff --git a/tests/Auth/MiddlewareTest.php b/tests/Auth/MiddlewareTest.php index 7515bc6..c547413 100644 --- a/tests/Auth/MiddlewareTest.php +++ b/tests/Auth/MiddlewareTest.php @@ -15,6 +15,8 @@ use Psr\Http\Server\RequestHandlerInterface; use Slim\Psr7\Factory\ServerRequestFactory; use Slim\Psr7\Response; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class MiddlewareTest extends TestCase { /** @var SessionManagerInterface&MockObject */ diff --git a/tests/Auth/PasswordResetControllerTest.php b/tests/Auth/PasswordResetControllerTest.php index 2be3f40..b0b25f5 100644 --- a/tests/Auth/PasswordResetControllerTest.php +++ b/tests/Auth/PasswordResetControllerTest.php @@ -27,6 +27,7 @@ use Tests\ControllerTestCase; * - showReset() valide le token avant d'afficher le formulaire * - reset() couvre 5 chemins de sortie (token vide, mismatch, trop court, invalide, succès) */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class PasswordResetControllerTest extends ControllerTestCase { /** @var \Slim\Views\Twig&MockObject */ diff --git a/tests/Auth/PasswordResetRepositoryTest.php b/tests/Auth/PasswordResetRepositoryTest.php index f912d1e..e534961 100644 --- a/tests/Auth/PasswordResetRepositoryTest.php +++ b/tests/Auth/PasswordResetRepositoryTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; * PDO et PDOStatement sont mockés pour isoler complètement * le dépôt de la base de données. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class PasswordResetRepositoryTest extends TestCase { /** @var PDO&MockObject */ @@ -60,7 +61,7 @@ final class PasswordResetRepositoryTest extends TestCase $expiresAt = date('Y-m-d H:i:s', time() + 3600); $stmt = $this->stmtOk(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('INSERT INTO password_resets')) ->willReturn($stmt); @@ -162,7 +163,7 @@ final class PasswordResetRepositoryTest extends TestCase $userId = 42; $stmt = $this->stmtOk(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('UPDATE password_resets')) ->willReturn($stmt); @@ -200,7 +201,7 @@ final class PasswordResetRepositoryTest extends TestCase public function testInvalidateByUserIdNeverCallsDelete(): void { $stmt = $this->stmtOk(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('UPDATE')) ->willReturn($stmt); diff --git a/tests/Auth/PasswordResetServiceIntegrationTest.php b/tests/Auth/PasswordResetServiceIntegrationTest.php index 780b5d1..71a4cbe 100644 --- a/tests/Auth/PasswordResetServiceIntegrationTest.php +++ b/tests/Auth/PasswordResetServiceIntegrationTest.php @@ -13,6 +13,8 @@ use App\User\UserRepository; use PDO; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class PasswordResetServiceIntegrationTest extends TestCase { private PDO $db; diff --git a/tests/Auth/PasswordResetServiceTest.php b/tests/Auth/PasswordResetServiceTest.php index 366193a..f3b03b5 100644 --- a/tests/Auth/PasswordResetServiceTest.php +++ b/tests/Auth/PasswordResetServiceTest.php @@ -20,6 +20,7 @@ use PHPUnit\Framework\TestCase; * Vérifie la génération de token, la validation et la réinitialisation * du mot de passe. Les dépendances sont mockées via leurs interfaces. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class PasswordResetServiceTest extends TestCase { /** @var PasswordResetRepositoryInterface&MockObject */ @@ -95,7 +96,7 @@ final class PasswordResetServiceTest extends TestCase $this->resetRepository->method('invalidateByUserId'); $this->resetRepository->expects($this->once()) ->method('create') - ->with($user->getId(), $this->isType('string'), $this->isType('string')); + ->with($user->getId(), $this->callback('is_string'), $this->callback('is_string')); $this->mailService->method('send'); $this->service->requestReset('alice@example.com', 'https://blog.exemple.com'); @@ -116,9 +117,9 @@ final class PasswordResetServiceTest extends TestCase ->method('send') ->with( 'alice@example.com', - $this->isType('string'), + $this->callback('is_string'), 'emails/password-reset.twig', - $this->isType('array'), + $this->callback('is_array'), ); $this->service->requestReset('alice@example.com', 'https://blog.exemple.com'); @@ -173,7 +174,7 @@ final class PasswordResetServiceTest extends TestCase $tokenRaw = 'montokenbrut'; $tokenHash = hash('sha256', $tokenRaw); - $this->resetRepository->method('findActiveByHash')->with($tokenHash)->willReturn([ + $this->resetRepository->expects($this->once())->method('findActiveByHash')->with($tokenHash)->willReturn([ 'user_id' => 1, 'token_hash' => $tokenHash, 'expires_at' => date('Y-m-d H:i:s', time() - 3600), @@ -194,13 +195,13 @@ final class PasswordResetServiceTest extends TestCase $tokenRaw = 'montokenbrut'; $tokenHash = hash('sha256', $tokenRaw); - $this->resetRepository->method('findActiveByHash')->with($tokenHash)->willReturn([ + $this->resetRepository->expects($this->once())->method('findActiveByHash')->with($tokenHash)->willReturn([ 'user_id' => $user->getId(), 'token_hash' => $tokenHash, 'expires_at' => date('Y-m-d H:i:s', time() + 3600), 'used_at' => null, ]); - $this->userRepository->method('findById')->with($user->getId())->willReturn($user); + $this->userRepository->expects($this->once())->method('findById')->with($user->getId())->willReturn($user); $result = $this->service->validateToken($tokenRaw); @@ -224,8 +225,8 @@ final class PasswordResetServiceTest extends TestCase 'used_at' => null, ]; - $this->resetRepository->method('findActiveByHash')->willReturn($row); - $this->userRepository->method('findById')->with(999)->willReturn(null); + $this->resetRepository->expects($this->once())->method('findActiveByHash')->willReturn($row); + $this->userRepository->expects($this->once())->method('findById')->with(999)->willReturn(null); $result = $this->service->validateToken('token-valide-mais-user-supprime'); @@ -282,7 +283,7 @@ final class PasswordResetServiceTest extends TestCase $this->resetRepository->expects($this->once()) ->method('consumeActiveToken') - ->with($tokenHash, $this->isType('string')) + ->with($tokenHash, $this->callback('is_string')) ->willReturn([ 'user_id' => $user->getId(), 'token_hash' => $tokenHash, diff --git a/tests/Category/CategoryControllerTest.php b/tests/Category/CategoryControllerTest.php index e52dde4..328e603 100644 --- a/tests/Category/CategoryControllerTest.php +++ b/tests/Category/CategoryControllerTest.php @@ -17,6 +17,7 @@ use Tests\ControllerTestCase; * rendu de la liste, création réussie, erreur de création, * suppression avec catégorie introuvable, succès et erreur métier. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class CategoryControllerTest extends ControllerTestCase { /** @var \Slim\Views\Twig&MockObject */ diff --git a/tests/Category/CategoryModelTest.php b/tests/Category/CategoryModelTest.php index 45dfacc..64fcf1d 100644 --- a/tests/Category/CategoryModelTest.php +++ b/tests/Category/CategoryModelTest.php @@ -6,6 +6,8 @@ namespace Tests\Category; use App\Category\Category; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class CategoryModelTest extends TestCase { public function testConstructAndGettersExposeCategoryData(): void diff --git a/tests/Category/CategoryRepositoryTest.php b/tests/Category/CategoryRepositoryTest.php index ec44ac8..b70ba09 100644 --- a/tests/Category/CategoryRepositoryTest.php +++ b/tests/Category/CategoryRepositoryTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; * PDO et PDOStatement sont mockés pour isoler complètement * le dépôt de la base de données. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class CategoryRepositoryTest extends TestCase { /** @var PDO&MockObject */ @@ -221,7 +222,7 @@ final class CategoryRepositoryTest extends TestCase $category = Category::fromArray($this->rowPhp); $stmt = $this->stmtForWrite(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('INSERT INTO categories')) ->willReturn($stmt); diff --git a/tests/Category/CategoryServiceTest.php b/tests/Category/CategoryServiceTest.php index cf646e0..a6031a0 100644 --- a/tests/Category/CategoryServiceTest.php +++ b/tests/Category/CategoryServiceTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase; * et la suppression (blocage si articles rattachés). * Le repository est remplacé par un mock pour isoler le service. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class CategoryServiceTest extends TestCase { /** @var CategoryRepositoryInterface&MockObject */ @@ -129,7 +130,7 @@ final class CategoryServiceTest extends TestCase { $category = new Category(5, 'PHP', 'php'); - $this->repository->method('hasPost')->with(5)->willReturn(false); + $this->repository->expects($this->once())->method('hasPost')->with(5)->willReturn(false); $this->repository->expects($this->once())->method('delete')->with(5); $this->service->delete($category); @@ -142,7 +143,7 @@ final class CategoryServiceTest extends TestCase { $category = new Category(5, 'PHP', 'php'); - $this->repository->method('hasPost')->with(5)->willReturn(true); + $this->repository->expects($this->once())->method('hasPost')->with(5)->willReturn(true); $this->repository->expects($this->never())->method('delete'); $this->expectException(\InvalidArgumentException::class); @@ -181,7 +182,7 @@ final class CategoryServiceTest extends TestCase public function testFindBySlugReturnsCategoryWhenFound(): void { $cat = new Category(3, 'PHP', 'php'); - $this->repository->method('findBySlug')->with('php')->willReturn($cat); + $this->repository->expects($this->once())->method('findBySlug')->with('php')->willReturn($cat); $this->assertSame($cat, $this->service->findBySlug('php')); } diff --git a/tests/ControllerTestCase.php b/tests/ControllerTestCase.php index c964d15..9f4fce9 100644 --- a/tests/ControllerTestCase.php +++ b/tests/ControllerTestCase.php @@ -18,6 +18,7 @@ use Slim\Psr7\Response as SlimResponse; * Chaque test de contrôleur invoque directement l'action (méthode publique) * sans passer par le routeur Slim — les middlewares sont testés séparément. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] abstract class ControllerTestCase extends TestCase { // ── Factories ──────────────────────────────────────────────────── diff --git a/tests/Media/MediaControllerTest.php b/tests/Media/MediaControllerTest.php index 9ce7b4d..adf5e23 100644 --- a/tests/Media/MediaControllerTest.php +++ b/tests/Media/MediaControllerTest.php @@ -23,6 +23,7 @@ use Tests\ControllerTestCase; * - upload : absence de fichier, erreur PSR-7, exceptions métier (taille, MIME, stockage), succès * - delete : introuvable, non-propriétaire, succès propriétaire, succès admin */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class MediaControllerTest extends ControllerTestCase { /** @var \Slim\Views\Twig&MockObject */ diff --git a/tests/Media/MediaModelTest.php b/tests/Media/MediaModelTest.php index dd4e33a..a957114 100644 --- a/tests/Media/MediaModelTest.php +++ b/tests/Media/MediaModelTest.php @@ -7,6 +7,8 @@ use App\Media\Media; use DateTime; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class MediaModelTest extends TestCase { public function testConstructAndGettersExposeMediaData(): void diff --git a/tests/Media/MediaRepositoryTest.php b/tests/Media/MediaRepositoryTest.php index 5076877..6dacbf3 100644 --- a/tests/Media/MediaRepositoryTest.php +++ b/tests/Media/MediaRepositoryTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; * PDO et PDOStatement sont mockés pour isoler complètement * le dépôt de la base de données. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class MediaRepositoryTest extends TestCase { /** @var PDO&MockObject */ @@ -258,7 +259,7 @@ final class MediaRepositoryTest extends TestCase $media = Media::fromArray($this->rowImage); $stmt = $this->stmtForWrite(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('INSERT INTO media')) ->willReturn($stmt); diff --git a/tests/Media/MediaServiceDuplicateAfterInsertRaceTest.php b/tests/Media/MediaServiceDuplicateAfterInsertRaceTest.php index 26d2272..468c505 100644 --- a/tests/Media/MediaServiceDuplicateAfterInsertRaceTest.php +++ b/tests/Media/MediaServiceDuplicateAfterInsertRaceTest.php @@ -12,6 +12,8 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UploadedFileInterface; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class MediaServiceDuplicateAfterInsertRaceTest extends TestCase { /** @var MediaRepositoryInterface&MockObject */ @@ -67,7 +69,7 @@ final class MediaServiceDuplicateAfterInsertRaceTest extends TestCase private function makeUploadedFileFromPath(string $path, int $size): UploadedFileInterface { $stream = $this->createMock(StreamInterface::class); - $stream->method('getMetadata')->with('uri')->willReturn($path); + $stream->expects($this->once())->method('getMetadata')->with('uri')->willReturn($path); $file = $this->createMock(UploadedFileInterface::class); $file->method('getSize')->willReturn($size); diff --git a/tests/Media/MediaServiceEdgeCasesTest.php b/tests/Media/MediaServiceEdgeCasesTest.php index 5e91875..147aa5d 100644 --- a/tests/Media/MediaServiceEdgeCasesTest.php +++ b/tests/Media/MediaServiceEdgeCasesTest.php @@ -11,6 +11,8 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\StreamInterface; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class MediaServiceEdgeCasesTest extends TestCase { public function testRejectsWhenSizeUnknown(): void diff --git a/tests/Media/MediaServiceInvalidMimeTest.php b/tests/Media/MediaServiceInvalidMimeTest.php index af14f6e..0c2a655 100644 --- a/tests/Media/MediaServiceInvalidMimeTest.php +++ b/tests/Media/MediaServiceInvalidMimeTest.php @@ -10,6 +10,8 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\StreamInterface; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class MediaServiceInvalidMimeTest extends TestCase { public function testRejectsNonImageContentEvenWithImageLikeFilename(): void @@ -21,7 +23,7 @@ final class MediaServiceInvalidMimeTest extends TestCase file_put_contents($tmpFile, 'not an image'); $stream = $this->createMock(StreamInterface::class); - $stream->method('getMetadata')->with('uri')->willReturn($tmpFile); + $stream->expects($this->once())->method('getMetadata')->with('uri')->willReturn($tmpFile); $file = $this->createMock(UploadedFileInterface::class); $file->method('getSize')->willReturn(filesize($tmpFile)); diff --git a/tests/Media/MediaServiceInvalidTempPathTest.php b/tests/Media/MediaServiceInvalidTempPathTest.php index 3c2fa9a..e4129dd 100644 --- a/tests/Media/MediaServiceInvalidTempPathTest.php +++ b/tests/Media/MediaServiceInvalidTempPathTest.php @@ -10,6 +10,8 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UploadedFileInterface; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class MediaServiceInvalidTempPathTest extends TestCase { public function testRejectsWhenTemporaryPathIsMissing(): void @@ -17,7 +19,7 @@ final class MediaServiceInvalidTempPathTest extends TestCase $repository = $this->createMock(MediaRepositoryInterface::class); $stream = $this->createMock(StreamInterface::class); - $stream->method('getMetadata')->with('uri')->willReturn(null); + $stream->expects($this->once())->method('getMetadata')->with('uri')->willReturn(null); $file = $this->createMock(UploadedFileInterface::class); $file->method('getSize')->willReturn(128); diff --git a/tests/Media/MediaServiceTest.php b/tests/Media/MediaServiceTest.php index 0373622..2886746 100644 --- a/tests/Media/MediaServiceTest.php +++ b/tests/Media/MediaServiceTest.php @@ -27,6 +27,7 @@ use Psr\Http\Message\UploadedFileInterface; * - stockage : fichier écrit sur disque, media créé en base * - suppression : fichier supprimé du disque et entrée retirée de la base */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class MediaServiceTest extends TestCase { /** @var MediaRepositoryInterface&MockObject */ @@ -196,7 +197,7 @@ final class MediaServiceTest extends TestCase public function testFindByIdReturnsMedia(): void { $media = new Media(3, 'photo.jpg', '/media/photo.jpg', 'abc123', 1); - $this->repository->method('findById')->with(3)->willReturn($media); + $this->repository->expects($this->once())->method('findById')->with(3)->willReturn($media); $this->assertSame($media, $this->service->findById(3)); } @@ -210,7 +211,7 @@ final class MediaServiceTest extends TestCase private function makeUploadedFile(int $size): UploadedFileInterface { $stream = $this->createMock(StreamInterface::class); - $stream->method('getMetadata')->with('uri')->willReturn('/nonexistent/path'); + $stream->method('getMetadata')->willReturnMap([['uri', '/nonexistent/path']]); $file = $this->createMock(UploadedFileInterface::class); $file->method('getSize')->willReturn($size); @@ -226,7 +227,7 @@ final class MediaServiceTest extends TestCase private function makeUploadedFileFromPath(string $path, int $size): UploadedFileInterface { $stream = $this->createMock(StreamInterface::class); - $stream->method('getMetadata')->with('uri')->willReturn($path); + $stream->method('getMetadata')->willReturnMap([['uri', $path]]); $file = $this->createMock(UploadedFileInterface::class); $file->method('getSize')->willReturn($size); diff --git a/tests/Post/PostConcurrentUpdateIntegrationTest.php b/tests/Post/PostConcurrentUpdateIntegrationTest.php index 84ffc3a..cc02775 100644 --- a/tests/Post/PostConcurrentUpdateIntegrationTest.php +++ b/tests/Post/PostConcurrentUpdateIntegrationTest.php @@ -13,6 +13,8 @@ use App\Shared\Html\HtmlSanitizerInterface; use PDO; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class PostConcurrentUpdateIntegrationTest extends TestCase { private PDO $db; diff --git a/tests/Post/PostControllerTest.php b/tests/Post/PostControllerTest.php index dc9015a..67d8095 100644 --- a/tests/Post/PostControllerTest.php +++ b/tests/Post/PostControllerTest.php @@ -27,6 +27,7 @@ use Tests\ControllerTestCase; * - update() : 404, droits insuffisants, succès, erreur de validation * - delete() : 404, droits insuffisants, succès */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class PostControllerTest extends ControllerTestCase { /** @var \Slim\Views\Twig&MockObject */ @@ -100,7 +101,7 @@ final class PostControllerTest extends ControllerTestCase public function testIndexFiltersByCategoryWhenSlugProvided(): void { $category = new Category(3, 'PHP', 'php'); - $this->categoryService->method('findBySlug')->with('php')->willReturn($category); + $this->categoryService->expects($this->once())->method('findBySlug')->with('php')->willReturn($category); $this->postService->expects($this->once()) ->method('getAllPosts') @@ -233,7 +234,7 @@ final class PostControllerTest extends ControllerTestCase public function testFormRendersFilledFormWhenUserIsAuthor(): void { $post = $this->buildPostEntity(7, 'Titre', 'Contenu', 'titre', 5); - $this->postService->method('getPostById')->with(7)->willReturn($post); + $this->postService->expects($this->once())->method('getPostById')->with(7)->willReturn($post); $this->sessionManager->method('isAdmin')->willReturn(false); $this->sessionManager->method('isEditor')->willReturn(false); $this->sessionManager->method('getUserId')->willReturn(5); diff --git a/tests/Post/PostExtensionTest.php b/tests/Post/PostExtensionTest.php index a2e3837..15b087e 100644 --- a/tests/Post/PostExtensionTest.php +++ b/tests/Post/PostExtensionTest.php @@ -8,6 +8,8 @@ use App\Post\PostExtension; use PHPUnit\Framework\TestCase; use Twig\TwigFunction; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class PostExtensionTest extends TestCase { /** @var array */ diff --git a/tests/Post/PostFtsUsernameSyncIntegrationTest.php b/tests/Post/PostFtsUsernameSyncIntegrationTest.php index c6579c3..7cb92cf 100644 --- a/tests/Post/PostFtsUsernameSyncIntegrationTest.php +++ b/tests/Post/PostFtsUsernameSyncIntegrationTest.php @@ -8,6 +8,8 @@ use App\Shared\Database\Migrator; use PDO; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class PostFtsUsernameSyncIntegrationTest extends TestCase { private PDO $db; diff --git a/tests/Post/PostModelEdgeCasesTest.php b/tests/Post/PostModelEdgeCasesTest.php index 32152c8..2d960f3 100644 --- a/tests/Post/PostModelEdgeCasesTest.php +++ b/tests/Post/PostModelEdgeCasesTest.php @@ -6,6 +6,8 @@ namespace Tests\Post; use App\Post\Post; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class PostModelEdgeCasesTest extends TestCase { public function testFromArrayKeepsMissingOptionalFieldsNull(): void diff --git a/tests/Post/PostModelTest.php b/tests/Post/PostModelTest.php index e743d8a..5964de4 100644 --- a/tests/Post/PostModelTest.php +++ b/tests/Post/PostModelTest.php @@ -7,6 +7,8 @@ use App\Post\Post; use DateTime; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class PostModelTest extends TestCase { public function testConstructAndGettersExposePostData(): void diff --git a/tests/Post/PostRepositoryTest.php b/tests/Post/PostRepositoryTest.php index 1abba26..e2b2568 100644 --- a/tests/Post/PostRepositoryTest.php +++ b/tests/Post/PostRepositoryTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; * PDO et PDOStatement sont mockés pour isoler complètement * le dépôt de la base de données. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class PostRepositoryTest extends TestCase { /** @var PDO&MockObject */ @@ -339,7 +340,7 @@ final class PostRepositoryTest extends TestCase $post = Post::fromArray($this->rowPost); $stmt = $this->stmtForWrite(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('INSERT INTO posts')) ->willReturn($stmt); @@ -403,7 +404,7 @@ final class PostRepositoryTest extends TestCase $post = Post::fromArray($this->rowPost); $stmt = $this->stmtForWrite(1); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('UPDATE posts')) ->willReturn($stmt); diff --git a/tests/Post/PostServiceTest.php b/tests/Post/PostServiceTest.php index d646163..4489a24 100644 --- a/tests/Post/PostServiceTest.php +++ b/tests/Post/PostServiceTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\TestCase; * Couvre la création, la mise à jour, la suppression et les lectures. * HtmlSanitizerInterface et PostRepository sont mockés pour isoler la logique métier. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class PostServiceTest extends TestCase { /** @var PostRepositoryInterface&MockObject */ @@ -65,7 +66,7 @@ final class PostServiceTest extends TestCase public function testGetPostsByUserIdDelegatesToRepository(): void { $posts = [$this->makePost(1, 'Titre', 'slug-titre')]; - $this->repository->method('findByUserId')->with(3, null)->willReturn($posts); + $this->repository->expects($this->once())->method('findByUserId')->with(3, null)->willReturn($posts); $this->assertSame($posts, $this->service->getPostsByUserId(3)); } @@ -181,7 +182,7 @@ final class PostServiceTest extends TestCase public function testSearchPostsDelegatesToRepository(): void { $posts = [$this->makePost(1, 'Résultat', 'resultat')]; - $this->repository->method('search')->with('mot', null, null)->willReturn($posts); + $this->repository->expects($this->once())->method('search')->with('mot', null, null)->willReturn($posts); $this->assertSame($posts, $this->service->searchPosts('mot')); } diff --git a/tests/Post/RssControllerTest.php b/tests/Post/RssControllerTest.php index 5c95929..f920ad4 100644 --- a/tests/Post/RssControllerTest.php +++ b/tests/Post/RssControllerTest.php @@ -19,6 +19,7 @@ use Tests\ControllerTestCase; * - Flux vide : XML minimal valide * - Appel à getRecentPosts() avec la constante FEED_LIMIT (20) */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class RssControllerTest extends ControllerTestCase { /** @var PostServiceInterface&MockObject */ diff --git a/tests/Shared/ClientIpResolverTest.php b/tests/Shared/ClientIpResolverTest.php index f119516..4706ec0 100644 --- a/tests/Shared/ClientIpResolverTest.php +++ b/tests/Shared/ClientIpResolverTest.php @@ -7,6 +7,8 @@ use App\Shared\Http\ClientIpResolver; use PHPUnit\Framework\TestCase; use Slim\Psr7\Factory\ServerRequestFactory; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class ClientIpResolverTest extends TestCase { public function testResolveReturnsDefaultWhenRemoteAddrMissing(): void diff --git a/tests/Shared/ConfigTest.php b/tests/Shared/ConfigTest.php index d21e6e0..189bc69 100644 --- a/tests/Shared/ConfigTest.php +++ b/tests/Shared/ConfigTest.php @@ -6,6 +6,8 @@ namespace Tests\Shared; use App\Shared\Config; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class ConfigTest extends TestCase { public function testGetTwigCacheReturnsFalseInDev(): void diff --git a/tests/Shared/DateParserTest.php b/tests/Shared/DateParserTest.php index 382e3bb..9624e6a 100644 --- a/tests/Shared/DateParserTest.php +++ b/tests/Shared/DateParserTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; * Couvre la conversion de valeurs brutes de base de données en DateTime, * ainsi que les cas silencieux (null, chaîne vide, valeur invalide). */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class DateParserTest extends TestCase { diff --git a/tests/Shared/ExtensionTest.php b/tests/Shared/ExtensionTest.php index 17c74f4..8ae3040 100644 --- a/tests/Shared/ExtensionTest.php +++ b/tests/Shared/ExtensionTest.php @@ -10,6 +10,8 @@ use PHPUnit\Framework\TestCase; use Slim\Csrf\Guard; use Slim\Psr7\Factory\ResponseFactory; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class ExtensionTest extends TestCase { protected function setUp(): void diff --git a/tests/Shared/FlashServiceConsumeTest.php b/tests/Shared/FlashServiceConsumeTest.php index 81a9bbb..c858d8d 100644 --- a/tests/Shared/FlashServiceConsumeTest.php +++ b/tests/Shared/FlashServiceConsumeTest.php @@ -6,6 +6,8 @@ namespace Tests\Shared; use App\Shared\Http\FlashService; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class FlashServiceConsumeTest extends TestCase { protected function setUp(): void diff --git a/tests/Shared/FlashServiceTest.php b/tests/Shared/FlashServiceTest.php index 1342a28..921c5af 100644 --- a/tests/Shared/FlashServiceTest.php +++ b/tests/Shared/FlashServiceTest.php @@ -6,6 +6,8 @@ namespace Tests\Shared; use App\Shared\Http\FlashService; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class FlashServiceTest extends TestCase { protected function setUp(): void diff --git a/tests/Shared/HelperEdgeCasesTest.php b/tests/Shared/HelperEdgeCasesTest.php index baaea19..a9d4db5 100644 --- a/tests/Shared/HelperEdgeCasesTest.php +++ b/tests/Shared/HelperEdgeCasesTest.php @@ -7,6 +7,8 @@ use App\Shared\Http\ClientIpResolver; use PHPUnit\Framework\TestCase; use Slim\Psr7\Factory\ServerRequestFactory; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class HelperEdgeCasesTest extends TestCase { public function testClientIpResolverFallsBackToRemoteAddr(): void diff --git a/tests/Shared/HtmlPurifierFactoryTest.php b/tests/Shared/HtmlPurifierFactoryTest.php index a3ec0a4..d05d75f 100644 --- a/tests/Shared/HtmlPurifierFactoryTest.php +++ b/tests/Shared/HtmlPurifierFactoryTest.php @@ -6,6 +6,8 @@ namespace Tests\Shared; use App\Shared\Html\HtmlPurifierFactory; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class HtmlPurifierFactoryTest extends TestCase { public function testCreateBuildsPurifierAndSanitizesDangerousHtml(): void diff --git a/tests/Shared/HtmlSanitizerTest.php b/tests/Shared/HtmlSanitizerTest.php index d492bfd..8f097c6 100644 --- a/tests/Shared/HtmlSanitizerTest.php +++ b/tests/Shared/HtmlSanitizerTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\TestCase; * Ces tests utilisent une vraie instance HTMLPurifier (pas de mock) * car c'est le comportement de purification lui-même qui est testé. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class HtmlSanitizerTest extends TestCase { private HtmlSanitizer $sanitizer; diff --git a/tests/Shared/MigratorTest.php b/tests/Shared/MigratorTest.php index 277ddc9..d980054 100644 --- a/tests/Shared/MigratorTest.php +++ b/tests/Shared/MigratorTest.php @@ -22,6 +22,7 @@ use PHPUnit\Framework\TestCase; * syncFtsIndex() requiert les tables posts, users et posts_fts — elles sont * créées minimalement avant chaque test qui en a besoin. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class MigratorTest extends TestCase { private PDO $db; diff --git a/tests/Shared/SeederTest.php b/tests/Shared/SeederTest.php index 24124f6..37d5673 100644 --- a/tests/Shared/SeederTest.php +++ b/tests/Shared/SeederTest.php @@ -18,6 +18,7 @@ use PHPUnit\Framework\TestCase; * PDO et PDOStatement sont mockés pour isoler le Seeder de la base de données. * Les variables d'environnement sont définies dans setUp() et restaurées dans tearDown(). */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class SeederTest extends TestCase { /** @var PDO&MockObject */ diff --git a/tests/Shared/SessionManagerEdgeCasesTest.php b/tests/Shared/SessionManagerEdgeCasesTest.php index 41aef3a..7e7c65c 100644 --- a/tests/Shared/SessionManagerEdgeCasesTest.php +++ b/tests/Shared/SessionManagerEdgeCasesTest.php @@ -6,6 +6,8 @@ namespace Tests\Shared; use App\Shared\Http\SessionManager; use PHPUnit\Framework\TestCase; +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] + final class SessionManagerEdgeCasesTest extends TestCase { private SessionManager $manager; diff --git a/tests/Shared/SessionManagerTest.php b/tests/Shared/SessionManagerTest.php index 1608964..07f12ea 100644 --- a/tests/Shared/SessionManagerTest.php +++ b/tests/Shared/SessionManagerTest.php @@ -18,6 +18,7 @@ use PHPUnit\Framework\TestCase; * session_status() === PHP_SESSION_ACTIVE dans SessionManager, ce qui les rend * sans effet en contexte CLI et évite toute notice PHP. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class SessionManagerTest extends TestCase { private SessionManager $manager; diff --git a/tests/Shared/SlugHelperTest.php b/tests/Shared/SlugHelperTest.php index ea2e386..7f1acdc 100644 --- a/tests/Shared/SlugHelperTest.php +++ b/tests/Shared/SlugHelperTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase; * Couvre la translittération ASCII, la normalisation en minuscules, * le remplacement des caractères non alphanumériques, et les cas limites. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class SlugHelperTest extends TestCase { diff --git a/tests/User/UserControllerTest.php b/tests/User/UserControllerTest.php index 36f960d..1546890 100644 --- a/tests/User/UserControllerTest.php +++ b/tests/User/UserControllerTest.php @@ -24,6 +24,7 @@ use Tests\ControllerTestCase; * - updateRole() : introuvable, propre rôle, cible admin, rôle invalide, succès * - delete() : introuvable, cible admin, soi-même, succès */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class UserControllerTest extends ControllerTestCase { /** @var \Slim\Views\Twig&MockObject */ diff --git a/tests/User/UserRepositoryTest.php b/tests/User/UserRepositoryTest.php index dbfaf72..50c344f 100644 --- a/tests/User/UserRepositoryTest.php +++ b/tests/User/UserRepositoryTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; * PDO et PDOStatement sont mockés pour isoler complètement * le dépôt de la base de données. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class UserRepositoryTest extends TestCase { /** @var PDO&MockObject */ @@ -258,7 +259,7 @@ final class UserRepositoryTest extends TestCase $user = User::fromArray($this->rowAlice); $stmt = $this->stmtForWrite(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('INSERT INTO users')) ->willReturn($stmt); @@ -301,7 +302,7 @@ final class UserRepositoryTest extends TestCase $newHash = password_hash('nouveaumdp', PASSWORD_BCRYPT); $stmt = $this->stmtForWrite(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('UPDATE users')) ->willReturn($stmt); @@ -322,7 +323,7 @@ final class UserRepositoryTest extends TestCase { $stmt = $this->stmtForWrite(); - $this->db->method('prepare') + $this->db->expects($this->once())->method('prepare') ->with($this->stringContains('UPDATE users')) ->willReturn($stmt); diff --git a/tests/User/UserServiceTest.php b/tests/User/UserServiceTest.php index 3fa8254..d3fc361 100644 --- a/tests/User/UserServiceTest.php +++ b/tests/User/UserServiceTest.php @@ -21,6 +21,7 @@ use PHPUnit\Framework\TestCase; * Les dépendances sont remplacées par des mocks via leurs interfaces pour * isoler le service. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class UserServiceTest extends TestCase { /** @var UserRepositoryInterface&MockObject */ @@ -186,7 +187,7 @@ final class UserServiceTest extends TestCase public function testFindByIdReturnsUser(): void { $user = $this->makeUser('alice', 'alice@example.com'); - $this->userRepository->method('findById')->with(1)->willReturn($user); + $this->userRepository->expects($this->once())->method('findById')->with(1)->willReturn($user); $this->assertSame($user, $this->service->findById(1)); } @@ -199,7 +200,7 @@ final class UserServiceTest extends TestCase */ public function testDeleteDelegatesToRepository(): void { - $this->userRepository->method('findById')->with(5)->willReturn($this->makeUser('alice', 'alice@example.com')); + $this->userRepository->expects($this->once())->method('findById')->with(5)->willReturn($this->makeUser('alice', 'alice@example.com')); $this->userRepository->expects($this->once())->method('delete')->with(5); $this->service->delete(5); @@ -213,7 +214,7 @@ final class UserServiceTest extends TestCase */ public function testUpdateRoleDelegatesToRepository(): void { - $this->userRepository->method('findById')->with(3)->willReturn($this->makeUser('alice', 'alice@example.com')); + $this->userRepository->expects($this->once())->method('findById')->with(3)->willReturn($this->makeUser('alice', 'alice@example.com')); $this->userRepository->expects($this->once()) ->method('updateRole') ->with(3, User::ROLE_EDITOR); @@ -239,7 +240,7 @@ final class UserServiceTest extends TestCase #[\PHPUnit\Framework\Attributes\DataProvider('validRolesProvider')] public function testUpdateRoleAcceptsAllValidRoles(string $role): void { - $this->userRepository->method('findById')->with(1)->willReturn($this->makeUser('alice', 'alice@example.com')); + $this->userRepository->expects($this->once())->method('findById')->with(1)->willReturn($this->makeUser('alice', 'alice@example.com')); $this->userRepository->expects($this->once())->method('updateRole')->with(1, $role); $this->service->updateRole(1, $role); diff --git a/tests/User/UserTest.php b/tests/User/UserTest.php index 9310616..3ce76e9 100644 --- a/tests/User/UserTest.php +++ b/tests/User/UserTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; * Vérifie la construction, la validation, les accesseurs * et l'hydratation depuis un tableau de base de données. */ +#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] final class UserTest extends TestCase {