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']); } }