178a26510SAndreas Gohr<?php 278a26510SAndreas Gohr 378a26510SAndreas Gohrnamespace dokuwiki\test\Treebuilder; 478a26510SAndreas Gohr 578a26510SAndreas Gohruse dokuwiki\TreeBuilder\PageTreeBuilder; 678a26510SAndreas Gohruse DokuWikiTest; 778a26510SAndreas Gohr 878a26510SAndreas Gohrclass PageTreeBuilderTest extends DokuWikiTest 978a26510SAndreas Gohr{ 1078a26510SAndreas Gohr protected $originalDataDir; 1178a26510SAndreas Gohr 1278a26510SAndreas Gohr public static function setUpBeforeClass(): void 1378a26510SAndreas Gohr { 1478a26510SAndreas Gohr parent::setUpBeforeClass(); 1578a26510SAndreas Gohr 1678a26510SAndreas Gohr // Create a test page hierarchy 1778a26510SAndreas Gohr saveWikiText('namespace:start', 'This is the start page', 'test'); 1878a26510SAndreas Gohr saveWikiText('namespace:page1', 'This is page 1', 'test'); 1978a26510SAndreas Gohr saveWikiText('namespace:page2', 'This is page 2', 'test'); 2078a26510SAndreas Gohr saveWikiText('namespace:subns:start', 'This is the subns start page', 'test'); 2178a26510SAndreas Gohr saveWikiText('namespace:subns:page3', 'This is page 3 in subns', 'test'); 2278a26510SAndreas Gohr saveWikiText('namespace:subns:deeper:start', 'This is the deeper start page', 'test'); 2378a26510SAndreas Gohr saveWikiText('namespace:subns:deeper:page4', 'This is page 4 in deeper', 'test'); 2478a26510SAndreas Gohr } 2578a26510SAndreas Gohr 2678a26510SAndreas Gohr public function setUp(): void 2778a26510SAndreas Gohr { 2878a26510SAndreas Gohr parent::setUp(); 2978a26510SAndreas Gohr global $conf; 3078a26510SAndreas Gohr $this->originalDataDir = $conf['datadir']; 3178a26510SAndreas Gohr } 3278a26510SAndreas Gohr 3378a26510SAndreas Gohr public function tearDown(): void 3478a26510SAndreas Gohr { 3578a26510SAndreas Gohr global $conf; 3678a26510SAndreas Gohr $conf['datadir'] = $this->originalDataDir; 3778a26510SAndreas Gohr parent::tearDown(); 3878a26510SAndreas Gohr } 3978a26510SAndreas Gohr 4078a26510SAndreas Gohr public function treeConfigProvider() 4178a26510SAndreas Gohr { 4278a26510SAndreas Gohr return [ 4378a26510SAndreas Gohr 'Default configuration' => [ 4478a26510SAndreas Gohr 'namespace' => 'namespace', 4578a26510SAndreas Gohr 'depth' => -1, 4678a26510SAndreas Gohr 'flags' => 0, 4778a26510SAndreas Gohr 'expected' => [ 4878a26510SAndreas Gohr '+namespace:start', 4978a26510SAndreas Gohr '+namespace:page1', 5078a26510SAndreas Gohr '+namespace:page2', 5178a26510SAndreas Gohr '+namespace:subns', 5278a26510SAndreas Gohr '++namespace:subns:start', 5378a26510SAndreas Gohr '++namespace:subns:page3', 5478a26510SAndreas Gohr '++namespace:subns:deeper', 5578a26510SAndreas Gohr '+++namespace:subns:deeper:start', 5678a26510SAndreas Gohr '+++namespace:subns:deeper:page4' 5778a26510SAndreas Gohr ] 5878a26510SAndreas Gohr ], 5978a26510SAndreas Gohr 'Depth limit 1' => [ 6078a26510SAndreas Gohr 'namespace' => 'namespace', 6178a26510SAndreas Gohr 'depth' => 1, 6278a26510SAndreas Gohr 'flags' => 0, 6378a26510SAndreas Gohr 'expected' => [ 6478a26510SAndreas Gohr '+namespace:start', 6578a26510SAndreas Gohr '+namespace:page1', 6678a26510SAndreas Gohr '+namespace:page2', 6778a26510SAndreas Gohr '+namespace:subns', 6878a26510SAndreas Gohr '++namespace:subns:start', 6978a26510SAndreas Gohr '++namespace:subns:page3', 7078a26510SAndreas Gohr '++namespace:subns:deeper' 7178a26510SAndreas Gohr ] 7278a26510SAndreas Gohr ], 7378a26510SAndreas Gohr 'Depth limit 1 with NS_AS_STARTPAGE' => [ 7478a26510SAndreas Gohr 'namespace' => 'namespace', 7578a26510SAndreas Gohr 'depth' => 1, 7678a26510SAndreas Gohr 'flags' => PageTreeBuilder::FLAG_NS_AS_STARTPAGE, 7778a26510SAndreas Gohr 'expected' => [ 7878a26510SAndreas Gohr '+namespace:page1', 7978a26510SAndreas Gohr '+namespace:page2', 8078a26510SAndreas Gohr '+namespace:subns:start', 8178a26510SAndreas Gohr '++namespace:subns:page3', 8278a26510SAndreas Gohr '++namespace:subns:deeper:start' 8378a26510SAndreas Gohr ] 8478a26510SAndreas Gohr ], 8578a26510SAndreas Gohr 'FLAG_NO_NS' => [ 8678a26510SAndreas Gohr 'namespace' => 'namespace', 8778a26510SAndreas Gohr 'depth' => -1, 8878a26510SAndreas Gohr 'flags' => PageTreeBuilder::FLAG_NO_NS, 8978a26510SAndreas Gohr 'expected' => [ 9078a26510SAndreas Gohr '+namespace:start', 9178a26510SAndreas Gohr '+namespace:page1', 9278a26510SAndreas Gohr '+namespace:page2' 9378a26510SAndreas Gohr ] 9478a26510SAndreas Gohr ], 9578a26510SAndreas Gohr 'FLAG_NO_PAGES' => [ 9678a26510SAndreas Gohr 'namespace' => 'namespace', 9778a26510SAndreas Gohr 'depth' => -1, 9878a26510SAndreas Gohr 'flags' => PageTreeBuilder::FLAG_NO_PAGES, 9978a26510SAndreas Gohr 'expected' => [ 10078a26510SAndreas Gohr '+namespace:subns', 10178a26510SAndreas Gohr '++namespace:subns:deeper' 10278a26510SAndreas Gohr ] 10378a26510SAndreas Gohr ], 10478a26510SAndreas Gohr 'FLAG_NS_AS_STARTPAGE' => [ 10578a26510SAndreas Gohr 'namespace' => 'namespace', 10678a26510SAndreas Gohr 'depth' => -1, 10778a26510SAndreas Gohr 'flags' => PageTreeBuilder::FLAG_NS_AS_STARTPAGE, 10878a26510SAndreas Gohr 'expected' => [ 10978a26510SAndreas Gohr '+namespace:page1', 11078a26510SAndreas Gohr '+namespace:page2', 11178a26510SAndreas Gohr '+namespace:subns:start', 11278a26510SAndreas Gohr '++namespace:subns:page3', 11378a26510SAndreas Gohr '++namespace:subns:deeper:start', 11478a26510SAndreas Gohr '+++namespace:subns:deeper:page4' 11578a26510SAndreas Gohr ] 11678a26510SAndreas Gohr ], 11778a26510SAndreas Gohr 'Combined FLAG_NO_NS and FLAG_NS_AS_STARTPAGE' => [ 11878a26510SAndreas Gohr 'namespace' => 'namespace', 11978a26510SAndreas Gohr 'depth' => -1, 12078a26510SAndreas Gohr 'flags' => PageTreeBuilder::FLAG_NO_NS | PageTreeBuilder::FLAG_NS_AS_STARTPAGE, 12178a26510SAndreas Gohr 'expected' => [ 12278a26510SAndreas Gohr '+namespace:page1', 12378a26510SAndreas Gohr '+namespace:page2' 12478a26510SAndreas Gohr ] 12578a26510SAndreas Gohr ], 12678a26510SAndreas Gohr 'FLAG_SELF_TOP' => [ 12778a26510SAndreas Gohr 'namespace' => 'namespace', 12878a26510SAndreas Gohr 'depth' => -1, 12978a26510SAndreas Gohr 'flags' => PageTreeBuilder::FLAG_SELF_TOP, 13078a26510SAndreas Gohr 'expected' => [ 13178a26510SAndreas Gohr '+namespace', 13278a26510SAndreas Gohr '++namespace:start', 13378a26510SAndreas Gohr '++namespace:page1', 13478a26510SAndreas Gohr '++namespace:page2', 13578a26510SAndreas Gohr '++namespace:subns', 13678a26510SAndreas Gohr '+++namespace:subns:start', 13778a26510SAndreas Gohr '+++namespace:subns:page3', 13878a26510SAndreas Gohr '+++namespace:subns:deeper', 13978a26510SAndreas Gohr '++++namespace:subns:deeper:start', 14078a26510SAndreas Gohr '++++namespace:subns:deeper:page4' 14178a26510SAndreas Gohr ] 14278a26510SAndreas Gohr ], 14378a26510SAndreas Gohr ]; 14478a26510SAndreas Gohr } 14578a26510SAndreas Gohr 14678a26510SAndreas Gohr 14778a26510SAndreas Gohr /** 14878a26510SAndreas Gohr * @dataProvider treeConfigProvider 14978a26510SAndreas Gohr */ 15078a26510SAndreas Gohr public function testPageTreeConfigurations(string $namespace, int $depth, int $flags, array $expected) 15178a26510SAndreas Gohr { 15278a26510SAndreas Gohr $tree = new PageTreeBuilder($namespace, $depth); 15378a26510SAndreas Gohr if ($flags) { 15478a26510SAndreas Gohr $tree->addFlag($flags); 15578a26510SAndreas Gohr } 15678a26510SAndreas Gohr $tree->generate(); 15778a26510SAndreas Gohr 15878a26510SAndreas Gohr $result = explode("\n", (string)$tree); 15978a26510SAndreas Gohr sort($expected); 16078a26510SAndreas Gohr sort($result); 16178a26510SAndreas Gohr 16278a26510SAndreas Gohr $this->assertEquals($expected, $result); 16378a26510SAndreas Gohr } 16478a26510SAndreas Gohr 16578a26510SAndreas Gohr /** 16678a26510SAndreas Gohr * This is the same test as above, but pretending that our data directory is in our test namespace. 16778a26510SAndreas Gohr * 16878a26510SAndreas Gohr * @dataProvider treeConfigProvider 16978a26510SAndreas Gohr */ 17078a26510SAndreas Gohr public function testTopLevelTree(string $namespace, int $depth, int $flags, array $expected) 17178a26510SAndreas Gohr { 17278a26510SAndreas Gohr global $conf; 17378a26510SAndreas Gohr $conf['datadir'] .= '/namespace'; 17478a26510SAndreas Gohr 17578a26510SAndreas Gohr $expected = array_map(function ($item) use ($namespace) { 17678a26510SAndreas Gohr return preg_replace('/namespace:?/', '', $item); 17778a26510SAndreas Gohr }, $expected); 17878a26510SAndreas Gohr 17978a26510SAndreas Gohr $namespace = ''; 18078a26510SAndreas Gohr $this->testPageTreeConfigurations($namespace, $depth, $flags, $expected); 18178a26510SAndreas Gohr } 18278a26510SAndreas Gohr 18378a26510SAndreas Gohr 18478a26510SAndreas Gohr public function testPageTreeLeaves() 18578a26510SAndreas Gohr { 18678a26510SAndreas Gohr $tree = new PageTreeBuilder('namespace'); 18778a26510SAndreas Gohr $tree->generate(); 18878a26510SAndreas Gohr 18978a26510SAndreas Gohr $leaves = $tree->getLeaves(); 19078a26510SAndreas Gohr $branches = $tree->getBranches(); 19178a26510SAndreas Gohr 19278a26510SAndreas Gohr // Test that we have both leaves and branches 19378a26510SAndreas Gohr $this->assertGreaterThan(0, count($leaves), 'Should have leaf pages'); 19478a26510SAndreas Gohr $this->assertGreaterThan(0, count($branches), 'Should have branch pages'); 19578a26510SAndreas Gohr 19678a26510SAndreas Gohr // The total should equal all pages 19778a26510SAndreas Gohr $this->assertEquals(count($tree->getAll()), count($leaves) + count($branches), 19878a26510SAndreas Gohr 'Leaves + branches should equal total pages'); 19978a26510SAndreas Gohr } 200*31003314SAndreas Gohr 201*31003314SAndreas Gohr public function invalidIdFilteringProvider() 202*31003314SAndreas Gohr { 203*31003314SAndreas Gohr return [ 204*31003314SAndreas Gohr 'invalid IDs filtered by default' => [ 205*31003314SAndreas Gohr 'flags' => 0, 206*31003314SAndreas Gohr 'expectFiltered' => true, 207*31003314SAndreas Gohr ], 208*31003314SAndreas Gohr 'invalid IDs kept with FLAG_KEEP_INVALID' => [ 209*31003314SAndreas Gohr 'flags' => PageTreeBuilder::FLAG_KEEP_INVALID, 210*31003314SAndreas Gohr 'expectFiltered' => false, 211*31003314SAndreas Gohr ], 212*31003314SAndreas Gohr ]; 213*31003314SAndreas Gohr } 214*31003314SAndreas Gohr 215*31003314SAndreas Gohr /** 216*31003314SAndreas Gohr * @dataProvider invalidIdFilteringProvider 217*31003314SAndreas Gohr */ 218*31003314SAndreas Gohr public function testInvalidIdFiltering(int $flags, bool $expectFiltered) 219*31003314SAndreas Gohr { 220*31003314SAndreas Gohr global $conf; 221*31003314SAndreas Gohr 222*31003314SAndreas Gohr $nsDir = $conf['datadir'] . '/namespace'; 223*31003314SAndreas Gohr 224*31003314SAndreas Gohr // Create files with invalid IDs directly in the filesystem 225*31003314SAndreas Gohr $invalidFiles = [ 226*31003314SAndreas Gohr '__template.txt', // namespace template 227*31003314SAndreas Gohr '_template.txt', // leading underscore 228*31003314SAndreas Gohr ]; 229*31003314SAndreas Gohr foreach ($invalidFiles as $file) { 230*31003314SAndreas Gohr io_saveFile($nsDir . '/' . $file, 'This should not appear in the tree'); 231*31003314SAndreas Gohr } 232*31003314SAndreas Gohr 233*31003314SAndreas Gohr // Create directories with invalid names 234*31003314SAndreas Gohr $invalidDirs = [ 235*31003314SAndreas Gohr '_private', // leading underscore 236*31003314SAndreas Gohr ]; 237*31003314SAndreas Gohr foreach ($invalidDirs as $dir) { 238*31003314SAndreas Gohr $dirPath = $nsDir . '/' . $dir; 239*31003314SAndreas Gohr io_mkdir_p($dirPath); 240*31003314SAndreas Gohr io_saveFile($dirPath . '/start.txt', 'This should not appear in the tree'); 241*31003314SAndreas Gohr } 242*31003314SAndreas Gohr 243*31003314SAndreas Gohr $tree = new PageTreeBuilder('namespace'); 244*31003314SAndreas Gohr if ($flags) { 245*31003314SAndreas Gohr $tree->addFlag($flags); 246*31003314SAndreas Gohr } 247*31003314SAndreas Gohr $tree->generate(); 248*31003314SAndreas Gohr 249*31003314SAndreas Gohr $allIds = array_map(fn($node) => $node->getId(), $tree->getAll()); 250*31003314SAndreas Gohr 251*31003314SAndreas Gohr // Check invalid pages 252*31003314SAndreas Gohr foreach ($invalidFiles as $file) { 253*31003314SAndreas Gohr $invalidId = pathID('namespace/' . $file); 254*31003314SAndreas Gohr if ($expectFiltered) { 255*31003314SAndreas Gohr $this->assertNotContains($invalidId, $allIds, "Invalid page '$file' should be filtered"); 256*31003314SAndreas Gohr } else { 257*31003314SAndreas Gohr $this->assertContains($invalidId, $allIds, "Invalid page '$file' should be kept"); 258*31003314SAndreas Gohr } 259*31003314SAndreas Gohr } 260*31003314SAndreas Gohr 261*31003314SAndreas Gohr // Check invalid namespaces 262*31003314SAndreas Gohr foreach ($invalidDirs as $dir) { 263*31003314SAndreas Gohr $invalidId = pathID('namespace/' . $dir); 264*31003314SAndreas Gohr if ($expectFiltered) { 265*31003314SAndreas Gohr $this->assertNotContains($invalidId, $allIds, "Invalid namespace '$dir' should be filtered"); 266*31003314SAndreas Gohr } else { 267*31003314SAndreas Gohr $this->assertContains($invalidId, $allIds, "Invalid namespace '$dir' should be kept"); 268*31003314SAndreas Gohr } 269*31003314SAndreas Gohr } 270*31003314SAndreas Gohr 271*31003314SAndreas Gohr // Valid pages and namespaces should always be present 272*31003314SAndreas Gohr $this->assertContains('namespace:page1', $allIds); 273*31003314SAndreas Gohr $this->assertContains('namespace:page2', $allIds); 274*31003314SAndreas Gohr $this->assertContains('namespace:subns', $allIds); 275*31003314SAndreas Gohr } 27678a26510SAndreas Gohr} 277