Passed
Push — master ( e68a5e...7386f0 )
by Robbie
05:57
created

SiteTreeTest::testDependentPagesOnUnsavedRecord()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\CMS\Tests\Model;
4
5
use LogicException;
6
use Page;
0 ignored issues
show
Bug introduced by
The type Page was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use ReflectionMethod;
8
use SilverStripe\CMS\Model\RedirectorPage;
9
use SilverStripe\CMS\Model\SiteTree;
10
use SilverStripe\CMS\Model\VirtualPage;
11
use SilverStripe\Control\ContentNegotiator;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Core\Config\Config;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\Dev\SapphireTest;
17
use SilverStripe\i18n\i18n;
18
use SilverStripe\ORM\DB;
19
use SilverStripe\ORM\DataObject;
20
use SilverStripe\ORM\ValidationException;
21
use SilverStripe\Security\Group;
22
use SilverStripe\Security\InheritedPermissions;
23
use SilverStripe\Security\Member;
24
use SilverStripe\Security\Permission;
25
use SilverStripe\Security\Security;
26
use SilverStripe\SiteConfig\SiteConfig;
27
use SilverStripe\Subsites\Extensions\SiteTreeSubsites;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Subsites\Extensions\SiteTreeSubsites was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
use SilverStripe\Versioned\Versioned;
29
use SilverStripe\View\Parsers\Diff;
30
use SilverStripe\View\Parsers\ShortcodeParser;
31
use SilverStripe\View\Parsers\URLSegmentFilter;
32
use TractorCow\Fluent\Extension\FluentSiteTreeExtension;
0 ignored issues
show
Bug introduced by
The type TractorCow\Fluent\Extens...FluentSiteTreeExtension was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
34
class SiteTreeTest extends SapphireTest
35
{
36
    protected static $fixture_file = 'SiteTreeTest.yml';
37
38
    protected static $illegal_extensions = [
39
        SiteTree::class => [
40
            SiteTreeSubsites::class,
41
            FluentSiteTreeExtension::class,
42
        ],
43
    ];
44
45
    protected static $extra_dataobjects = [
46
        SiteTreeTest_ClassA::class,
47
        SiteTreeTest_ClassB::class,
48
        SiteTreeTest_ClassC::class,
49
        SiteTreeTest_ClassD::class,
50
        SiteTreeTest_ClassCext::class,
51
        SiteTreeTest_NotRoot::class,
52
        SiteTreeTest_StageStatusInherit::class,
53
        SiteTreeTest_DataObject::class,
54
    ];
55
56
    public function reservedSegmentsProvider()
57
    {
58
        return [
59
            // segments reserved by rules
60
            ['Admin', 'admin-2'],
61
            ['Dev', 'dev-2'],
62
            ['Robots in disguise', 'robots-in-disguise'],
63
            // segments reserved by folder name
64
            [RESOURCES_DIR, RESOURCES_DIR . '-2'],
0 ignored issues
show
Bug introduced by
The constant SilverStripe\CMS\Tests\Model\RESOURCES_DIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
65
            ['assets', 'assets-2'],
66
            ['notafoldername', 'notafoldername'],
67
        ];
68
    }
69
70
    public function testCreateDefaultpages()
71
    {
72
        $remove = SiteTree::get();
73
        if ($remove) {
0 ignored issues
show
introduced by
$remove is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
74
            foreach ($remove as $page) {
75
                $page->delete();
76
            }
77
        }
78
        // Make sure the table is empty
79
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
80
81
        // Disable the creation
82
        SiteTree::config()->create_default_pages = false;
83
        singleton(SiteTree::class)->requireDefaultRecords();
84
85
        // The table should still be empty
86
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
87
88
        // Enable the creation
89
        SiteTree::config()->create_default_pages = true;
90
        singleton(SiteTree::class)->requireDefaultRecords();
91
92
        // The table should now have three rows (home, about-us, contact-us)
93
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 3);
94
    }
95
96
    /**
97
     * Test generation of the URLSegment values.
98
     *  - Turns things into lowercase-hyphen-format
99
     *  - Generates from Title by default, unless URLSegment is explicitly set
100
     *  - Resolves duplicates by appending a number
101
     *  - renames classes with a class name conflict
102
     */
103
    public function testURLGeneration()
104
    {
105
        $expectedURLs = [
106
            'home' => 'home',
107
            'staff' => 'my-staff',
108
            'about' => 'about-us',
109
            'staffduplicate' => 'my-staff-2',
110
            'product1' => '1-1-test-product',
111
            'product2' => 'another-product',
112
            'product3' => 'another-product-2',
113
            'product4' => 'another-product-3',
114
            'object'   => 'object',
115
            'controller' => 'controller',
116
            'numericonly' => '1930',
117
        ];
118
119
        foreach ($expectedURLs as $fixture => $urlSegment) {
120
            $obj = $this->objFromFixture(SiteTree::class, $fixture);
121
            $this->assertEquals($urlSegment, $obj->URLSegment);
122
        }
123
    }
124
125
    /**
126
     * Check if reserved URL's are properly appended with a number at top level
127
     * @dataProvider reservedSegmentsProvider
128
     */
129
    public function testDisallowedURLGeneration($title, $urlSegment)
130
    {
131
        $page = SiteTree::create(['Title' => $title]);
132
        $id = $page->write();
133
        $page = SiteTree::get()->byID($id);
134
        $this->assertEquals($urlSegment, $page->URLSegment);
135
    }
136
137
    /**
138
     * Check if reserved URL's are not appended with a number on a child page
139
     * It's okay to have a URL like domain.com/my-page/admin as it won't interfere with domain.com/admin
140
     * @dataProvider reservedSegmentsProvider
141
     */
142
    public function testDisallowedChildURLGeneration($title, $urlSegment)
143
    {
144
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
145
        // Using the same dataprovider, strip out the -2 from the admin and dev segment
146
        $urlSegment = str_replace('-2', '', $urlSegment);
147
        $page = SiteTree::create(['Title' => $title, 'ParentID' => 1]);
148
        $id = $page->write();
149
        $page = SiteTree::get()->byID($id);
150
        $this->assertEquals($urlSegment, $page->URLSegment);
151
    }
152
153
    /**
154
     * Test that publication copies data to SiteTree_Live
155
     */
156
    public function testPublishCopiesToLiveTable()
157
    {
158
        $obj = $this->objFromFixture(SiteTree::class, 'about');
159
        $obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
160
161
        $createdID = DB::query(
162
            "SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"URLSegment\" = '$obj->URLSegment'"
163
        )->value();
164
        $this->assertEquals($obj->ID, $createdID);
165
    }
166
167
    /**
168
     * Test that field which are set and then cleared are also transferred to the published site.
169
     */
170
    public function testPublishDeletedFields()
171
    {
172
        $this->logInWithPermission('ADMIN');
173
174
        $obj = $this->objFromFixture(SiteTree::class, 'about');
175
        $obj->Title = "asdfasdf";
176
        $obj->write();
177
        $this->assertTrue($obj->publishRecursive());
178
179
        $this->assertEquals(
180
            'asdfasdf',
181
            DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value()
182
        );
183
184
        $obj->Title = null;
185
        $obj->write();
186
        $this->assertTrue($obj->publishRecursive());
187
188
        $this->assertNull(DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value());
189
    }
190
191
    public function testParentNodeCachedInMemory()
192
    {
193
        $parent = SiteTree::create();
194
        $parent->Title = 'Section Title';
195
        $child = SiteTree::create();
196
        $child->Title = 'Page Title';
197
        $child->setParent($parent);
198
199
        $this->assertInstanceOf(SiteTree::class, $child->Parent);
200
        $this->assertEquals("Section Title", $child->Parent->Title);
201
    }
202
203
    public function testParentModelReturnType()
204
    {
205
        $parent = new SiteTreeTest_PageNode();
206
        $child = new SiteTreeTest_PageNode();
207
208
        $child->setParent($parent);
209
        $this->assertInstanceOf(SiteTreeTest_PageNode::class, $child->Parent);
0 ignored issues
show
Bug Best Practice introduced by
The property Parent does not exist on SilverStripe\CMS\Tests\Model\SiteTreeTest_PageNode. Since you implemented __get, consider adding a @property annotation.
Loading history...
210
    }
211
212
    /**
213
     * Confirm that DataObject::get_one() gets records from SiteTree_Live
214
     */
215
    public function testGetOneFromLive()
216
    {
217
        $s = SiteTree::create();
218
        $s->Title = "V1";
219
        $s->URLSegment = "get-one-test-page";
220
        $s->write();
221
        $s->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
222
        $s->Title = "V2";
223
        $s->write();
224
225
        $oldMode = Versioned::get_reading_mode();
226
        Versioned::set_stage(Versioned::LIVE);
227
228
        $checkSiteTree = DataObject::get_one(SiteTree::class, [
229
            '"SiteTree"."URLSegment"' => 'get-one-test-page',
230
        ]);
231
        $this->assertEquals("V1", $checkSiteTree->Title);
232
233
        Versioned::set_reading_mode($oldMode);
234
    }
235
236
    public function testChidrenOfRootAreTopLevelPages()
237
    {
238
        $pages = SiteTree::get();
239
        foreach ($pages as $page) {
240
            $page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
241
        }
242
        unset($pages);
243
244
        /* If we create a new SiteTree object with ID = 0 */
245
        $obj = SiteTree::create();
246
        /* Then its children should be the top-level pages */
247
        $stageChildren = $obj->stageChildren()->map('ID', 'Title');
248
        $liveChildren = $obj->liveChildren()->map('ID', 'Title');
249
        $allChildren = $obj->AllChildrenIncludingDeleted()->map('ID', 'Title');
250
251
        $this->assertContains('Home', $stageChildren);
252
        $this->assertContains('Products', $stageChildren);
253
        $this->assertNotContains('Staff', $stageChildren);
254
255
        $this->assertContains('Home', $liveChildren);
256
        $this->assertContains('Products', $liveChildren);
257
        $this->assertNotContains('Staff', $liveChildren);
258
259
        $this->assertContains('Home', $allChildren);
260
        $this->assertContains('Products', $allChildren);
261
        $this->assertNotContains('Staff', $allChildren);
262
    }
263
264
    public function testCanSaveBlankToHasOneRelations()
265
    {
266
        /* DataObject::write() should save to a has_one relationship if you set a field called (relname)ID */
267
        $page = SiteTree::create();
268
        $parentID = $this->idFromFixture(SiteTree::class, 'home');
269
        $page->ParentID = $parentID;
270
        $page->write();
271
        $this->assertEquals(
272
            $parentID,
273
            DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value()
274
        );
275
276
        /* You should then be able to save a null/0/'' value to the relation */
277
        $page->ParentID = null;
278
        $page->write();
279
        $this->assertEquals(0, DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value());
280
    }
281
282
    public function testStageStates()
283
    {
284
        // newly created page
285
        $createdPage = SiteTree::create();
286
        $createdPage->write();
287
        $this->assertTrue($createdPage->isOnDraft());
288
        $this->assertFalse($createdPage->isPublished());
289
        $this->assertTrue($createdPage->isOnDraftOnly());
290
        $this->assertTrue($createdPage->isModifiedOnDraft());
291
292
        // published page
293
        $publishedPage = SiteTree::create();
294
        $publishedPage->write();
295
        $publishedPage->copyVersionToStage('Stage', 'Live');
296
        $this->assertTrue($publishedPage->isOnDraft());
297
        $this->assertTrue($publishedPage->isPublished());
298
        $this->assertFalse($publishedPage->isOnDraftOnly());
299
        $this->assertFalse($publishedPage->isOnLiveOnly());
300
        $this->assertFalse($publishedPage->isModifiedOnDraft());
301
302
        // published page, deleted from stage
303
        $deletedFromDraftPage = SiteTree::create();
304
        $deletedFromDraftPage->write();
305
        $deletedFromDraftPage->copyVersionToStage('Stage', 'Live');
306
        $deletedFromDraftPage->deleteFromStage('Stage');
307
        $this->assertFalse($deletedFromDraftPage->isArchived());
308
        $this->assertFalse($deletedFromDraftPage->isOnDraft());
309
        $this->assertTrue($deletedFromDraftPage->isPublished());
310
        $this->assertFalse($deletedFromDraftPage->isOnDraftOnly());
311
        $this->assertTrue($deletedFromDraftPage->isOnLiveOnly());
312
        $this->assertFalse($deletedFromDraftPage->isModifiedOnDraft());
313
314
        // published page, deleted from live
315
        $deletedFromLivePage = SiteTree::create();
316
        $deletedFromLivePage->write();
317
        $deletedFromLivePage->copyVersionToStage('Stage', 'Live');
318
        $deletedFromLivePage->deleteFromStage('Live');
319
        $this->assertFalse($deletedFromLivePage->isArchived());
320
        $this->assertTrue($deletedFromLivePage->isOnDraft());
321
        $this->assertFalse($deletedFromLivePage->isPublished());
322
        $this->assertTrue($deletedFromLivePage->isOnDraftOnly());
323
        $this->assertFalse($deletedFromLivePage->isOnLiveOnly());
324
        $this->assertTrue($deletedFromLivePage->isModifiedOnDraft());
325
326
        // published page, deleted from both stages
327
        $deletedFromAllStagesPage = SiteTree::create();
328
        $deletedFromAllStagesPage->write();
329
        $deletedFromAllStagesPage->copyVersionToStage('Stage', 'Live');
330
        $deletedFromAllStagesPage->deleteFromStage('Stage');
331
        $deletedFromAllStagesPage->deleteFromStage('Live');
332
        $this->assertTrue($deletedFromAllStagesPage->isArchived());
333
        $this->assertFalse($deletedFromAllStagesPage->isOnDraft());
334
        $this->assertFalse($deletedFromAllStagesPage->isPublished());
335
        $this->assertFalse($deletedFromAllStagesPage->isOnDraftOnly());
336
        $this->assertFalse($deletedFromAllStagesPage->isOnLiveOnly());
337
        $this->assertFalse($deletedFromAllStagesPage->isModifiedOnDraft());
338
339
        // published page, modified
340
        $modifiedOnDraftPage = SiteTree::create();
341
        $modifiedOnDraftPage->write();
342
        $modifiedOnDraftPage->copyVersionToStage('Stage', 'Live');
343
        $modifiedOnDraftPage->Content = 'modified';
344
        $modifiedOnDraftPage->write();
345
        $this->assertFalse($modifiedOnDraftPage->isArchived());
346
        $this->assertTrue($modifiedOnDraftPage->isOnDraft());
347
        $this->assertTrue($modifiedOnDraftPage->isPublished());
348
        $this->assertFalse($modifiedOnDraftPage->isOnDraftOnly());
349
        $this->assertFalse($modifiedOnDraftPage->isOnLiveOnly());
350
        $this->assertTrue($modifiedOnDraftPage->isModifiedOnDraft());
351
    }
352
353
    /**
354
     * Test that a page can be completely deleted and restored to the stage site
355
     */
356
    public function testRestoreToStage()
357
    {
358
        $page = $this->objFromFixture(SiteTree::class, 'about');
359
        $pageID = $page->ID;
360
        $page->delete();
361
        $this->assertTrue(!DataObject::get_by_id(SiteTree::class, $pageID));
362
363
        $deletedPage = Versioned::get_latest_version(SiteTree::class, $pageID);
364
        $resultPage = $deletedPage->doRestoreToStage();
365
366
        $requeriedPage = DataObject::get_by_id(SiteTree::class, $pageID);
367
368
        $this->assertEquals($pageID, $resultPage->ID);
369
        $this->assertEquals($pageID, $requeriedPage->ID);
370
        $this->assertEquals('About Us', $requeriedPage->Title);
371
        $this->assertInstanceOf(SiteTree::class, $requeriedPage);
372
373
374
        $page2 = $this->objFromFixture(SiteTree::class, 'products');
375
        $page2ID = $page2->ID;
376
        $page2->doUnpublish();
377
        $page2->delete();
378
379
        // Check that if we restore while on the live site that the content still gets pushed to
380
        // stage
381
        Versioned::set_stage(Versioned::LIVE);
382
        $deletedPage = Versioned::get_latest_version(SiteTree::class, $page2ID);
383
        $deletedPage->doRestoreToStage();
384
        $this->assertFalse(
385
            (bool)Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, "\"SiteTree\".\"ID\" = " . $page2ID)
386
        );
387
388
        Versioned::set_stage(Versioned::DRAFT);
389
        $requeriedPage = DataObject::get_by_id(SiteTree::class, $page2ID);
390
        $this->assertEquals('Products', $requeriedPage->Title);
391
        $this->assertInstanceOf(SiteTree::class, $requeriedPage);
392
    }
393
394
    public function testNoCascadingDeleteWithoutID()
395
    {
396
        Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
0 ignored issues
show
Bug introduced by
The method update() does not exist on SilverStripe\Config\Coll...nfigCollectionInterface. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\Config\Coll...nfigCollectionInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

396
        Config::inst()->/** @scrutinizer ignore-call */ update('SiteTree', 'enforce_strict_hierarchy', true);
Loading history...
397
        $count = SiteTree::get()->count();
398
        $this->assertNotEmpty($count);
399
        $obj = SiteTree::create();
400
        $this->assertFalse($obj->exists());
401
        $fail = true;
402
        try {
403
            $obj->delete();
404
        } catch (LogicException $e) {
405
            $fail = false;
406
        }
407
        if ($fail) {
408
            $this->fail('Failed to throw delete exception');
409
        }
410
        $this->assertCount($count, SiteTree::get());
411
    }
412
413
    public function testGetByLink()
414
    {
415
        $home     = $this->objFromFixture(SiteTree::class, 'home');
416
        $about    = $this->objFromFixture(SiteTree::class, 'about');
417
        $staff    = $this->objFromFixture(SiteTree::class, 'staff');
418
        $product  = $this->objFromFixture(SiteTree::class, 'product1');
419
420
        SiteTree::config()->nested_urls = false;
421
422
        $this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
423
        $this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
424
        $this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
425
        $this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
426
        $this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
427
428
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
429
430
        $this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
431
        $this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
432
        $this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
433
        $this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
434
        $this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
435
436
        $this->assertEquals(
437
            $staff->ID,
438
            SiteTree::get_by_link('/my-staff/', false)->ID,
439
            'Assert a unique URLSegment can be used for b/c.'
440
        );
441
    }
442
443
    public function testRelativeLink()
444
    {
445
        $about    = $this->objFromFixture(SiteTree::class, 'about');
446
        $staff    = $this->objFromFixture(SiteTree::class, 'staff');
447
448
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
449
450
        $this->assertEquals(
451
            'about-us/',
452
            $about->RelativeLink(),
453
            'Matches URLSegment on top level without parameters'
454
        );
455
        $this->assertEquals(
456
            'about-us/my-staff/',
457
            $staff->RelativeLink(),
458
            'Matches URLSegment plus parent on second level without parameters'
459
        );
460
        $this->assertEquals(
461
            'about-us/edit',
462
            $about->RelativeLink('edit'),
463
            'Matches URLSegment plus parameter on top level'
464
        );
465
        $this->assertEquals(
466
            'about-us/tom&jerry',
467
            $about->RelativeLink('tom&jerry'),
468
            'Doesnt url encode parameter'
469
        );
470
    }
471
472
    public function testPageLevel()
473
    {
474
        $about = $this->objFromFixture(SiteTree::class, 'about');
475
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
476
        $this->assertEquals(1, $about->getPageLevel());
477
        $this->assertEquals(2, $staff->getPageLevel());
478
    }
479
480
    public function testAbsoluteLiveLink()
481
    {
482
        $parent = $this->objFromFixture(SiteTree::class, 'about');
483
        $child = $this->objFromFixture(SiteTree::class, 'staff');
484
485
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
486
487
        $child->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
488
        $parent->URLSegment = 'changed-on-live';
489
        $parent->write();
490
        $parent->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
491
        $parent->URLSegment = 'changed-on-draft';
492
        $parent->write();
493
494
        $this->assertStringEndsWith('changed-on-live/my-staff/', $child->getAbsoluteLiveLink(false));
495
        $this->assertStringEndsWith('changed-on-live/my-staff/?stage=Live', $child->getAbsoluteLiveLink());
496
    }
497
498
    public function testDuplicateChildrenRetainSort()
499
    {
500
        $parent = SiteTree::create();
501
        $parent->Title = 'Parent';
502
        $parent->write();
503
504
        $child1 = SiteTree::create();
505
        $child1->ParentID = $parent->ID;
506
        $child1->Title = 'Child 1';
507
        $child1->Sort = 2;
508
        $child1->write();
509
510
        $child2 = SiteTree::create();
511
        $child2->ParentID = $parent->ID;
512
        $child2->Title = 'Child 2';
513
        $child2->Sort = 1;
514
        $child2->write();
515
516
        $duplicateParent = $parent->duplicateWithChildren();
517
        $duplicateChildren = $duplicateParent->AllChildren()->toArray();
518
        $this->assertCount(2, $duplicateChildren);
519
520
        $duplicateChild2 = array_shift($duplicateChildren);
521
        $duplicateChild1 = array_shift($duplicateChildren);
522
523
524
        $this->assertEquals('Child 1', $duplicateChild1->Title);
525
        $this->assertEquals('Child 2', $duplicateChild2->Title);
526
527
        // assertGreaterThan works by having the LOWER value first
528
        $this->assertGreaterThan($duplicateChild2->Sort, $duplicateChild1->Sort);
529
    }
530
531
    public function testDeleteFromStageOperatesRecursively()
532
    {
533
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
534
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
535
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
536
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
537
538
        $pageAbout->delete();
539
540
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
541
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
542
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
543
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
544
    }
545
546
    public function testDeleteFromStageOperatesRecursivelyStrict()
547
    {
548
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
549
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
550
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
551
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
552
553
        $pageAbout->delete();
554
555
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
556
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
557
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
558
    }
559
560
    public function testDuplicate()
561
    {
562
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
563
        $dupe = $pageAbout->duplicate();
564
        $this->assertEquals($pageAbout->Title, $dupe->Title);
565
        $this->assertNotEquals($pageAbout->URLSegment, $dupe->URLSegment);
566
        $this->assertNotEquals($pageAbout->Sort, $dupe->Sort);
567
    }
568
569
    public function testDeleteFromLiveOperatesRecursively()
570
    {
571
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
572
        $this->logInWithPermission('ADMIN');
573
574
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
575
        $pageAbout->publishRecursive();
576
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
577
        $pageStaff->publishRecursive();
578
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
579
        $pageStaffDuplicate->publishRecursive();
580
581
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
582
583
        $parentPage->doUnpublish();
584
585
        Versioned::set_stage(Versioned::LIVE);
586
587
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
588
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
589
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
590
        Versioned::set_stage(Versioned::DRAFT);
591
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
592
    }
593
594
    public function testUnpublishDoesNotDeleteChildrenWithLooseHierachyOn()
595
    {
596
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
597
        $this->logInWithPermission('ADMIN');
598
599
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
600
        $pageAbout->publishRecursive();
601
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
602
        $pageStaff->publishRecursive();
603
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
604
        $pageStaffDuplicate->publishRecursive();
605
606
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
607
        $parentPage->doUnpublish();
608
609
        Versioned::set_stage(Versioned::LIVE);
610
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
611
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
612
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
613
        Versioned::set_stage(Versioned::DRAFT);
614
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
615
    }
616
617
    public function testDeleteFromLiveOperatesRecursivelyStrict()
618
    {
619
        Config::inst()->update(SiteTree::class, 'enforce_strict_hierarchy', true);
620
        $this->logInWithPermission('ADMIN');
621
622
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
623
        $pageAbout->publishRecursive();
624
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
625
        $pageStaff->publishRecursive();
626
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
627
        $pageStaffDuplicate->publishRecursive();
628
629
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
630
        $parentPage->doUnpublish();
631
632
        Versioned::set_stage(Versioned::LIVE);
633
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
634
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
635
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
636
        Versioned::set_stage(Versioned::DRAFT);
637
    }
638
639
    /**
640
     * Simple test to confirm that querying from a particular archive date doesn't throw
641
     * an error
642
     */
643
    public function testReadArchiveDate()
644
    {
645
        $date = '2009-07-02 14:05:07';
646
        Versioned::reading_archived_date($date);
647
        SiteTree::get()->where([
648
            '"SiteTree"."ParentID"' => 0,
649
        ])->sql($args);
650
        $this->assertContains($date, $args);
651
    }
652
653
    public function testEditPermissions()
654
    {
655
        $editor = $this->objFromFixture(Member::class, "editor");
656
657
        $home = $this->objFromFixture(SiteTree::class, "home");
658
        $staff = $this->objFromFixture(SiteTree::class, "staff");
659
        $products = $this->objFromFixture(SiteTree::class, "products");
660
        $product1 = $this->objFromFixture(SiteTree::class, "product1");
661
        $product4 = $this->objFromFixture(SiteTree::class, "product4");
662
663
        // Test logged out users cannot edit
664
        $this->logOut();
665
        $this->assertFalse($staff->canEdit());
666
667
        // Can't edit a page that is locked to admins
668
        $this->assertFalse($home->canEdit($editor));
669
670
        // Can edit a page that is locked to editors
671
        $this->assertTrue($products->canEdit($editor));
672
673
        // Can edit a child of that page that inherits
674
        $this->assertTrue($product1->canEdit($editor));
675
676
        // Can't edit a child of that page that has its permissions overridden
677
        $this->assertFalse($product4->canEdit($editor));
678
    }
679
680
    public function testCanEditWithAccessToAllSections()
681
    {
682
        $page = SiteTree::create();
683
        $page->write();
684
        $allSectionMember = $this->objFromFixture(Member::class, 'allsections');
685
        $securityAdminMember = $this->objFromFixture(Member::class, 'securityadmin');
686
687
        $this->assertTrue($page->canEdit($allSectionMember));
688
        $this->assertFalse($page->canEdit($securityAdminMember));
689
    }
690
691
    public function testCreatePermissions()
692
    {
693
        // Test logged out users cannot create
694
        $this->logOut();
695
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
696
697
        // Login with another permission
698
        $this->logInWithPermission('DUMMY');
699
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
700
701
        // Login with basic CMS permission
702
        $perms = SiteConfig::config()->required_permission;
703
        $this->logInWithPermission(reset($perms));
704
        $this->assertTrue(singleton(SiteTree::class)->canCreate());
705
706
        // Test creation underneath a parent which this user doesn't have access to
707
        $parent = $this->objFromFixture(SiteTree::class, 'about');
708
        $this->assertFalse(singleton(SiteTree::class)->canCreate(null, ['Parent' => $parent]));
709
710
        // Test creation underneath a parent which doesn't allow a certain child
711
        $parentB = new SiteTreeTest_ClassB();
712
        $parentB->Title = 'Only Allows SiteTreeTest_ClassC';
713
        $parentB->write();
714
        $this->assertTrue(singleton(SiteTreeTest_ClassA::class)->canCreate(null));
715
        $this->assertFalse(singleton(SiteTreeTest_ClassA::class)->canCreate(null, ['Parent' => $parentB]));
716
        $this->assertTrue(singleton(SiteTreeTest_ClassC::class)->canCreate(null, ['Parent' => $parentB]));
717
718
        // Test creation underneath a parent which doesn't exist in the database. This should
719
        // fall back to checking whether the user can create pages at the root of the site
720
        $this->assertTrue(singleton(SiteTree::class)->canCreate(null, ['Parent' => singleton(SiteTree::class)]));
721
722
        //Test we don't check for allowedChildren on parent context if it's not SiteTree instance
723
        $this->assertTrue(
724
            singleton(SiteTree::class)->canCreate(
725
                null,
726
                ['Parent' => $this->objFromFixture(SiteTreeTest_DataObject::class, 'relations')]
727
            )
728
        );
729
    }
730
731
    public function testEditPermissionsOnDraftVsLive()
732
    {
733
        // Create an inherit-permission page
734
        $page = SiteTree::create();
735
        $page->write();
736
        $page->CanEditType = "Inherit";
737
        $page->publishRecursive();
738
        $pageID = $page->ID;
0 ignored issues
show
Unused Code introduced by
The assignment to $pageID is dead and can be removed.
Loading history...
739
740
        // Lock down the site config
741
        $sc = $page->SiteConfig;
742
        $sc->CanEditType = 'OnlyTheseUsers';
743
        $sc->EditorGroups()->add($this->idFromFixture(Group::class, 'admins'));
744
        $sc->write();
745
746
        // Confirm that Member.editor can't edit the page
747
        $member = $this->objFromFixture(Member::class, 'editor');
748
        Security::setCurrentUser($member);
749
        $this->assertFalse($page->canEdit());
750
751
        // Change the page to be editable by Group.editors, but do not publish
752
        $admin = $this->objFromFixture(Member::class, 'admin');
753
        Security::setCurrentUser($admin);
754
        $page->CanEditType = 'OnlyTheseUsers';
755
        $page->EditorGroups()->add($this->idFromFixture(Group::class, 'editors'));
756
        $page->write();
757
758
        // Clear permission cache
759
        /** @var InheritedPermissions $checker */
760
        $checker = SiteTree::getPermissionChecker();
761
        $checker->clearCache();
762
763
        // Confirm that Member.editor can now edit the page
764
        $member = $this->objFromFixture(Member::class, 'editor');
765
        Security::setCurrentUser($member);
766
        $this->assertTrue($page->canEdit());
767
768
        // Publish the changes to the page
769
        $admin = $this->objFromFixture(Member::class, 'admin');
770
        Security::setCurrentUser($admin);
771
        $page->publishRecursive();
772
773
        // Confirm that Member.editor can still edit the page
774
        $member = $this->objFromFixture(Member::class, 'editor');
775
        Security::setCurrentUser($member);
776
        $this->assertTrue($page->canEdit());
777
    }
778
779
    public function testCompareVersions()
780
    {
781
        // Necessary to avoid
782
        $oldCleanerClass = Diff::$html_cleaner_class;
783
        Diff::$html_cleaner_class = SiteTreeTest_NullHtmlCleaner::class;
784
785
        $page = SiteTree::create();
786
        $page->write();
787
        $this->assertEquals(1, $page->Version);
788
789
        // Use inline element to avoid double wrapping applied to
790
        // blocklevel elements depending on HTMLCleaner implementation:
791
        // <ins><p> gets converted to <ins><p><inst>
792
        $page->Content = "<span>This is a test</span>";
793
        $page->write();
794
        $this->assertEquals(2, $page->Version);
795
796
        $diff = $page->compareVersions(1, 2);
797
798
        $processedContent = trim($diff->Content);
799
        $processedContent = preg_replace('/\s*</', '<', $processedContent);
800
        $processedContent = preg_replace('/>\s*/', '>', $processedContent);
801
        $this->assertEquals("<ins><span>This is a test</span></ins>", $processedContent);
802
803
        Diff::$html_cleaner_class = $oldCleanerClass;
804
    }
805
806
    public function testAuthorIDAndPublisherIDFilledOutOnPublish()
807
    {
808
        // Ensure that we have a member ID who is doing all this work
809
        $member = $this->objFromFixture(Member::class, "admin");
810
        $this->logInAs($member);
811
812
        // Write the page
813
        $about = $this->objFromFixture(SiteTree::class, 'about');
814
        $about->Title = "Another title";
815
        $about->write();
816
817
        // Check the version created
818
        $savedVersion = DB::prepared_query(
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\Connect\Query::first() has been deprecated: Use record() instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

818
        $savedVersion = /** @scrutinizer ignore-deprecated */ DB::prepared_query(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
819
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
820
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
821
            [$about->ID]
822
        )->first();
823
        $this->assertEquals($member->ID, $savedVersion['AuthorID']);
824
        $this->assertEquals(0, $savedVersion['PublisherID']);
825
826
        // Publish the page
827
        $about->publishRecursive();
828
        $publishedVersion = DB::prepared_query(
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\Connect\Query::first() has been deprecated: Use record() instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

828
        $publishedVersion = /** @scrutinizer ignore-deprecated */ DB::prepared_query(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
829
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
830
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
831
            [$about->ID]
832
        )->first();
833
834
        // Check the version created
835
        $this->assertEquals($member->ID, $publishedVersion['AuthorID']);
836
        $this->assertEquals($member->ID, $publishedVersion['PublisherID']);
837
    }
838
839
    public function testLinkShortcodeHandler()
840
    {
841
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
842
        $redirectPage = $this->objFromFixture(RedirectorPage::class, 'external');
843
844
        $parser = new ShortcodeParser();
845
        $parser->register('sitetree_link', [SiteTree::class, 'link_shortcode_handler']);
846
847
        $aboutShortcode = sprintf('[sitetree_link,id=%d]', $aboutPage->ID);
848
        $aboutEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $aboutPage->ID);
849
850
        $aboutShortcodeExpected = $aboutPage->Link();
851
        $aboutEnclosedExpected  = sprintf('<a href="%s">Example Content</a>', $aboutPage->Link());
852
853
        $this->assertEquals(
854
            $aboutShortcodeExpected,
855
            $parser->parse($aboutShortcode),
856
            'Test that simple linking works.'
857
        );
858
        $this->assertEquals(
859
            $aboutEnclosedExpected,
860
            $parser->parse($aboutEnclosed),
861
            'Test enclosed content is linked.'
862
        );
863
864
        $aboutPage->delete();
865
866
        $this->assertEquals(
867
            $aboutShortcodeExpected,
868
            $parser->parse($aboutShortcode),
869
            'Test that deleted pages still link.'
870
        );
871
        $this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed));
872
873
        $aboutShortcode = '[sitetree_link,id="-1"]';
874
        $aboutEnclosed  = '[sitetree_link,id="-1"]Example Content[/sitetree_link]';
875
876
        $this->assertEquals('', $parser->parse($aboutShortcode), 'Test empty result if no suitable matches.');
877
        $this->assertEquals('', $parser->parse($aboutEnclosed));
878
879
        $redirectShortcode = sprintf('[sitetree_link,id=%d]', $redirectPage->ID);
880
        $redirectEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $redirectPage->ID);
881
        $redirectExpected = 'http://www.google.com?a&amp;b';
882
883
        $this->assertEquals($redirectExpected, $parser->parse($redirectShortcode));
884
        $this->assertEquals(
885
            sprintf('<a href="%s">Example Content</a>', $redirectExpected),
886
            $parser->parse($redirectEnclosed)
887
        );
888
889
        $this->assertEquals('', $parser->parse('[sitetree_link]'), 'Test that invalid ID attributes are not parsed.');
890
        $this->assertEquals('', $parser->parse('[sitetree_link,id="text"]'));
891
        $this->assertEquals('', $parser->parse('[sitetree_link]Example Content[/sitetree_link]'));
892
    }
893
894
    public function testIsCurrent()
895
    {
896
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
897
        $productPage = $this->objFromFixture(SiteTree::class, 'products');
898
899
        Director::set_current_page($aboutPage);
900
        $this->assertTrue($aboutPage->isCurrent(), 'Assert that basic isCurrent checks works.');
901
        $this->assertFalse($productPage->isCurrent());
902
903
        $this->assertTrue(
904
            DataObject::get_one(SiteTree::class, [
905
                '"SiteTree"."Title"' => 'About Us',
906
            ])->isCurrent(),
907
            'Assert that isCurrent works on another instance with the same ID.'
908
        );
909
910
        Director::set_current_page($newPage = SiteTree::create());
911
        $this->assertTrue($newPage->isCurrent(), 'Assert that isCurrent works on unsaved pages.');
912
    }
913
914
    public function testIsSection()
915
    {
916
        $about = $this->objFromFixture(SiteTree::class, 'about');
917
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
918
        $ceo   = $this->objFromFixture(SiteTree::class, 'ceo');
919
920
        Director::set_current_page($about);
921
        $this->assertTrue($about->isSection());
922
        $this->assertFalse($staff->isSection());
923
        $this->assertFalse($ceo->isSection());
924
925
        Director::set_current_page($staff);
926
        $this->assertTrue($about->isSection());
927
        $this->assertTrue($staff->isSection());
928
        $this->assertFalse($ceo->isSection());
929
930
        Director::set_current_page($ceo);
931
        $this->assertTrue($about->isSection());
932
        $this->assertTrue($staff->isSection());
933
        $this->assertTrue($ceo->isSection());
934
    }
935
936
    public function testURLSegmentReserved()
937
    {
938
        $siteTree = SiteTree::create(['URLSegment' => 'admin']);
939
        $segment = $siteTree->validURLSegment();
940
941
        $this->assertFalse($segment);
942
    }
943
944
    public function testURLSegmentAutoUpdate()
945
    {
946
        $sitetree = SiteTree::create();
947
        $sitetree->Title = _t(
948
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
949
            'New {pagetype}',
950
            ['pagetype' => $sitetree->i18n_singular_name()]
951
        );
952
        $sitetree->write();
953
        $this->assertEquals(
954
            'new-page',
955
            $sitetree->URLSegment,
956
            'Sets based on default title on first save'
957
        );
958
959
        $sitetree->Title = 'Changed';
960
        $sitetree->write();
961
        $this->assertEquals(
962
            'changed',
963
            $sitetree->URLSegment,
964
            'Auto-updates when set to default title'
965
        );
966
967
        $sitetree->Title = 'Changed again';
968
        $sitetree->write();
969
        $this->assertEquals(
970
            'changed',
971
            $sitetree->URLSegment,
972
            'Does not auto-update once title has been changed'
973
        );
974
    }
975
976
    public function testURLSegmentAutoUpdateLocalized()
977
    {
978
        $oldLocale = i18n::get_locale();
979
        i18n::set_locale('de_DE');
980
981
        $sitetree = SiteTree::create();
982
        $sitetree->Title = _t(
983
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
984
            'New {pagetype}',
985
            ['pagetype' => $sitetree->i18n_singular_name()]
986
        );
987
        $sitetree->write();
988
        $this->assertEquals(
989
            'neue-seite',
990
            $sitetree->URLSegment,
991
            'Sets based on default title on first save'
992
        );
993
994
        $sitetree->Title = 'Changed';
995
        $sitetree->write();
996
        $this->assertEquals(
997
            'changed',
998
            $sitetree->URLSegment,
999
            'Auto-updates when set to default title'
1000
        );
1001
1002
        $sitetree->Title = 'Changed again';
1003
        $sitetree->write();
1004
        $this->assertEquals(
1005
            'changed',
1006
            $sitetree->URLSegment,
1007
            'Does not auto-update once title has been changed'
1008
        );
1009
1010
        i18n::set_locale($oldLocale);
1011
    }
1012
1013
    /**
1014
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1015
     */
1016
    public function testValidURLSegmentURLSegmentConflicts()
1017
    {
1018
        $sitetree = SiteTree::create();
1019
        SiteTree::config()->nested_urls = false;
1020
1021
        $sitetree->URLSegment = 'home';
1022
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1023
        $sitetree->URLSegment = 'home-noconflict';
1024
        $this->assertTrue($sitetree->validURLSegment());
1025
1026
        $sitetree->ParentID   = $this->idFromFixture(SiteTree::class, 'about');
1027
        $sitetree->URLSegment = 'home';
1028
        $this->assertFalse($sitetree->validURLSegment(), 'Conflicts are still recognised with a ParentID value');
1029
1030
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1031
1032
        $sitetree->ParentID   = 0;
1033
        $sitetree->URLSegment = 'home';
1034
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1035
1036
        $sitetree->ParentID = $this->idFromFixture(SiteTree::class, 'about');
1037
        $this->assertTrue($sitetree->validURLSegment(), 'URLSegments can be the same across levels');
1038
1039
        $sitetree->URLSegment = 'my-staff';
1040
        $this->assertFalse($sitetree->validURLSegment(), 'Nested URLSegment conflicts are recognised');
1041
        $sitetree->URLSegment = 'my-staff-noconflict';
1042
        $this->assertTrue($sitetree->validURLSegment());
1043
    }
1044
1045
    /**
1046
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1047
     */
1048
    public function testValidURLSegmentClassNameConflicts()
1049
    {
1050
        $sitetree = SiteTree::create();
1051
        $sitetree->URLSegment = Controller::class;
1052
1053
        $this->assertTrue($sitetree->validURLSegment(), 'Class names are no longer conflicts');
1054
    }
1055
1056
    /**
1057
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1058
     */
1059
    public function testValidURLSegmentControllerConflicts()
1060
    {
1061
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1062
1063
        $sitetree = SiteTree::create();
1064
        $sitetree->ParentID = $this->idFromFixture(SiteTreeTest_Conflicted::class, 'parent');
1065
1066
        $sitetree->URLSegment = 'index';
1067
        $this->assertFalse($sitetree->validURLSegment(), 'index is not a valid URLSegment');
1068
1069
        $sitetree->URLSegment = 'conflicted-action';
1070
        $this->assertFalse($sitetree->validURLSegment(), 'allowed_actions conflicts are recognised');
1071
1072
        $sitetree->URLSegment = 'conflicted-template';
1073
        $this->assertFalse($sitetree->validURLSegment(), 'Action-specific template conflicts are recognised');
1074
1075
        $sitetree->URLSegment = 'valid';
1076
        $this->assertTrue($sitetree->validURLSegment(), 'Valid URLSegment values are allowed');
1077
    }
1078
1079
    public function testURLSegmentPrioritizesExtensionVotes()
1080
    {
1081
        $sitetree = SiteTree::create();
1082
        $sitetree->URLSegment = 'unique-segment';
1083
        $this->assertTrue($sitetree->validURLSegment());
1084
1085
        SiteTree::add_extension(SiteTreeTest_Extension::class);
1086
        $sitetree = SiteTree::create();
1087
        $sitetree->URLSegment = 'unique-segment';
1088
        $this->assertFalse($sitetree->validURLSegment());
1089
        SiteTree::remove_extension(SiteTreeTest_Extension::class);
1090
    }
1091
1092
    public function testURLSegmentMultiByte()
1093
    {
1094
        URLSegmentFilter::config()->set('default_allow_multibyte', true);
1095
        $sitetree = SiteTree::create();
1096
        $sitetree->write();
1097
1098
        $sitetree->URLSegment = 'brötchen';
1099
        $sitetree->write();
1100
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1101
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1102
1103
        $sitetree->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1104
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1105
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1106
        $sitetreeLive = Versioned::get_one_by_stage(
1107
            SiteTree::class,
1108
            Versioned::LIVE,
1109
            '"SiteTree"."ID" = ' . $sitetree->ID,
1110
            false
1111
        );
1112
        $this->assertEquals($sitetreeLive->URLSegment, rawurlencode('brötchen'));
1113
    }
1114
1115
    public function testVersionsAreCreated()
1116
    {
1117
        $p = SiteTree::create();
1118
        $p->Content = "one";
1119
        $p->write();
1120
        $this->assertEquals(1, $p->Version);
1121
1122
        // No changes don't bump version
1123
        $p->write();
1124
        $this->assertEquals(1, $p->Version);
1125
1126
        $p->Content = "two";
1127
        $p->write();
1128
        $this->assertEquals(2, $p->Version);
1129
1130
        // Only change meta-data don't bump version
1131
        $p->HasBrokenLink = true;
1132
        $p->write();
1133
        $p->HasBrokenLink = false;
1134
        $p->write();
1135
        $this->assertEquals(2, $p->Version);
1136
1137
        $p->Content = "three";
1138
        $p->write();
1139
        $this->assertEquals(3, $p->Version);
1140
    }
1141
1142
    public function testPageTypeClasses()
1143
    {
1144
        $classes = SiteTree::page_type_classes();
1145
        $this->assertNotContains(SiteTree::class, $classes, 'Page types do not include base class');
1146
        $this->assertContains('Page', $classes, 'Page types do contain subclasses');
1147
1148
        // Testing what happens in an incorrect config value is set - hide_ancestor should be a string
1149
        Config::modify()->set(SiteTreeTest_ClassA::class, 'hide_ancestor', true);
1150
        $newClasses = SiteTree::page_type_classes();
1151
        $this->assertEquals(
1152
            $classes,
1153
            $newClasses,
1154
            'Setting hide_ancestor to a boolean (incorrect) value caused a page class to be hidden'
1155
        );
1156
    }
1157
1158
    /**
1159
     * Tests that core subclasses of SiteTree are included in allowedChildren() by default, but not instances of
1160
     * HiddenClass
1161
     */
1162
    public function testAllowedChildrenContainsCoreSubclassesButNotHiddenClass()
1163
    {
1164
        $page = SiteTree::create();
1165
        $allowedChildren = $page->allowedChildren();
1166
1167
        $this->assertContains(
1168
            VirtualPage::class,
1169
            $allowedChildren,
1170
            'Includes core subclasses by default'
1171
        );
1172
1173
        $this->assertNotContains(
1174
            SiteTreeTest_ClassE::class,
1175
            $allowedChildren,
1176
            'HiddenClass instances should not be returned'
1177
        );
1178
    }
1179
1180
    /**
1181
     * Tests that various types of SiteTree classes will or will not be returned from the allowedChildren method
1182
     * @dataProvider allowedChildrenProvider
1183
     * @param string $className
1184
     * @param array  $expected
1185
     * @param string $assertionMessage
1186
     */
1187
    public function testAllowedChildren($className, $expected, $assertionMessage)
1188
    {
1189
        $class = new $className();
1190
        $this->assertEquals($expected, $class->allowedChildren(), $assertionMessage);
1191
    }
1192
1193
    /**
1194
     * @return array
1195
     */
1196
    public function allowedChildrenProvider()
1197
    {
1198
        return [
1199
            [
1200
                // Class name
1201
                SiteTreeTest_ClassA::class,
1202
                // Expected
1203
                [ SiteTreeTest_ClassB::class ],
1204
                // Assertion message
1205
                'Direct setting of allowed children',
1206
            ],
1207
            [
1208
                SiteTreeTest_ClassB::class,
1209
                [ SiteTreeTest_ClassC::class, SiteTreeTest_ClassCext::class ],
1210
                'Includes subclasses',
1211
            ],
1212
            [
1213
                SiteTreeTest_ClassC::class,
1214
                [],
1215
                'Null setting',
1216
            ],
1217
            [
1218
                SiteTreeTest_ClassD::class,
1219
                [SiteTreeTest_ClassC::class],
1220
                'Excludes subclasses if class is prefixed by an asterisk',
1221
            ],
1222
        ];
1223
    }
1224
1225
    public function testAllowedChildrenValidation()
1226
    {
1227
        $page = SiteTree::create();
1228
        $page->write();
1229
        $classA = new SiteTreeTest_ClassA();
1230
        $classA->write();
1231
        $classB = new SiteTreeTest_ClassB();
1232
        $classB->write();
1233
        $classC = new SiteTreeTest_ClassC();
1234
        $classC->write();
1235
        $classD = new SiteTreeTest_ClassD();
1236
        $classD->write();
1237
        $classCext = new SiteTreeTest_ClassCext();
1238
        $classCext->write();
1239
1240
        $classB->ParentID = $page->ID;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1241
        $valid = $classB->doValidate();
1242
        $this->assertTrue($valid->isValid(), "Does allow children on unrestricted parent");
1243
1244
        $classB->ParentID = $classA->ID;
1245
        $valid = $classB->doValidate();
1246
        $this->assertTrue($valid->isValid(), "Does allow child specifically allowed by parent");
1247
1248
        $classC->ParentID = $classA->ID;
1249
        $valid = $classC->doValidate();
1250
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents specifically restricting children");
1251
1252
        $classB->ParentID = $classC->ID;
1253
        $valid = $classB->doValidate();
1254
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents disallowing all children");
1255
1256
        $classB->ParentID = $classCext->ID;
1257
        $valid = $classB->doValidate();
1258
        $this->assertTrue($valid->isValid(), "Extensions of allowed classes are incorrectly reported as invalid");
1259
1260
        $classCext->ParentID = $classD->ID;
1261
        $valid = $classCext->doValidate();
1262
        $this->assertFalse(
1263
            $valid->isValid(),
1264
            "Doesnt allow child where only parent class is allowed on parent node, and asterisk prefixing is used"
1265
        );
1266
    }
1267
1268
    public function testClassDropdown()
1269
    {
1270
        $sitetree = SiteTree::create();
1271
        $method = new ReflectionMethod($sitetree, 'getClassDropdown');
1272
        $method->setAccessible(true);
1273
1274
        Security::setCurrentUser(null);
1275
        $this->assertArrayNotHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1276
1277
        $this->loginWithPermission('ADMIN');
1278
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1279
1280
        $this->loginWithPermission('CMS_ACCESS_CMSMain');
1281
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1282
1283
        Security::setCurrentUser(null);
1284
    }
1285
1286
    public function testCanBeRoot()
1287
    {
1288
        $page = SiteTree::create();
1289
        $page->ParentID = 0;
1290
        $page->write();
1291
1292
        $notRootPage = new SiteTreeTest_NotRoot();
1293
        $notRootPage->ParentID = 0;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1294
        $isDetected = false;
1295
        try {
1296
            $notRootPage->write();
1297
        } catch (ValidationException $e) {
1298
            $this->assertContains('is not allowed on the root level', $e->getMessage());
1299
            $isDetected = true;
1300
        }
1301
1302
        if (!$isDetected) {
1303
            $this->fail('Fails validation with $can_be_root=false');
1304
        }
1305
    }
1306
1307
    public function testModifyStatusFlagByInheritance()
1308
    {
1309
        $node = new SiteTreeTest_StageStatusInherit();
1310
        $treeTitle = $node->getTreeTitle();
1311
        $this->assertContains('InheritedTitle', $treeTitle);
1312
        $this->assertContains('inherited-class', $treeTitle);
1313
    }
1314
1315
    public function testMenuTitleIsUnsetWhenEqualsTitle()
1316
    {
1317
        $page = SiteTree::create();
1318
        $page->Title = 'orig';
1319
        $page->MenuTitle = 'orig';
1320
        $page->write();
1321
1322
        // change menu title
1323
        $page->MenuTitle = 'changed';
1324
        $page->write();
1325
        $page = SiteTree::get()->byID($page->ID);
1326
        $this->assertEquals('changed', $page->getField('MenuTitle'));
1327
1328
        // change menu title back
1329
        $page->MenuTitle = 'orig';
1330
        $page->write();
1331
        $page = SiteTree::get()->byID($page->ID);
1332
        $this->assertEquals(null, $page->getField('MenuTitle'));
1333
    }
1334
1335
    public function testMetaTagGeneratorDisabling()
1336
    {
1337
        $generator = Config::inst()->get(SiteTree::class, 'meta_generator');
0 ignored issues
show
Unused Code introduced by
The assignment to $generator is dead and can be removed.
Loading history...
1338
1339
        // Stub to ensure customisations don't affect the test
1340
        Config::modify()->set(SiteTree::class, 'meta_generator', 'SilverStripe - https://www.silverstripe.org');
1341
1342
        $page = new SiteTreeTest_PageNode();
1343
1344
        $meta = $page->MetaTags();
1345
        $this->assertContains('meta name="generator"', $meta, 'Should include generator tag');
1346
        $this->assertContains(
1347
            'content="SilverStripe - https://www.silverstripe.org',
1348
            $meta,
1349
            'Should contain default meta generator info'
1350
        );
1351
1352
        // test proper escaping of quotes in attribute value
1353
        Config::modify()->set(SiteTree::class, 'meta_generator', 'Generator with "quotes" in it');
1354
        $meta = $page->MetaTags();
1355
        $this->assertContains(
1356
            'content="Generator with &quot;quotes&quot; in it',
1357
            $meta,
1358
            'test proper escaping of values from Config'
1359
        );
1360
1361
        // test empty generator - no tag should appear at all
1362
        Config::modify()->set(SiteTree::class, 'meta_generator', '');
1363
        $meta = $page->MetaTags();
1364
        $this->assertNotContains(
1365
            'meta name="generator"',
1366
            $meta,
1367
            'test blank value means no tag generated'
1368
        );
1369
    }
1370
1371
1372
    public function testGetBreadcrumbItems()
1373
    {
1374
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs");
1375
        $this->assertEquals(1, $page->getBreadcrumbItems()->count(), "Only display current page.");
1376
1377
        // Test breadcrumb order
1378
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs5");
1379
        $breadcrumbs = $page->getBreadcrumbItems();
1380
        $this->assertEquals($breadcrumbs->count(), 5, "Display all breadcrumbs");
1381
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs", "Breadcrumbs should be the first item.");
1382
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last item.");
1383
1384
        // Test breadcrumb max depth
1385
        $breadcrumbs = $page->getBreadcrumbItems(2);
1386
        $this->assertEquals($breadcrumbs->count(), 2, "Max depth should limit the breadcrumbs to 2 items.");
1387
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs 4", "First item should be Breadrcumbs 4.");
1388
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last.");
1389
    }
1390
1391
    /**
1392
     * Tests SiteTree::MetaTags
1393
     * Note that this test makes no assumption on the closing of tags (other than <title></title>)
1394
     */
1395
    public function testMetaTags()
1396
    {
1397
        $this->logInWithPermission('ADMIN');
1398
        $page = $this->objFromFixture(SiteTree::class, 'metapage');
1399
1400
        // Test with title
1401
        $meta = $page->MetaTags();
1402
        $charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
1403
        $this->assertContains('<meta http-equiv="Content-Type" content="text/html; charset=' . $charset . '"', $meta);
1404
        $this->assertContains('<meta name="description" content="The &lt;br /&gt; and &lt;br&gt; tags"', $meta);
1405
        $this->assertContains('<link rel="canonical" href="http://www.mysite.com/html-and-xml"', $meta);
1406
        $this->assertContains('<meta name="x-page-id" content="' . $page->ID . '"', $meta);
1407
        $this->assertContains('<meta name="x-cms-edit-link" content="' . $page->CMSEditLink() . '"', $meta);
1408
        $this->assertContains('<title>HTML &amp; XML</title>', $meta);
1409
1410
        // Test without title
1411
        $meta = $page->MetaTags(false);
1412
        $this->assertNotContains('<title>', $meta);
1413
    }
1414
1415
    /**
1416
     * Test that orphaned pages are handled correctly
1417
     */
1418
    public function testOrphanedPages()
1419
    {
1420
        $origStage = Versioned::get_reading_mode();
1421
1422
        // Setup user who can view draft content, but lacks cms permission.
1423
        // To users such as this, orphaned pages should be inaccessible. canView for these pages is only
1424
        // necessary for admin / cms users, who require this permission to edit / rearrange these pages.
1425
        $permission = new Permission();
1426
        $permission->Code = 'VIEW_DRAFT_CONTENT';
1427
        $group = new Group(['Title' => 'Staging Users']);
1428
        $group->write();
1429
        $group->Permissions()->add($permission);
1430
        $member = new Member();
1431
        $member->Email = '[email protected]';
1432
        $member->write();
1433
        $member->Groups()->add($group);
1434
1435
        // both pages are viewable in stage
1436
        Versioned::set_stage(Versioned::DRAFT);
1437
        $about = $this->objFromFixture(SiteTree::class, 'about');
1438
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1439
        $this->assertFalse($about->isOrphaned());
1440
        $this->assertFalse($staff->isOrphaned());
1441
        $this->assertTrue($about->canView($member));
1442
        $this->assertTrue($staff->canView($member));
1443
1444
        // Publishing only the child page to live should orphan the live record, but not the staging one
1445
        $staff->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1446
        $this->assertFalse($staff->isOrphaned());
1447
        $this->assertTrue($staff->canView($member));
1448
        Versioned::set_stage(Versioned::LIVE);
1449
        $staff = $this->objFromFixture(SiteTree::class, 'staff'); // Live copy of page
1450
        $this->assertTrue($staff->isOrphaned()); // because parent isn't published
1451
        $this->assertFalse($staff->canView($member));
1452
1453
        // Publishing the parent page should restore visibility
1454
        Versioned::set_stage(Versioned::DRAFT);
1455
        $about = $this->objFromFixture(SiteTree::class, 'about');
1456
        $about->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1457
        Versioned::set_stage(Versioned::LIVE);
1458
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1459
        $this->assertFalse($staff->isOrphaned());
1460
        $this->assertTrue($staff->canView($member));
1461
1462
        // Removing staging page should not prevent live page being visible
1463
        $about->deleteFromStage('Stage');
1464
        $staff->deleteFromStage('Stage');
1465
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1466
        $this->assertFalse($staff->isOrphaned());
1467
        $this->assertTrue($staff->canView($member));
1468
1469
        // Cleanup
1470
        Versioned::set_reading_mode($origStage);
1471
    }
1472
1473
    /**
1474
     * Test archived page behaviour
1475
     */
1476
    public function testArchivedPages()
1477
    {
1478
        $this->logInWithPermission('ADMIN');
1479
1480
        /** @var SiteTree $page */
1481
        $page = $this->objFromFixture(SiteTree::class, 'home');
1482
        $this->assertTrue($page->canAddChildren());
1483
        $this->assertTrue($page->isOnDraft());
1484
        $this->assertFalse($page->isPublished());
1485
1486
        // Publish
1487
        $page->publishRecursive();
1488
        $this->assertTrue($page->canAddChildren());
1489
        $this->assertTrue($page->isOnDraft());
1490
        $this->assertTrue($page->isPublished());
1491
1492
        // Archive
1493
        $page->doArchive();
1494
        $this->assertFalse($page->canAddChildren());
1495
        $this->assertFalse($page->isOnDraft());
1496
        $this->assertTrue($page->isArchived());
1497
        $this->assertFalse($page->isPublished());
1498
    }
1499
1500
    public function testCanNot()
1501
    {
1502
        // Test that
1503
        $this->logInWithPermission('ADMIN');
1504
        $page = new SiteTreeTest_AdminDenied();
1505
        $this->assertFalse($page->canCreate());
1506
        $this->assertFalse($page->canEdit());
1507
        $this->assertFalse($page->canDelete());
1508
        $this->assertFalse($page->canAddChildren());
1509
        $this->assertFalse($page->canView());
1510
    }
1511
1512
    public function testCanPublish()
1513
    {
1514
        $page = new SiteTreeTest_ClassD();
1515
        $this->logOut();
1516
1517
        // Test that false overrides any can_publish = true
1518
        SiteTreeTest_ExtensionA::$can_publish = true;
1519
        SiteTreeTest_ExtensionB::$can_publish = false;
1520
        $this->assertFalse($page->canPublish());
1521
        SiteTreeTest_ExtensionA::$can_publish = false;
1522
        SiteTreeTest_ExtensionB::$can_publish = true;
1523
        $this->assertFalse($page->canPublish());
1524
1525
        // Test null extensions fall back to canEdit()
1526
        SiteTreeTest_ExtensionA::$can_publish = null;
1527
        SiteTreeTest_ExtensionB::$can_publish = null;
1528
        $page->canEditValue = true;
1529
        $this->assertTrue($page->canPublish());
1530
        $page->canEditValue = false;
1531
        $this->assertFalse($page->canPublish());
1532
    }
1533
1534
    /**
1535
     * Test url rewriting extensions
1536
     */
1537
    public function testLinkExtension()
1538
    {
1539
        Director::config()->set('alternate_base_url', 'http://www.baseurl.com');
1540
        $page = new SiteTreeTest_ClassD();
1541
        $page->URLSegment = 'classd';
1542
        $page->write();
1543
        $this->assertEquals(
1544
            'http://www.updatedhost.com/classd/myaction?extra=1',
1545
            $page->Link('myaction')
1546
        );
1547
        $this->assertEquals(
1548
            'http://www.updatedhost.com/classd/myaction?extra=1',
1549
            $page->AbsoluteLink('myaction')
1550
        );
1551
        $this->assertEquals(
1552
            'classd/myaction',
1553
            $page->RelativeLink('myaction')
1554
        );
1555
    }
1556
1557
    /**
1558
     * Test that the controller name for a SiteTree instance can be gathered by appending "Controller" to the SiteTree
1559
     * class name in a PSR-2 compliant manner.
1560
     */
1561
    public function testGetControllerName()
1562
    {
1563
        $class = Page::create();
1564
        $this->assertSame('PageController', $class->getControllerName());
1565
    }
1566
1567
    /**
1568
     * Test that the controller name for a SiteTree instance can be gathered when set directly via config var
1569
     */
1570
    public function testGetControllerNameFromConfig()
1571
    {
1572
        Config::inst()->update(Page::class, 'controller_name', 'This\\Is\\A\\New\\Controller');
1573
        $class = new Page();
1574
        $this->assertSame('This\\Is\\A\\New\\Controller', $class->getControllerName());
1575
    }
1576
1577
    /**
1578
     * Test that underscored class names (legacy) are still supported (deprecation notice is issued though).
1579
     */
1580
    public function testGetControllerNameWithUnderscoresIsSupported()
1581
    {
1582
        $class = new SiteTreeTest_LegacyControllerName();
1583
        $this->assertEquals(SiteTreeTest_LegacyControllerName_Controller::class, $class->getControllerName());
1584
    }
1585
1586
    public function testTreeTitleCache()
1587
    {
1588
        $siteTree = SiteTree::create();
1589
        $user = $this->objFromFixture(Member::class, 'allsections');
1590
        Security::setCurrentUser($user);
1591
        $pageClass = array_values(SiteTree::page_type_classes())[0];
1592
1593
        $mockPageMissesCache = $this->getMockBuilder($pageClass)
1594
            ->setMethods(['canCreate'])
1595
            ->getMock();
1596
        $mockPageMissesCache
1597
            ->expects($this->exactly(3))
1598
            ->method('canCreate');
1599
1600
        $mockPageHitsCache = $this->getMockBuilder($pageClass)
1601
            ->setMethods(['canCreate'])
1602
            ->getMock();
1603
        $mockPageHitsCache
1604
            ->expects($this->never())
1605
            ->method('canCreate');
1606
1607
        // Initially, cache misses (1)
1608
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1609
        $title = $siteTree->getTreeTitle();
1610
        $this->assertNotNull($title);
1611
1612
        // Now it hits
1613
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1614
        $title = $siteTree->getTreeTitle();
1615
        $this->assertNotNull($title);
1616
1617
1618
        // Mutating member record invalidates cache. Misses (2)
1619
        $user->FirstName = 'changed';
1620
        $user->write();
1621
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1622
        $title = $siteTree->getTreeTitle();
1623
        $this->assertNotNull($title);
1624
1625
        // Now it hits again
1626
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1627
        $title = $siteTree->getTreeTitle();
1628
        $this->assertNotNull($title);
1629
1630
        // Different user. Misses. (3)
1631
        $user = $this->objFromFixture(Member::class, 'editor');
1632
        Security::setCurrentUser($user);
1633
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1634
        $title = $siteTree->getTreeTitle();
1635
        $this->assertNotNull($title);
1636
    }
1637
1638
    public function testDependentPagesOnUnsavedRecord()
1639
    {
1640
        $record = new SiteTree();
1641
        $pages = $record->DependentPages();
1642
        $this->assertCount(0, $pages, 'Unsaved pages should have no dependent pages');
1643
    }
1644
}
1645