Working state but no uploads

This commit is contained in:
julien
2026-03-16 02:33:18 +01:00
parent cc3fbdc830
commit 55d2da9f2f
52 changed files with 121 additions and 49 deletions

1
.gitignore vendored
View File

@@ -24,6 +24,7 @@ public/assets/
database/*.sqlite
database/*.sqlite-shm
database/*.sqlite-wal
database/.provision.lock
# ============================================
# Cache & Logs

View File

@@ -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<string, mixed>|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<string, mixed>|null
*/
public function findActiveByHash(string $tokenHash): ?array;
public function invalidateByUserId(int $userId): void;
/**
* @return array<string, mixed>|null
*/
public function consumeActiveToken(string $tokenHash, string $usedAt): ?array;
}

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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');

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 */

View File

@@ -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

View File

@@ -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);

View File

@@ -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'));
}

View File

@@ -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 ────────────────────────────────────────────────────

View File

@@ -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 */

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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));

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<string, TwigFunction> */

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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'));
}

View File

@@ -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 */

View File

@@ -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

View File

@@ -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

View File

@@ -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
{

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 */

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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 */

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
{