Move article save/delete logic from monolithic factory to ArticleRepository with DI-based controller actions, following the established refactoring pattern. - ArticleRepository: add save() with 9 private helpers, archive() method - ArticlesController: add save() and delete() actions with DI - Factory methods delegate to repository (backward compatibility) - Router: add article_save/article_delete action mappings - Old controls methods marked @deprecated - 59 tests, 123 assertions passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
396 lines
14 KiB
PHP
396 lines
14 KiB
PHP
<?php
|
|
namespace Tests\Unit\Domain\Article;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Domain\Article\ArticleRepository;
|
|
|
|
class ArticleRepositoryTest extends TestCase
|
|
{
|
|
public function testFindReturnsArticleWithRelations(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('get')
|
|
->with('pp_articles', '*', ['id' => 7])
|
|
->willReturn(['id' => 7, 'status' => 1]);
|
|
|
|
$mockDb->expects($this->exactly(4))
|
|
->method('select')
|
|
->willReturnOnConsecutiveCalls(
|
|
[
|
|
['lang_id' => 'pl', 'title' => 'Artykul'],
|
|
['lang_id' => 'en', 'title' => 'Article'],
|
|
],
|
|
[
|
|
['id' => 10, 'src' => '/img/a.jpg']
|
|
],
|
|
[
|
|
['id' => 20, 'src' => '/files/a.pdf']
|
|
],
|
|
[1, 2]
|
|
);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$article = $repository->find(7);
|
|
|
|
$this->assertIsArray($article);
|
|
$this->assertEquals(7, $article['id']);
|
|
$this->assertArrayHasKey('languages', $article);
|
|
$this->assertEquals('Artykul', $article['languages']['pl']['title']);
|
|
$this->assertCount(1, $article['images']);
|
|
$this->assertCount(1, $article['files']);
|
|
$this->assertEquals([1, 2], $article['pages']);
|
|
}
|
|
|
|
public function testFindReturnsNullWhenArticleDoesNotExist(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('get')
|
|
->with('pp_articles', '*', ['id' => 999])
|
|
->willReturn(false);
|
|
$mockDb->expects($this->never())->method('select');
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$article = $repository->find(999);
|
|
|
|
$this->assertNull($article);
|
|
}
|
|
|
|
public function testDeleteNonassignedFilesDeletesDbRows(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('select')
|
|
->with('pp_articles_files', '*', ['article_id' => null])
|
|
->willReturn([
|
|
['id' => 1, 'src' => '/this/path/does/not/exist-file.tmp']
|
|
]);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('delete')
|
|
->with('pp_articles_files', ['article_id' => null]);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$repository->deleteNonassignedFiles();
|
|
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testDeleteNonassignedImagesDeletesDbRows(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('select')
|
|
->with('pp_articles_images', '*', ['article_id' => null])
|
|
->willReturn([
|
|
['id' => 1, 'src' => '/this/path/does/not/exist-image.tmp']
|
|
]);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('delete')
|
|
->with('pp_articles_images', ['article_id' => null]);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$repository->deleteNonassignedImages();
|
|
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
private function getSampleData(): array
|
|
{
|
|
return [
|
|
'title' => ['pl' => 'Testowy artykul', 'en' => 'Test article'],
|
|
'main_image' => ['pl' => '/img/pl.jpg', 'en' => ''],
|
|
'entry' => ['pl' => 'Wstep', 'en' => 'Entry'],
|
|
'text' => ['pl' => 'Tresc', 'en' => 'Content'],
|
|
'table_of_contents' => ['pl' => '', 'en' => ''],
|
|
'status' => 'on',
|
|
'show_title' => 'on',
|
|
'show_table_of_contents' => '',
|
|
'show_date_add' => 'on',
|
|
'date_add' => '',
|
|
'show_date_modify' => '',
|
|
'date_modify' => '',
|
|
'seo_link' => ['pl' => 'testowy-artykul', 'en' => 'test-article'],
|
|
'meta_title' => ['pl' => 'Meta PL', 'en' => ''],
|
|
'meta_description' => ['pl' => '', 'en' => ''],
|
|
'meta_keywords' => ['pl' => '', 'en' => ''],
|
|
'layout_id' => '2',
|
|
'pages' => ['1', '3'],
|
|
'noindex' => ['pl' => '', 'en' => 'on'],
|
|
'repeat_entry' => '',
|
|
'copy_from' => ['pl' => '', 'en' => ''],
|
|
'social_icons' => 'on',
|
|
'block_direct_access' => ['pl' => '', 'en' => ''],
|
|
];
|
|
}
|
|
|
|
public function testSaveCreatesNewArticle(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$data = $this->getSampleData();
|
|
|
|
$insertCalls = [];
|
|
$mockDb->method('insert')
|
|
->willReturnCallback(function ($table, $row) use (&$insertCalls) {
|
|
$insertCalls[] = ['table' => $table, 'row' => $row];
|
|
return true;
|
|
});
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('id')
|
|
->willReturn(42);
|
|
|
|
$mockDb->method('select')->willReturn([]);
|
|
$mockDb->method('max')->willReturn(5);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$result = $repository->save(0, $data, 1);
|
|
|
|
$this->assertEquals(42, $result);
|
|
|
|
// Verify article insert
|
|
$articleInsert = $insertCalls[0];
|
|
$this->assertEquals('pp_articles', $articleInsert['table']);
|
|
$this->assertEquals(1, $articleInsert['row']['status']);
|
|
$this->assertEquals(1, $articleInsert['row']['show_title']);
|
|
$this->assertEquals(2, $articleInsert['row']['layout_id']);
|
|
$this->assertArrayHasKey('date_add', $articleInsert['row']);
|
|
}
|
|
|
|
public function testSaveReturnsZeroWhenInsertFails(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->method('insert');
|
|
$mockDb->method('id')->willReturn(null);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$result = $repository->save(0, $this->getSampleData(), 1);
|
|
|
|
$this->assertEquals(0, $result);
|
|
}
|
|
|
|
public function testSaveUpdatesExistingArticle(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$data = $this->getSampleData();
|
|
|
|
$updateCalls = [];
|
|
$mockDb->method('update')
|
|
->willReturnCallback(function ($table, $row, $where = null) use (&$updateCalls) {
|
|
$updateCalls[] = ['table' => $table, 'row' => $row, 'where' => $where];
|
|
return true;
|
|
});
|
|
|
|
$mockDb->method('get')->willReturn(99);
|
|
$mockDb->method('select')->willReturn([]);
|
|
$mockDb->method('max')->willReturn(0);
|
|
$mockDb->method('insert')->willReturn(true);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$result = $repository->save(10, $data, 1);
|
|
|
|
$this->assertEquals(10, $result);
|
|
|
|
// Verify article update
|
|
$articleUpdate = $updateCalls[0];
|
|
$this->assertEquals('pp_articles', $articleUpdate['table']);
|
|
$this->assertEquals(1, $articleUpdate['row']['status']);
|
|
$this->assertArrayNotHasKey('date_add', $articleUpdate['row']);
|
|
$this->assertEquals(['id' => 10], $articleUpdate['where']);
|
|
}
|
|
|
|
public function testSaveTranslationsInsertsForNewArticle(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$data = $this->getSampleData();
|
|
|
|
// 1 insert for pp_articles + 2 inserts for translations (pl, en) + 2 inserts for pages
|
|
$insertCalls = [];
|
|
$mockDb->method('insert')
|
|
->willReturnCallback(function ($table, $row) use (&$insertCalls) {
|
|
$insertCalls[] = ['table' => $table, 'row' => $row];
|
|
return true;
|
|
});
|
|
|
|
$mockDb->method('id')->willReturn(50);
|
|
$mockDb->method('select')->willReturn([]);
|
|
$mockDb->method('max')->willReturn(0);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$repository->save(0, $data, 1);
|
|
|
|
$langInserts = array_filter($insertCalls, function ($c) {
|
|
return $c['table'] === 'pp_articles_langs';
|
|
});
|
|
|
|
$this->assertCount(2, $langInserts);
|
|
|
|
$plInsert = array_values(array_filter($langInserts, function ($c) {
|
|
return $c['row']['lang_id'] === 'pl';
|
|
}))[0]['row'];
|
|
|
|
$this->assertEquals(50, $plInsert['article_id']);
|
|
$this->assertEquals('Testowy artykul', $plInsert['title']);
|
|
$this->assertEquals('/img/pl.jpg', $plInsert['main_image']);
|
|
}
|
|
|
|
public function testSaveTranslationsUpsertsForExistingArticle(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$data = $this->getSampleData();
|
|
|
|
// get returns translation ID for 'pl', null for 'en'
|
|
$mockDb->method('get')
|
|
->willReturnOnConsecutiveCalls(100, null);
|
|
|
|
$updateCalls = [];
|
|
$mockDb->method('update')
|
|
->willReturnCallback(function ($table, $row, $where = null) use (&$updateCalls) {
|
|
$updateCalls[] = ['table' => $table, 'row' => $row, 'where' => $where];
|
|
return true;
|
|
});
|
|
|
|
$insertCalls = [];
|
|
$mockDb->method('insert')
|
|
->willReturnCallback(function ($table, $row) use (&$insertCalls) {
|
|
$insertCalls[] = ['table' => $table, 'row' => $row];
|
|
return true;
|
|
});
|
|
|
|
$mockDb->method('select')->willReturn([]);
|
|
$mockDb->method('max')->willReturn(0);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$repository->save(10, $data, 1);
|
|
|
|
// pl should be updated (translation_id=100)
|
|
$langUpdates = array_filter($updateCalls, function ($c) {
|
|
return $c['table'] === 'pp_articles_langs';
|
|
});
|
|
$this->assertCount(1, $langUpdates);
|
|
|
|
// en should be inserted (no existing translation)
|
|
$langInserts = array_filter($insertCalls, function ($c) {
|
|
return $c['table'] === 'pp_articles_langs';
|
|
});
|
|
$this->assertCount(1, $langInserts);
|
|
}
|
|
|
|
public function testSavePagesForNewArticle(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$data = $this->getSampleData();
|
|
$data['pages'] = ['5', '8'];
|
|
|
|
$insertCalls = [];
|
|
$mockDb->method('insert')
|
|
->willReturnCallback(function ($table, $row) use (&$insertCalls) {
|
|
$insertCalls[] = ['table' => $table, 'row' => $row];
|
|
return true;
|
|
});
|
|
|
|
$mockDb->method('id')->willReturn(60);
|
|
$mockDb->method('select')->willReturn([]);
|
|
$mockDb->method('max')->willReturn(10);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$repository->save(0, $data, 1);
|
|
|
|
$pageInserts = array_filter($insertCalls, function ($c) {
|
|
return $c['table'] === 'pp_articles_pages';
|
|
});
|
|
|
|
$this->assertCount(2, $pageInserts);
|
|
|
|
$pageIds = array_map(function ($c) {
|
|
return $c['row']['page_id'];
|
|
}, array_values($pageInserts));
|
|
|
|
$this->assertContains(5, $pageIds);
|
|
$this->assertContains(8, $pageIds);
|
|
}
|
|
|
|
public function testSaveDeletesMarkedImagesOnUpdate(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$data = $this->getSampleData();
|
|
$data['pages'] = null;
|
|
|
|
$mockDb->method('update')->willReturn(true);
|
|
$mockDb->method('get')->willReturn(null);
|
|
$mockDb->method('max')->willReturn(0);
|
|
|
|
$selectCalls = 0;
|
|
$mockDb->method('select')
|
|
->willReturnCallback(function ($table, $columns, $where) use (&$selectCalls) {
|
|
$selectCalls++;
|
|
// Return marked images for deletion query
|
|
if ($table === 'pp_articles_images' && isset($where['AND']['to_delete'])) {
|
|
return [['id' => 1, 'src' => '/nonexistent/path/img.jpg']];
|
|
}
|
|
if ($table === 'pp_articles_files' && isset($where['AND']['to_delete'])) {
|
|
return [['id' => 2, 'src' => '/nonexistent/path/file.pdf']];
|
|
}
|
|
return [];
|
|
});
|
|
|
|
$deleteCalls = [];
|
|
$mockDb->method('delete')
|
|
->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
|
|
$deleteCalls[] = ['table' => $table, 'where' => $where];
|
|
return true;
|
|
});
|
|
|
|
$mockDb->method('insert')->willReturn(true);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$repository->save(15, $data, 1);
|
|
|
|
$imageDeletes = array_filter($deleteCalls, function ($c) {
|
|
return $c['table'] === 'pp_articles_images';
|
|
});
|
|
$fileDeletes = array_filter($deleteCalls, function ($c) {
|
|
return $c['table'] === 'pp_articles_files';
|
|
});
|
|
|
|
$this->assertNotEmpty($imageDeletes);
|
|
$this->assertNotEmpty($fileDeletes);
|
|
}
|
|
|
|
public function testArchiveSetsStatusToMinusOne(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('update')
|
|
->with('pp_articles', ['status' => -1], ['id' => 25])
|
|
->willReturn(true);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$result = $repository->archive(25);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function testArchiveReturnsFalseWhenUpdateFails(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('update')
|
|
->with('pp_articles', ['status' => -1], ['id' => 999])
|
|
->willReturn(false);
|
|
|
|
$repository = new ArticleRepository($mockDb);
|
|
$result = $repository->archive(999);
|
|
|
|
$this->assertFalse($result);
|
|
}
|
|
}
|