Files
slim-blog/tests/Post/RssControllerTest.php
2026-03-16 02:33:18 +01:00

184 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Post;
use App\Post\Post;
use App\Post\PostServiceInterface;
use App\Post\RssController;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\ControllerTestCase;
/**
* Tests unitaires pour RssController.
*
* Couvre feed() :
* - Content-Type application/rss+xml
* - Structure XML valide (balises channel obligatoires)
* - Articles inclus dans le flux (titre, lien, guid)
* - 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 */
private PostServiceInterface $postService;
private RssController $controller;
private const APP_URL = 'https://example.com';
private const APP_NAME = 'Mon Blog';
protected function setUp(): void
{
$this->postService = $this->createMock(PostServiceInterface::class);
$this->controller = new RssController(
$this->postService,
self::APP_URL,
self::APP_NAME,
);
}
/**
* feed() doit retourner un Content-Type application/rss+xml.
*/
public function testFeedReturnsRssContentType(): void
{
$this->postService->method('getRecentPosts')->willReturn([]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$this->assertStringContainsString('application/rss+xml', $res->getHeaderLine('Content-Type'));
}
/**
* feed() doit retourner un XML valide même si aucun article n'existe.
*/
public function testFeedReturnsValidXmlWhenNoPostsExist(): void
{
$this->postService->method('getRecentPosts')->willReturn([]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$body = (string) $res->getBody();
$xml = simplexml_load_string($body);
$this->assertNotFalse($xml, 'Le corps de la réponse doit être du XML valide');
$this->assertSame('2.0', (string) $xml['version']);
}
/**
* feed() doit inclure les balises channel obligatoires (title, link, description).
*/
public function testFeedIncludesRequiredChannelElements(): void
{
$this->postService->method('getRecentPosts')->willReturn([]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$xml = simplexml_load_string((string) $res->getBody());
$this->assertNotFalse($xml);
$channel = $xml->channel;
$this->assertSame(self::APP_NAME, (string) $channel->title);
$this->assertNotEmpty((string) $channel->link);
$this->assertNotEmpty((string) $channel->description);
}
/**
* feed() doit inclure un item par article avec title, link et guid.
*/
public function testFeedIncludesOneItemPerPost(): void
{
$post = new Post(1, 'Titre test', 'Contenu de test', 'titre-test', 1, 'alice');
$this->postService->method('getRecentPosts')->willReturn([$post]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$xml = simplexml_load_string((string) $res->getBody());
$this->assertNotFalse($xml);
$items = $xml->channel->item;
$this->assertCount(1, $items);
$this->assertSame('Titre test', (string) $items[0]->title);
$this->assertStringContainsString('titre-test', (string) $items[0]->link);
$this->assertSame((string) $items[0]->link, (string) $items[0]->guid);
}
/**
* feed() doit tronquer le contenu à 300 caractères dans la description.
*/
public function testFeedTruncatesLongContentTo300Chars(): void
{
$longContent = str_repeat('a', 400);
$post = new Post(1, 'Titre', $longContent, 'titre', 1);
$this->postService->method('getRecentPosts')->willReturn([$post]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$xml = simplexml_load_string((string) $res->getBody());
$this->assertNotFalse($xml);
$description = (string) $xml->channel->item[0]->description;
// 300 caractères + '…' = 301 octets UTF-8 pour le contenu visible
$this->assertLessThanOrEqual(302, mb_strlen($description));
$this->assertStringEndsWith('…', $description);
}
/**
* feed() doit appeler getRecentPosts() avec la limite de 20 articles.
*/
public function testFeedRequestsTwentyRecentPosts(): void
{
$this->postService->expects($this->once())
->method('getRecentPosts')
->with(20)
->willReturn([]);
$this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
}
/**
* feed() doit inclure la balise author si l'article a un auteur.
*/
public function testFeedIncludesAuthorWhenPresent(): void
{
$post = new Post(1, 'Titre', 'Contenu', 'titre', 1, 'alice');
$this->postService->method('getRecentPosts')->willReturn([$post]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$xml = simplexml_load_string((string) $res->getBody());
$this->assertNotFalse($xml);
$this->assertSame('alice', (string) $xml->channel->item[0]->author);
}
/**
* feed() ne doit pas inclure la balise author si l'auteur est null.
*/
public function testFeedOmitsAuthorWhenNull(): void
{
$post = new Post(1, 'Titre', 'Contenu', 'titre', null, null);
$this->postService->method('getRecentPosts')->willReturn([$post]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$xml = simplexml_load_string((string) $res->getBody());
$this->assertNotFalse($xml);
$this->assertCount(0, $xml->channel->item[0]->author);
}
/**
* feed() doit construire les URLs des articles en utilisant APP_URL.
*/
public function testFeedBuildsPostUrlsWithAppUrl(): void
{
$post = new Post(1, 'Titre', 'Contenu', 'mon-slug', 1);
$this->postService->method('getRecentPosts')->willReturn([$post]);
$res = $this->controller->feed($this->makeGet('/rss.xml'), $this->makeResponse());
$xml = simplexml_load_string((string) $res->getBody());
$this->assertNotFalse($xml);
$this->assertStringStartsWith(self::APP_URL, (string) $xml->channel->item[0]->link);
}
}