xref: /dokuwiki/_test/tests/TreeBuilder/PageTreeBuilderTest.php (revision 31003314c05cb43e9840ac995129d131e4e750b8)
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