Files
shopPRO/tests/Unit/Domain/Article/ArticleRepositoryTest.php
Jacek Pyziak d83d0ecdea feat: eliminate htaccess.conf, move all URL routes to pp_routes (v0.329-0.330)
- Add category_id, page_id, article_id, type columns to pp_routes (migration 0.329)
- Move routing block in index.php before checkUrlParams() with Redis cache
- Routes for categories, pages, articles now stored in pp_routes instead of .htaccess
- Delete category/page/article routes on entity delete in respective repositories
- Eliminate libraries/htaccess.conf: generate .htaccess content entirely from PHP
- Move 32 static system routes (koszyk, logowanie, newsletter, AJAX modules, etc.)
  plus dynamic language/producer routes to pp_routes with type='system'
- Invalidate pp_routes Redis cache on every htacces() regeneration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 22:06:33 +01:00

1015 lines
34 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 testSaveGalleryOrderUpdatesImageOrder(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(3))
->method('update')
->withConsecutive(
[
'pp_articles_images',
['o' => 0],
['AND' => ['article_id' => 12, 'id' => 50]]
],
[
'pp_articles_images',
['o' => 1],
['AND' => ['article_id' => 12, 'id' => 51]]
],
[
'pp_articles_images',
['o' => 2],
['AND' => ['article_id' => 12, 'id' => 52]]
]
)
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$result = $repository->saveGalleryOrder(12, '50;51;52');
$this->assertTrue($result);
}
public function testSaveGalleryOrderSkipsEmptyValues(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with(
'pp_articles_images',
['o' => 0],
['AND' => ['article_id' => 7, 'id' => 99]]
)
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$result = $repository->saveGalleryOrder(7, ';99;');
$this->assertTrue($result);
}
public function testSaveFilesOrderUpdatesFilesOrder(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(3))
->method('update')
->withConsecutive(
[
'pp_articles_files',
['o' => 0],
['AND' => ['article_id' => 12, 'id' => 70]]
],
[
'pp_articles_files',
['o' => 1],
['AND' => ['article_id' => 12, 'id' => 71]]
],
[
'pp_articles_files',
['o' => 2],
['AND' => ['article_id' => 12, 'id' => 72]]
]
)
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$result = $repository->saveFilesOrder(12, '70;71;72');
$this->assertTrue($result);
}
public function testSaveFilesOrderSkipsEmptyValues(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with(
'pp_articles_files',
['o' => 0],
['AND' => ['article_id' => 7, 'id' => 101]]
)
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$result = $repository->saveFilesOrder(7, ';101;');
$this->assertTrue($result);
}
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);
}
public function testRestoreSetsStatusToZero(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with('pp_articles', ['status' => 0], ['id' => 25])
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$result = $repository->restore(25);
$this->assertTrue($result);
}
public function testDeletePermanentlyRemovesArticleAndRelations(): void
{
$mockDb = $this->createMock(\medoo::class);
$deleteCalls = [];
$mockDb->expects($this->exactly(6))
->method('delete')
->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
$deleteCalls[] = ['table' => $table, 'where' => $where];
return true;
});
$repository = new ArticleRepository($mockDb);
$result = $repository->deletePermanently(77);
$this->assertTrue($result);
$this->assertCount(6, $deleteCalls);
$this->assertSame('pp_articles_pages', $deleteCalls[0]['table']);
$this->assertSame('pp_articles_langs', $deleteCalls[1]['table']);
$this->assertSame('pp_articles_images', $deleteCalls[2]['table']);
$this->assertSame('pp_articles_files', $deleteCalls[3]['table']);
$this->assertSame('pp_routes', $deleteCalls[4]['table']);
$this->assertSame('pp_articles', $deleteCalls[5]['table']);
}
public function testPagesSummaryForArticlesBuildsLabels(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('query')
->willReturnCallback(function ($sql, $params = []) {
return new class {
public function fetchAll()
{
return [
['article_id' => 5, 'page_id' => 10, 'title' => 'Blog'],
['article_id' => 5, 'page_id' => 11, 'title' => 'Poradniki'],
['article_id' => 8, 'page_id' => 12, 'title' => 'Aktualnosci'],
];
}
};
});
$repository = new ArticleRepository($mockDb);
$result = $repository->pagesSummaryForArticles([5, 8]);
$this->assertSame(' - Blog / Poradniki', $result[5]);
$this->assertSame(' - Aktualnosci', $result[8]);
}
public function testUpdateImageAltDelegatesToDatabase(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with('pp_articles_images', ['alt' => 'Nowy alt'], ['id' => 33])
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$this->assertTrue($repository->updateImageAlt(33, 'Nowy alt'));
}
public function testMarkFileToDeleteDelegatesToDatabase(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with('pp_articles_files', ['to_delete' => 1], ['id' => 17])
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$this->assertTrue($repository->markFileToDelete(17));
}
public function testListArchivedForAdminWhitelistsSortAndDirection(): void
{
$mockDb = $this->createMock(\medoo::class);
$queries = [];
$mockDb->method('query')
->willReturnCallback(function ($sql, $params = []) use (&$queries) {
$queries[] = ['sql' => $sql, 'params' => $params];
if (strpos($sql, 'COUNT(0)') !== false) {
return new class {
public function fetchAll()
{
return [[1]];
}
};
}
return new class {
public function fetchAll()
{
return [[
'id' => 1,
'date_add' => '2020-01-01 00:00:00',
'date_modify' => '2020-01-01 00:00:00',
'title' => 'A',
]];
}
};
});
$repository = new ArticleRepository($mockDb);
$repository->listArchivedForAdmin(
[],
'date_add DESC; DROP TABLE pp_articles; --',
'DESC; DELETE FROM pp_users; --',
1,
100000
);
$this->assertCount(2, $queries);
$dataSql = $queries[1]['sql'];
$this->assertMatchesRegularExpression('/ORDER BY\s+pa\.date_add\s+DESC,\s+pa\.id\s+DESC/i', $dataSql);
$this->assertStringNotContainsString('DROP TABLE', $dataSql);
$this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql);
$this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
$this->assertStringContainsString('pa.status = -1', $dataSql);
}
public function testListForAdminWhitelistsSortAndDirection(): void
{
$mockDb = $this->createMock(\medoo::class);
$queries = [];
$mockDb->method('query')
->willReturnCallback(function ($sql, $params = []) use (&$queries) {
$queries[] = ['sql' => $sql, 'params' => $params];
if (strpos($sql, 'COUNT(0)') !== false) {
return new class {
public function fetchAll()
{
return [[1]];
}
};
}
return new class {
public function fetchAll()
{
return [[
'id' => 1,
'date_add' => '2020-01-01 00:00:00',
'date_modify' => '2020-01-01 00:00:00',
'status' => 1,
'title' => 'A',
'user' => 'admin',
]];
}
};
});
$repository = new ArticleRepository($mockDb);
$repository->listForAdmin(
[],
'date_add DESC; DROP TABLE pp_articles; --',
'DESC; DELETE FROM pp_users; --',
1,
100000
);
$this->assertCount(2, $queries);
$dataSql = $queries[1]['sql'];
$this->assertMatchesRegularExpression('/ORDER BY\s+pa\.date_add\s+DESC,\s+pa\.id\s+DESC/i', $dataSql);
$this->assertStringNotContainsString('DROP TABLE', $dataSql);
$this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql);
$this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
}
// =========================================================================
// FRONTEND METHODS
// =========================================================================
public function testArticleDetailsFrontendReturnsArticleWithRelations(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(2))
->method('get')
->willReturnOnConsecutiveCalls(
['id' => 5, 'status' => 1, 'show_title' => 1],
['lang_id' => 'pl', 'title' => 'Testowy', 'copy_from' => null]
);
$mockDb->expects($this->exactly(3))
->method('select')
->willReturnOnConsecutiveCalls(
[['id' => 10, 'src' => '/img/a.jpg']],
[['id' => 20, 'src' => '/files/a.pdf']],
[1, 2]
);
$repo = new ArticleRepository($mockDb);
$article = $repo->articleDetailsFrontend(5, 'pl');
$this->assertIsArray($article);
$this->assertEquals(5, $article['id']);
$this->assertArrayHasKey('language', $article);
$this->assertEquals('Testowy', $article['language']['title']);
$this->assertCount(1, $article['images']);
$this->assertCount(1, $article['files']);
}
public function testArticleDetailsFrontendReturnsNullForMissing(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->willReturn(false);
$repo = new ArticleRepository($mockDb);
$this->assertNull($repo->articleDetailsFrontend(999, 'pl'));
}
public function testArticleDetailsFrontendCopyFromFallback(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')
->willReturnOnConsecutiveCalls(
['id' => 7, 'status' => 1],
['lang_id' => 'en', 'title' => 'English', 'copy_from' => 'pl'],
['lang_id' => 'pl', 'title' => 'Polski']
);
$mockDb->expects($this->exactly(3))
->method('select')
->willReturnOnConsecutiveCalls(
[],
[],
[]
);
$repo = new ArticleRepository($mockDb);
$article = $repo->articleDetailsFrontend(7, 'en');
$this->assertEquals('Polski', $article['language']['title']);
}
private function createFetchAllMock(array $data): object
{
return new class($data) {
private $data;
public function __construct($data) { $this->data = $data; }
public function fetchAll($mode = null) { return $this->data; }
};
}
public function testArticlesIdsReturnsSortedIds(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('query')
->willReturn($this->createFetchAllMock([
['id' => 3],
['id' => 7],
['id' => 1],
]));
$repo = new ArticleRepository($mockDb);
$result = $repo->articlesIds(1, 'pl', 10, 1, 0);
$this->assertEquals([3, 7, 1], $result);
}
public function testArticlesIdsReturnsNullForEmpty(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('query')
->willReturn($this->createFetchAllMock([]));
$repo = new ArticleRepository($mockDb);
$this->assertNull($repo->articlesIds(1, 'pl', 10, 0, 0));
}
public function testPageArticlesCountReturnsInt(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('query')
->willReturn($this->createFetchAllMock([[12]]));
$repo = new ArticleRepository($mockDb);
$this->assertSame(12, $repo->pageArticlesCount(5, 'pl'));
}
public function testPageArticlesCountReturnsZeroForEmpty(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('query')
->willReturn($this->createFetchAllMock([]));
$repo = new ArticleRepository($mockDb);
$this->assertSame(0, $repo->pageArticlesCount(5, 'pl'));
}
public function testPageArticlesPagination(): void
{
$mockDb = $this->createMock(\medoo::class);
// pageArticlesCount query returns 25 articles
// articlesIds query returns 10 article IDs
$mockDb->method('query')
->willReturnOnConsecutiveCalls(
$this->createFetchAllMock([[25]]),
$this->createFetchAllMock([
['id' => 11], ['id' => 12], ['id' => 13], ['id' => 14], ['id' => 15],
['id' => 16], ['id' => 17], ['id' => 18], ['id' => 19], ['id' => 20],
])
);
$page = ['id' => 3, 'articles_limit' => 10, 'sort_type' => 1];
$repo = new ArticleRepository($mockDb);
$result = $repo->pageArticles($page, 'pl', 2);
$this->assertArrayHasKey('articles', $result);
$this->assertArrayHasKey('ls', $result);
$this->assertSame(3, $result['ls']); // ceil(25/10) = 3
$this->assertCount(10, $result['articles']);
}
public function testArticleNoindexReturnsBool(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->with('pp_articles_langs', 'noindex', [
'AND' => ['article_id' => 5, 'lang_id' => 'pl']
])
->willReturn(1);
$repo = new ArticleRepository($mockDb);
$this->assertTrue($repo->articleNoindex(5, 'pl'));
}
public function testArticleNoindexReturnsFalseForNonNoindex(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')
->willReturn(null);
$repo = new ArticleRepository($mockDb);
$this->assertFalse($repo->articleNoindex(5, 'pl'));
}
public function testNewsReturnsArticlesArray(): void
{
$mockDb = $this->createMock(\medoo::class);
// First get() for sort_type, then get() for article_details
$mockDb->method('get')
->willReturnOnConsecutiveCalls(
1, // sort_type
['id' => 10, 'status' => 1] // article data
);
// articlesIds query returns [10]
$mockDb->method('query')
->willReturn($this->createFetchAllMock([['id' => 10]]));
// article details selects
$mockDb->method('select')
->willReturnOnConsecutiveCalls(
[['lang_id' => 'pl', 'title' => 'News', 'copy_from' => null]],
[],
[],
[]
);
$repo = new ArticleRepository($mockDb);
$result = $repo->news(3, 6, 'pl');
$this->assertIsArray($result);
$this->assertCount(1, $result);
$this->assertEquals(10, $result[0]['id']);
}
public function testTopArticlesOrderByViews(): void
{
$mockDb = $this->createMock(\medoo::class);
$queryCalls = 0;
$mockDb->method('query')
->willReturnCallback(function ($sql) use (&$queryCalls) {
$queryCalls++;
if ($queryCalls === 1) {
$this->assertStringContainsString('views DESC', $sql);
return $this->createFetchAllMock([
['id' => 5, 'date_add' => '2025-01-01', 'views' => 100, 'title' => 'Popular'],
]);
}
return $this->createFetchAllMock([]);
});
$mockDb->method('get')
->willReturnOnConsecutiveCalls(
['id' => 5, 'status' => 1],
['lang_id' => 'pl', 'title' => 'Popular', 'copy_from' => null]
);
$mockDb->method('select')
->willReturnOnConsecutiveCalls(
[],
[],
[]
);
$repo = new ArticleRepository($mockDb);
$result = $repo->topArticles(3, 6, 'pl');
$this->assertIsArray($result);
$this->assertCount(1, $result);
}
public function testNewsListArticlesOrderByDateDesc(): void
{
$mockDb = $this->createMock(\medoo::class);
$queryCalls = 0;
$mockDb->method('query')
->willReturnCallback(function ($sql) use (&$queryCalls) {
$queryCalls++;
if ($queryCalls === 1) {
$this->assertStringContainsString('date_add DESC', $sql);
return $this->createFetchAllMock([
['id' => 8, 'date_add' => '2025-06-15', 'title' => 'Newest'],
]);
}
return $this->createFetchAllMock([]);
});
$mockDb->method('get')
->willReturnOnConsecutiveCalls(
['id' => 8, 'status' => 1],
['lang_id' => 'pl', 'title' => 'Newest', 'copy_from' => null]
);
$mockDb->method('select')
->willReturnOnConsecutiveCalls(
[],
[],
[]
);
$repo = new ArticleRepository($mockDb);
$result = $repo->newsListArticles(3, 6, 'pl');
$this->assertIsArray($result);
$this->assertCount(1, $result);
}
public function testListForAdminUsesBoundParamsForTitleFilter(): void
{
$mockDb = $this->createMock(\medoo::class);
$queries = [];
$attack = "' OR 1=1 --";
$mockDb->method('query')
->willReturnCallback(function ($sql, $params = []) use (&$queries) {
$queries[] = ['sql' => $sql, 'params' => $params];
if (strpos($sql, 'COUNT(0)') !== false) {
return new class {
public function fetchAll()
{
return [[0]];
}
};
}
return new class {
public function fetchAll()
{
return [];
}
};
});
$repository = new ArticleRepository($mockDb);
$repository->listForAdmin(['title' => $attack], 'title', 'ASC', 1, 15);
$this->assertCount(2, $queries);
$countSql = $queries[0]['sql'];
$countParams = $queries[0]['params'];
$this->assertStringContainsString('LIKE :title', $countSql);
$this->assertStringNotContainsString($attack, $countSql);
$this->assertArrayHasKey(':title', $countParams);
$this->assertSame('%' . $attack . '%', $countParams[':title']);
}
}