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
25 changes: 18 additions & 7 deletions src/App/src/Fixture/PostLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,20 @@ public function load(ObjectManager $manager): void
}

/** @var Author $author */
$author = $this->getReference('author_' . $authorSlug, Author::class);
$title = html_entity_decode($articleData['post_title'], ENT_QUOTES, 'UTF-8');
$slug = $this->slugify($title);
$author = $this->getReference('author_' . $authorSlug, Author::class);
$title = html_entity_decode($articleData['post_title'], ENT_QUOTES, 'UTF-8');
$autoSlug = $this->slugify($title);

if (isset($usedSlugs[$slug])) {
$usedSlugs[$slug]++;
$slug .= '-' . $usedSlugs[$slug];
if (isset($usedSlugs[$autoSlug])) {
$usedSlugs[$autoSlug]++;
$autoSlug .= '-' . $usedSlugs[$autoSlug];
} else {
$usedSlugs[$slug] = 1;
$usedSlugs[$autoSlug] = 1;
}

$customSlug = trim((string) ($articleData['post_slug'] ?? ''));
$slug = $customSlug !== '' ? $customSlug : $autoSlug;

$status = match ($articleData['post_status']) {
'published' => PostStatusEnum::Published,
'private' => PostStatusEnum::Private,
Expand All @@ -85,6 +88,10 @@ public function load(ObjectManager $manager): void
$openGraphImg = $articleData['opengraph_img'] ?? null;

$article = $repository->findOneBy(['slug' => $slug]);
if ($article === null && $slug !== $autoSlug) {
// post was imported before post_slug was set: find it by the title-based slug
$article = $repository->findOneBy(['slug' => $autoSlug]);
}

if ($article === null) {
$article = new Post();
Expand All @@ -105,6 +112,10 @@ public function load(ObjectManager $manager): void
} else {
$changed = false;

if ($article->getSlug() !== $slug) {
$article->setSlug($slug);
$changed = true;
}
if ($article->getTitle() !== $title) {
$article->setTitle($title);
$changed = true;
Expand Down
1 change: 1 addition & 0 deletions src/App/src/Fixture/articles_cleaned.json
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,7 @@
},
{
"post_title": "Autologin using Cookie / Remember Me in Dotkernel",
"post_slug": "autologin-cookie-remember-me-feature",
"post_date": "2022-07-18 09:12:59",
"post_status": "published",
"author": {
Expand Down
202 changes: 202 additions & 0 deletions test/Unit/App/Fixture/PostLoaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?php

declare(strict_types=1);

namespace LightTest\Unit\App\Fixture;

use DateTimeImmutable;
use Doctrine\Common\DataFixtures\ReferenceRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\UnitOfWork;
use Light\App\Fixture\AuthorLoader;
use Light\App\Fixture\CategoryLoader;
use Light\App\Fixture\PostLoader;
use Light\Blog\Entity\Author;
use Light\Blog\Entity\Category;
use Light\Blog\Entity\Post;
use LightTest\Unit\UnitTest;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;

use function array_count_values;
use function dirname;
use function file_get_contents;
use function json_decode;
use function preg_quote;
use function preg_replace;
use function strtolower;
use function trim;

/**
* Runs PostLoader against the real articles_cleaned.json, using the
* "Autologin using Cookie / Remember Me in Dotkernel" article that sets post_slug.
*/
class PostLoaderTest extends UnitTest
{
private const string TITLE = 'Autologin using Cookie / Remember Me in Dotkernel';
private const string CUSTOM_SLUG = 'autologin-cookie-remember-me-feature';
private const string TITLE_SLUG = 'autologin-using-cookie-remember-me-in-dotkernel';

/**
* @throws Exception
*/
public function testLoadCreatesPostWithCustomSlugWhenNothingMatchesInDb(): void
{
$lookups = [];
$manager = $this->createEntityManager($this->createRepository([], $lookups));

$persistedSlugs = [];
$manager->expects($this->atLeastOnce())->method('persist')
->willReturnCallback(function (Post $post) use (&$persistedSlugs): void {
$persistedSlugs[$post->getTitle()] = $post->getSlug();
});

$this->expectOutputRegex('/CREATE: ' . preg_quote(self::TITLE, '/') . '\n/');
$this->runLoader($manager);

$this->assertSame(self::CUSTOM_SLUG, $persistedSlugs[self::TITLE]);
$this->assertNotContains(self::TITLE_SLUG, $persistedSlugs);

// Not found by post_slug, so the title-based slug is tried before creating.
$counts = array_count_values($lookups);
$this->assertSame(1, $counts[self::CUSTOM_SLUG]);
$this->assertSame(1, $counts[self::TITLE_SLUG]);
}

/**
* @throws Exception
*/
public function testLoadUpdatesPostFoundByCustomSlug(): void
{
$existing = $this->existingPost(self::CUSTOM_SLUG, 'Old title');

$lookups = [];
$manager = $this->createEntityManager($this->createRepository([self::CUSTOM_SLUG => $existing], $lookups));
$manager->expects($this->atLeastOnce())->method('flush');

$this->expectOutputRegex('/UPDATE: ' . preg_quote(self::TITLE, '/') . '\n/');
$this->runLoader($manager);

$this->assertSame(self::CUSTOM_SLUG, $existing->getSlug());
$this->assertSame(self::TITLE, $existing->getTitle());
$this->assertNotContains(self::TITLE_SLUG, $lookups);
}

/**
* @throws Exception
*/
public function testLoadRenamesPostImportedUnderTitleSlugToCustomSlug(): void
{
// Imported before post_slug was added to the JSON.
$existing = $this->existingPost(self::TITLE_SLUG, self::TITLE);

$lookups = [];
$manager = $this->createEntityManager($this->createRepository([self::TITLE_SLUG => $existing], $lookups));
$manager->expects($this->atLeastOnce())->method('flush');

$this->expectOutputRegex('/UPDATE: ' . preg_quote(self::TITLE, '/') . '\n/');
$this->runLoader($manager);

$this->assertSame(self::CUSTOM_SLUG, $existing->getSlug());
}

public function testDependenciesAndOrder(): void
{
$loader = new PostLoader();

$this->assertSame([AuthorLoader::class, CategoryLoader::class], $loader->getDependencies());
$this->assertSame(3, $loader->getOrder());
}

private function existingPost(string $slug, string $title): Post
{
$post = new Post();
$post->setSlug($slug);
$post->setTitle($title);
$post->setPostDate(new DateTimeImmutable());
$post->setCategory(new Category());
$post->setAuthor(new Author());
$post->setExcerpt('');

return $post;
}

/**
* @param array<string, Post> $postsBySlug
* @param list<string> $lookups
* @return EntityRepository<Post>
* @throws Exception
*/
private function createRepository(array $postsBySlug, array &$lookups): EntityRepository
{
$repository = $this->createStub(EntityRepository::class);
$repository->method('findOneBy')->willReturnCallback(
function (array $criteria) use ($postsBySlug, &$lookups): ?Post {
$lookups[] = $criteria['slug'];
return $postsBySlug[$criteria['slug']] ?? null;
}
);

return $repository;
}

/**
* Registers the category and author references the real articles_cleaned.json needs.
*/
private function runLoader(EntityManagerInterface $manager): void
{
$contents = file_get_contents(dirname(__DIR__, 4) . '/src/App/src/Fixture/articles_cleaned.json');
$this->assertIsString($contents);

/** @var list<array{slug: string, articles: list<array{author?: array{display_name?: string}}>}> $categories */
$categories = json_decode($contents, true);

$referenceRepository = new ReferenceRepository($manager);
foreach ($categories as $category) {
$referenceRepository->setReference('category_' . $category['slug'], new Category());

foreach ($category['articles'] as $article) {
$authorName = $article['author']['display_name'] ?? null;
if ($authorName !== null) {
$referenceRepository->setReference('author_' . $this->slugify($authorName), new Author());
}
}
}

$loader = new PostLoader();
$loader->setReferenceRepository($referenceRepository);
$loader->load($manager);
}

private function slugify(string $text): string
{
$text = strtolower(trim($text));
$text = preg_replace('/[^a-z0-9]+/', '-', $text) ?? '';
return trim($text, '-');
}

/**
* @param EntityRepository<Post> $repository
* @throws Exception
*/
private function createEntityManager(EntityRepository $repository): EntityManagerInterface&MockObject
{
$manager = $this->createMock(EntityManagerInterface::class);
$manager->method('getRepository')->willReturn($repository);
$manager->method('contains')->willReturn(true);

$unitOfWork = $this->createStub(UnitOfWork::class);
$unitOfWork->method('isInIdentityMap')->willReturn(false);
$manager->method('getUnitOfWork')->willReturn($unitOfWork);

$manager->method('getClassMetadata')->willReturnCallback(function (string $class) {
$metadata = $this->createStub(ClassMetadata::class);
$metadata->method('getName')->willReturn($class);
return $metadata;
});

return $manager;
}
}
Loading