Skip to content

Commit

Permalink
[TASK] Add functional tests for RootlineUtility
Browse files Browse the repository at this point in the history
This change introduces functional tests for the rootline utility.

For now, this patch ensures the validity of Workspace
Overlays when reading a given page tree / rootline.

Covered tests:
* Live workspace (no overlays)
* New record in workspace
* Modified record in workspace
* Deleted record in workspace
* Moved record in workspace

Adding such a test class helps to extend
coverage for language overlays and MountPoints
in conjunction with workspaces in the future.

Resolves: #92311
Releases: master, 10.4
Change-Id: Ie8a746253e4b10f98c05a448d736a5d00d59d3fc
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/65724
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
bmack committed Sep 16, 2020
1 parent c914ef0 commit 9e524c1
Show file tree
Hide file tree
Showing 2 changed files with 376 additions and 0 deletions.
@@ -0,0 +1,65 @@
__variables:
- &pageStandard 0
- &pageShortcut 4
- &pageMount 7

entitySettings:
'*':
nodeColumnName: 'pid'
columnNames: {id: 'uid', language: 'sys_language_uid'}
defaultValues: {pid: 0}
page:
isNode: true
tableName: 'pages'
parentColumnName: 'pid'
languageColumnNames: ['l10n_parent', 'l10n_source']
columnNames: {type: 'doktype', root: 'is_siteroot', mount: 'mount_pid', showContentOfMountedPage: 'mount_pid_ol', visitorGroups: 'fe_group'}
defaultValues: {hidden: 0, doktype: *pageStandard}
valueInstructions:
shortcut:
first: {shortcut: 0, shortcut_mode: 1}
language:
tableName: 'sys_language'
columnNames: {code: 'language_isocode'}
workspace:
tableName: 'sys_workspace'

entities:
workspace:
- self: {id: 1, title: 'My Personal Workspace' }
- self: {id: 2, title: 'Another Workspace' }
language:
- self: {id: 1, title: 'French', code: 'fr' }
- self: {id: 2, title: 'Franco-Canadian', code: 'fr' }
- self: {id: 3, title: 'Spanish', code: 'es'}
page:
- self: {id: 1000, title: 'ACME Global', type: *pageShortcut, shortcut: 'first', root: true, slug: '/'}
versionVariants:
- version: { workspace: 1, title: 'ACME Global modified in Workspace 1' }
children:
- self: {id: 1100, title: 'EN: Welcome', slug: '/welcome' }
languageVariants:
- self: {id: 1101, title: 'FR: Welcome', language: 1, slug: '/bienvenue' }
- self: {id: 1102, title: 'FR-CA: Welcome', language: 2, slug: '/bienvenue' }
- self: {id: 1200, title: 'EN: Features', slug: '/features'}
children:
- self: {id: 1210, title: 'EN: Frontend Editing', slug: '/features/frontend-editing'}
- self: {id: 1300, title: 'EN: Products', slug: '/products'}
versionVariants:
- version: { workspace: 2 }
actions:
- { action: 'delete' }
children:
- self: {id: 1310, title: 'EN: Toys', slug: '/products/toys'}
- self: {id: 1320, title: 'EN: Card Games', slug: '/products/card-games'}
- self: {id: 1330, title: 'EN: Board Games', slug: '/products/board-games'}
children:
- self: {id: 1331, title: 'EN: Monopoly', slug: '/products/monopoly'}
- self: {id: 1332, title: 'EN: Catan', slug: '/products/board-games/catan'}
- self: {id: 1333, title: 'EN: Risk', slug: '/risk'}
versionVariants:
- version: { workspace: 1 }
actions:
- { action: 'move', type: 'toPage', target: 1320 }
- version: {id: 1400, title: 'EN: A new page in workspace', slug: '/a-new-page', workspace: 1 }
- self: {id: 9999, title: 'Another root page as dummy to incremental numbers', root: true, slug: '/'}
311 changes: 311 additions & 0 deletions typo3/sysext/core/Tests/Functional/Utility/RootlineUtilityTest.php
@@ -0,0 +1,311 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

namespace TYPO3\CMS\Core\Tests\Functional\Utility;

use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Exception\Page\PageNotFoundException;
use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

/**
* Test case
*/
class RootlineUtilityTest extends FunctionalTestCase
{
use SiteBasedTestTrait;

protected const LANGUAGE_PRESETS = [
'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''],
'FR' => ['id' => 1, 'title' => 'French', 'locale' => 'fr_FR.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-FR', 'direction' => ''],
'FR-CA' => ['id' => 2, 'title' => 'Franco-Canadian', 'locale' => 'fr_CA.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-CA', 'direction' => ''],
'ES' => ['id' => 3, 'title' => 'Spanish', 'locale' => 'es_ES.UTF8', 'iso' => 'es', 'hrefLang' => 'es-ES', 'direction' => ''],
];

/**
* @var string[]
*/
protected $coreExtensionsToLoad = ['workspaces'];

protected function setUp(): void
{
parent::setUp();
RootlineUtility::purgeCaches();

$this->writeSiteConfiguration(
'main',
$this->buildSiteConfiguration(1000, 'https://acme.com/'),
[
$this->buildDefaultLanguageConfiguration('EN', '/'),
$this->buildLanguageConfiguration('FR', '/fr/', ['EN']),
$this->buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
]
);
$this->withDatabaseSnapshot(function () {
$this->setUpDatabase();
});
}

public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
static::initializeDatabaseSnapshot();
}

public static function tearDownAfterClass(): void
{
static::destroyDatabaseSnapshot();
parent::tearDownAfterClass();
}

protected function setUpDatabase()
{
$backendUser = $this->setUpBackendUserFromFixture(1);
Bootstrap::initializeLanguageObject();

$factory = DataHandlerFactory::fromYamlFile(__DIR__ . '/Fixtures/RootlineScenario.yaml');
$writer = DataHandlerWriter::withBackendUser($backendUser);
$writer->invokeFactory($factory);
static::failIfArrayIsNotEmpty(
$writer->getErrors()
);
}

/**
* @test
*/
public function resolveLivePagesAndSkipWorkspacedVersions()
{
$context = GeneralUtility::makeInstance(Context::class);
$context->setAspect('workspace', new WorkspaceAspect(0));
$subject = new RootlineUtility(1330, '', $context);
$result = $subject->get();

$expected = [
2 => [
'pid' => 1300,
'uid' => 1330,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' =>0,
'title' => 'EN: Board Games'
],
1 => [
'pid' => 1000,
'uid' => 1300,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'EN: Products'
],
0 => [
'pid' => 0,
'uid' => 1000,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'ACME Global'
],
];
self::assertSame($expected, $this->filterExpectedValues($result, ['pid', 'uid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_move_id', 'title']));
}

/**
* @test
*/
public function resolveWorkspaceOverlaysOfNewPageInWorkspace()
{
$context = GeneralUtility::makeInstance(Context::class);
$context->setAspect('workspace', new WorkspaceAspect(1));
$subject = new RootlineUtility(1400, '', $context);
$result = $subject->get();

$expected = [
1 => [
'pid' => 1000,
'uid' => 1400,
't3ver_oid' => 1400,
't3ver_wsid' => 1,
't3ver_state' => -1,
'title' => 'EN: A new page in workspace',
'_ORIG_pid' => 1000,
'_ORIG_uid' => 10002,
],
0 => [
'pid' => 0,
'uid' => 1000,
't3ver_oid' => 1000,
't3ver_wsid' => 1,
't3ver_state' => 0,
'title' => 'ACME Global modified in Workspace 1',
'_ORIG_pid' => 0,
'_ORIG_uid' => 10000,
],
];
self::assertSame($expected, $this->filterExpectedValues($result, ['pid', 'uid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_move_id', 'title', '_ORIG_uid', '_ORIG_pid']));
// Now explicitly requesting the versioned ID, which DOES not hold the same information
// it explicitly removes the _ORIG_uid, and does not rewrite the uid
$subject = new RootlineUtility(10002, '', $context);
$result = $subject->get();
$expected[1]['uid'] = 10002;
unset($expected[1]['_ORIG_uid']);
self::assertSame($expected, $this->filterExpectedValues($result, ['pid', 'uid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_move_id', 'title', '_ORIG_uid', '_ORIG_pid']));
}

/**
* @test
*/
public function resolveLiveRootLineForMovedPage()
{
$context = GeneralUtility::makeInstance(Context::class);
$context->setAspect('workspace', new WorkspaceAspect(0));
$subject = new RootlineUtility(1333, '', $context);
$result = $subject->get();

$expected = [
3 => [
'pid' => 1330,
'uid' => 1333,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'EN: Risk',
],
2 => [
'pid' => 1300,
'uid' => 1330,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'EN: Board Games',
],
1 => [
'pid' => 1000,
'uid' => 1300,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'EN: Products'
],
0 => [
'pid' => 0,
'uid' => 1000,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'ACME Global',
],
];
self::assertSame($expected, $this->filterExpectedValues($result, ['pid', 'uid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_move_id', 'title', '_ORIG_uid', '_ORIG_pid']));
}

/**
* @test
*/
public function resolveWorkspaceOverlaysOfMovedPage()
{
$context = GeneralUtility::makeInstance(Context::class);
$context->setAspect('workspace', new WorkspaceAspect(1));
$subject = new RootlineUtility(1333, '', $context);
$result = $subject->get();

$expected = [
3 => [
'pid' => 1320,
'uid' => 1333,
't3ver_oid' => 1333,
't3ver_wsid' => 1,
't3ver_state' => 4,
'title' => 'EN: Risk',
'_ORIG_pid' => 1330, // Pointing to the LIVE pid! WHY? All others point to the same PID!
'_ORIG_uid' => 10001,
],
2 => [
'pid' => 1300,
'uid' => 1320,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'EN: Card Games',
],
1 => [
'pid' => 1000,
'uid' => 1300,
't3ver_oid' => 0,
't3ver_wsid' => 0,
't3ver_state' => 0,
'title' => 'EN: Products'
],
0 => [
'pid' => 0,
'uid' => 1000,
't3ver_oid' => 1000,
't3ver_wsid' => 1,
't3ver_state' => 0,
'title' => 'ACME Global modified in Workspace 1',
'_ORIG_pid' => 0,
'_ORIG_uid' => 10000,
],
];
self::assertSame($expected, $this->filterExpectedValues($result, ['pid', 'uid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_move_id', 'title', '_ORIG_uid', '_ORIG_pid']));

// Now explicitly requesting the versioned ID, which holds the same result
$subject = new RootlineUtility(10001, '', $context);
$result = $subject->get();
self::assertSame($expected, $this->filterExpectedValues($result, ['pid', 'uid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_move_id', 'title', '_ORIG_uid', '_ORIG_pid']));

// Now explicitly requesting the move placeholder, which gets resolved to the live ID, which holds the same result
$subject = new RootlineUtility(10004, '', $context);
$result = $subject->get();
self::assertSame($expected, $this->filterExpectedValues($result, ['pid', 'uid', 't3ver_oid', 't3ver_wsid', 't3ver_state', 't3ver_move_id', 'title', '_ORIG_uid', '_ORIG_pid']));
}

/**
* @test
*/
public function rootlineFailsForDeletedParentPageInWorkspace()
{
self::expectException(PageNotFoundException::class);
self::expectExceptionCode(1343464101);
$context = GeneralUtility::makeInstance(Context::class);
$context->setAspect('workspace', new WorkspaceAspect(2));
$subject = new RootlineUtility(1310, '', $context);
$subject->get();
}

protected function filterExpectedValues(array $incomingData, array $fields): array
{
$result = [];
foreach ($incomingData as $pos => $values) {
array_walk($values, function (&$val) {
if (is_numeric($val)) {
$val = (int)$val;
}
});
$result[$pos] = array_filter($values, function ($fieldName) use ($fields) {
return in_array($fieldName, $fields, true);
}, ARRAY_FILTER_USE_KEY);
}
return $result;
}
}

0 comments on commit 9e524c1

Please sign in to comment.