Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Blog/src/Entity/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ public function setSlug(string $slug): void
$this->slug = $slug;
}

/**
* Visibility only controls the category sidebar widget (and the 404/410 pages, post, tag, author
* and page views that render it), via CategoryRepository::getCategories().
*
* It does not hide a category that has published posts: those are still listed on /categories/
* and in the sitemap (CategoryRepository::getCategoriesWithPublishedPosts()), and their
* /category/{slug}/ page is served.
*/
public function isVisible(): bool
{
return $this->isVisible;
Expand Down
2 changes: 1 addition & 1 deletion src/Blog/src/Handler/GetCategoryCollectionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(

public function handle(ServerRequestInterface $request): ResponseInterface
{
$categories = $this->categoryRepository->getCategories();
$categories = $this->categoryRepository->getCategoriesWithPublishedPosts();

return new HtmlResponse(
$this->template->render('page::categories', [
Expand Down
2 changes: 1 addition & 1 deletion src/Blog/src/Handler/GetCategoryResourceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
if ($category === null) {
return $this->blogService->notFound($categories);
}
if (! $category->isVisible()) {
if ($category->getPublishedPostsCount() === 0) {
return $this->blogService->gone($categories);
}
$meta = $category;
Expand Down
4 changes: 1 addition & 3 deletions src/Blog/src/Repository/CategoryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ public function getCategoriesWithPublishedPosts(): array
->from(Category::class, 'categories');

$qb->where($qb->expr()->in('categories.id', $publishedCategoryIds->getDQL()))
->andWhere('categories.isVisible = :visible')
->setParameter('published', PostStatusEnum::Published)
->setParameter('visible', true);
->setParameter('published', PostStatusEnum::Published);

return $qb->getQuery()->getResult();
}
Expand Down
65 changes: 65 additions & 0 deletions test/Unit/Blog/Handler/GetCategoryCollectionHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace LightTest\Unit\Blog\Handler;

use Laminas\Diactoros\ServerRequest;
use Light\Blog\Entity\Category;
use Light\Blog\Handler\GetCategoryCollectionHandler;
use Light\Blog\Repository\CategoryRepository;
use LightTest\Unit\UnitTest;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\MockObject\Exception;

class GetCategoryCollectionHandlerTest extends UnitTest
{
/**
* Visibility must not decide what /categories/ lists: only categories with published posts do.
*
* @throws Exception
*/
public function testHandleListsCategoriesWithPublishedPostsRegardlessOfVisibility(): void
{
$hiddenWithPosts = new Category();
$hiddenWithPosts->setVisibility(false);
$categories = [$hiddenWithPosts];

$categoryRepository = $this->createMock(CategoryRepository::class);
$categoryRepository->expects($this->once())
->method('getCategoriesWithPublishedPosts')
->willReturn($categories);
$categoryRepository->expects($this->never())->method('getCategories');

$template = $this->createMock(TemplateRendererInterface::class);
$template->expects($this->once())
->method('render')
->with('page::categories', ['categories' => $categories])
->willReturn('<html lang="en"></html>');

$response = (new GetCategoryCollectionHandler($template, $categoryRepository))
->handle(new ServerRequest());

$this->assertSame(200, $response->getStatusCode());
}

/**
* @throws Exception
*/
public function testHandleRendersEmptyListWhenNoCategoryHasPublishedPosts(): void
{
$categoryRepository = $this->createStub(CategoryRepository::class);
$categoryRepository->method('getCategoriesWithPublishedPosts')->willReturn([]);

$template = $this->createMock(TemplateRendererInterface::class);
$template->expects($this->once())
->method('render')
->with('page::categories', ['categories' => []])
->willReturn('');

$response = (new GetCategoryCollectionHandler($template, $categoryRepository))
->handle(new ServerRequest());

$this->assertSame(200, $response->getStatusCode());
}
}
75 changes: 74 additions & 1 deletion test/Unit/Blog/Handler/GetCategoryResourceHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace LightTest\Unit\Blog\Handler;

use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\ServerRequest;
Expand Down Expand Up @@ -31,16 +33,87 @@ public function testHandleReturnsNotFoundWhenCategoryDoesNotExist(): void
/**
* @throws Exception
*/
public function testHandleReturnsGoneWhenCategoryIsNotVisible(): void
public function testHandleReturnsGoneWhenCategoryIsNotVisibleAndHasNoPublishedPosts(): void
{
$category = $this->createStub(Category::class);
$category->method('isVisible')->willReturn(false);
$category->method('getPublishedPostsCount')->willReturn(0);

$response = $this->handle($category);

$this->assertSame(410, $response->getStatusCode());
}

/**
* @throws Exception
*/
public function testHandleReturnsGoneWhenVisibleCategoryHasNoPublishedPosts(): void
{
$category = $this->createStub(Category::class);
$category->method('isVisible')->willReturn(true);
$category->method('getPublishedPostsCount')->willReturn(0);

$response = $this->handle($category);

$this->assertSame(410, $response->getStatusCode());
}

/**
* A hidden category is listed on /categories/ when it has published posts, so its page must be served.
*
* @throws Exception
*/
public function testHandleRendersHiddenCategoryThatHasPublishedPosts(): void
{
$category = $this->createStub(Category::class);
$category->method('isVisible')->willReturn(false);
$category->method('getPublishedPostsCount')->willReturn(2);

$this->assertSame(200, $this->handleRendering($category)->getStatusCode());
}

/**
* @throws Exception
*/
public function testHandleRendersVisibleCategoryThatHasPublishedPosts(): void
{
$category = $this->createStub(Category::class);
$category->method('isVisible')->willReturn(true);
$category->method('getPublishedPostsCount')->willReturn(1);

$this->assertSame(200, $this->handleRendering($category)->getStatusCode());
}

/**
* @throws Exception
*/
private function handleRendering(Category $category): ResponseInterface
{
$query = $this->createStub(Query::class);
$query->method('getResult')->willReturn([]);

$paginator = $this->createStub(DoctrinePaginator::class);
$paginator->method('count')->willReturn(0);
$paginator->method('getQuery')->willReturn($query);

$categoryRepository = $this->createStub(CategoryRepository::class);
$categoryRepository->method('getCategories')->willReturn([]);
$categoryRepository->method('getCategoryResource')->willReturn($category);
$categoryRepository->method('getCategoryPost')->willReturn($paginator);

$template = $this->createMock(TemplateRendererInterface::class);
$template->expects($this->once())->method('render')
->with('page::category-resource', $this->anything())
->willReturn('<html lang="en"></html>');

$blogService = $this->createMock(BlogServiceInterface::class);
$blogService->expects($this->never())->method('gone');
$blogService->expects($this->never())->method('notFound');

return (new GetCategoryResourceHandler($template, $categoryRepository, $blogService))
->handle((new ServerRequest())->withAttribute('slug', 'a-category'));
}

/**
* @throws Exception
*/
Expand Down
151 changes: 151 additions & 0 deletions test/Unit/Blog/Repository/CategoryRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

declare(strict_types=1);

namespace LightTest\Unit\Blog\Repository;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\QueryBuilder;
use Light\Blog\Entity\Category;
use Light\Blog\Enum\PostStatusEnum;
use Light\Blog\Repository\CategoryRepository;
use LightTest\Unit\UnitTest;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;

class CategoryRepositoryTest extends UnitTest
{
/**
* @throws Exception
*/
public function testGetCategoriesWithPublishedPostsDoesNotFilterOnVisibility(): void
{
$categories = [new Category()];
$query = $this->createChainableQuery();
$query->expects($this->once())->method('getResult')->willReturn($categories);

$dql = null;
$entityManager = $this->createEntityManager(function (string $queryDql) use (&$dql, $query): Query {
$dql = $queryDql;

return $query;
});

$result = $this->createRepository($entityManager)->getCategoriesWithPublishedPosts();

$this->assertSame($categories, $result);
$this->assertIsString($dql);
$this->assertStringNotContainsString('isVisible', $dql);
}

/**
* Only published posts count: a category with just drafts, private or archived posts is not listed.
*
* @throws Exception
*/
public function testGetCategoriesWithPublishedPostsOnlyConsidersPublishedPosts(): void
{
$query = $this->createChainableQuery();
$query->expects($this->once())->method('getResult')->willReturn([]);

$dql = null;
$entityManager = $this->createEntityManager(function (string $queryDql) use (&$dql, $query): Query {
$dql = $queryDql;

return $query;
});

$this->createRepository($entityManager)->getCategoriesWithPublishedPosts();

$this->assertIsString($dql);
$this->assertStringContainsString('post.status = :published', $dql);
$this->assertStringContainsString('categories.id IN', $dql);
}

/**
* @throws Exception
*/
public function testGetCategoriesWithPublishedPostsBindsThePublishedStatus(): void
{
$query = $this->createChainableQuery();
$query->method('getResult')->willReturn([]);
$query->expects($this->once())->method('setParameters')->with($this->callback(
function (ArrayCollection $parameters): bool {
$parameter = $parameters->first();

return $parameters->count() === 1
&& $parameter instanceof Parameter
&& $parameter->getName() === 'published'
&& $parameter->getValue() === PostStatusEnum::Published;
}
))->willReturnSelf();

$entityManager = $this->createEntityManager(static fn (): Query => $query);

$this->createRepository($entityManager)->getCategoriesWithPublishedPosts();
}

/**
* @throws Exception
*/
public function testGetCategoriesStillFiltersOnVisibility(): void
{
$query = $this->createChainableQuery();
$query->expects($this->once())->method('getResult')->willReturn([]);

$dql = null;
$entityManager = $this->createEntityManager(function (string $queryDql) use (&$dql, $query): Query {
$dql = $queryDql;

return $query;
});

$this->createRepository($entityManager)->getCategories();

$this->assertIsString($dql);
$this->assertStringContainsString('categories.isVisible = :visible', $dql);
}

/**
* Entity manager whose query builders are real (so DQL is built for real) and whose
* createQuery() hands the generated DQL to $onCreateQuery.
*
* @param callable(string): Query $onCreateQuery
* @throws Exception
*/
private function createEntityManager(callable $onCreateQuery): EntityManagerInterface
{
$entityManager = $this->createStub(EntityManagerInterface::class);
$entityManager->method('createQueryBuilder')
->willReturnCallback(static fn (): QueryBuilder => new QueryBuilder($entityManager));
$entityManager->method('getExpressionBuilder')->willReturn(new Expr());
$entityManager->method('createQuery')
->willReturnCallback(static fn (string $dql = ''): Query => $onCreateQuery($dql));

return $entityManager;
}

/**
* @throws Exception
*/
private function createChainableQuery(): Query&MockObject
{
$query = $this->createMock(Query::class);
$query->method('setParameters')->willReturnSelf();
$query->method('setParameter')->willReturnSelf();
$query->method('setFirstResult')->willReturnSelf();
$query->method('setMaxResults')->willReturnSelf();

return $query;
}

private function createRepository(EntityManagerInterface $entityManager): CategoryRepository
{
return new CategoryRepository($entityManager, new ClassMetadata(Category::class));
}
}
Loading