silverstripe-framework/tests/model/ChangeSetTest.php
Damian Mooyman 316ac86036
API Writes to versioned dataobjects always writes to stage even when written on live
API Remove "Archive" actions
API "Delete" actions for pages now archives records
BUG Fix batch actions failing on certain controllers
Fixes #6059
2016-10-21 13:16:32 +13:00

524 lines
14 KiB
PHP

<?php
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\ChangeSet;
use SilverStripe\ORM\Versioning\ChangeSetItem;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Security\Permission;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Session;
/**
* Provides a set of targettable permissions for tested models
*
* @mixin Versioned
* @mixin DataObject
*/
trait ChangeSetTest_Permissions {
public function canEdit($member = null) {
return $this->can(__FUNCTION__, $member);
}
public function canDelete($member = null) {
return $this->can(__FUNCTION__, $member);
}
public function canCreate($member = null, $context = array()) {
return $this->can(__FUNCTION__, $member, $context);
}
public function canPublish($member = null, $context = array()) {
return $this->can(__FUNCTION__, $member, $context);
}
public function canUnpublish($member = null, $context = array()) {
return $this->can(__FUNCTION__, $member, $context);
}
public function can($perm, $member = null, $context = array()) {
$perms = [
"PERM_{$perm}",
'CAN_ALL',
];
return Permission::checkMember($member, $perms);
}
}
/**
* @mixin Versioned
*/
class ChangeSetTest_Base extends DataObject implements TestOnly {
use ChangeSetTest_Permissions;
private static $db = [
'Foo' => 'Int',
];
private static $has_many = [
'Mids' => ChangeSetTest_Mid::class,
];
private static $owns = [
'Mids',
];
private static $extensions = [
"SilverStripe\\ORM\\Versioning\\Versioned",
];
}
/**
* @mixin Versioned
*/
class ChangeSetTest_Mid extends DataObject implements TestOnly {
use ChangeSetTest_Permissions;
private static $db = [
'Bar' => 'Int',
];
private static $has_one = [
'Base' => ChangeSetTest_Base::class,
'End' => ChangeSetTest_End::class,
];
private static $owns = [
'End',
];
private static $extensions = [
"SilverStripe\\ORM\\Versioning\\Versioned",
];
}
/**
* @mixin Versioned
*/
class ChangeSetTest_End extends DataObject implements TestOnly {
use ChangeSetTest_Permissions;
private static $db = [
'Baz' => 'Int',
];
private static $extensions = [
"SilverStripe\\ORM\\Versioning\\Versioned",
];
}
/**
* @mixin Versioned
*/
class ChangeSetTest_EndChild extends ChangeSetTest_End implements TestOnly {
private static $db = [
'Qux' => 'Int',
];
}
/**
* Test {@see ChangeSet} and {@see ChangeSetItem} models
*/
class ChangeSetTest extends SapphireTest {
protected static $fixture_file = 'ChangeSetTest.yml';
protected $extraDataObjects = [
ChangeSetTest_Base::class,
ChangeSetTest_Mid::class,
ChangeSetTest_End::class,
ChangeSetTest_EndChild::class,
];
/**
* Automatically publish all objects
*/
protected function publishAllFixtures() {
$this->logInWithPermission('ADMIN');
foreach($this->fixtureFactory->getFixtures() as $class => $fixtures) {
foreach ($fixtures as $handle => $id) {
/** @var Versioned|DataObject $object */
$object = $this->objFromFixture($class, $handle);
$object->publishSingle();
}
}
}
/**
* Check that the changeset includes the given items
*
* @param ChangeSet $cs
* @param array $match Array of object fixture keys with change type values
*/
protected function assertChangeSetLooksLike($cs, $match) {
$items = $cs->Changes()->toArray();
foreach($match as $key => $mode) {
list($class, $identifier) = explode('.', $key);
$object = $this->objFromFixture($class, $identifier);
foreach($items as $i => $item) {
if ( $item->ObjectClass == $object->baseClass()
&& $item->ObjectID == $object->ID
&& $item->Added == $mode
) {
unset($items[$i]);
continue 2;
}
}
throw new PHPUnit_Framework_ExpectationFailedException(
'Change set didn\'t include expected item',
new \SebastianBergmann\Comparator\ComparisonFailure(array('Class' => $class, 'ID' => $object->ID, 'Added' => $mode), null, "$key => $mode", '')
);
}
if (count($items)) {
$extra = [];
foreach ($items as $item) $extra[] = ['Class' => $item->ObjectClass, 'ID' => $item->ObjectID, 'Added' => $item->Added, 'ChangeType' => $item->getChangeType()];
throw new PHPUnit_Framework_ExpectationFailedException(
'Change set included items that weren\'t expected',
new \SebastianBergmann\Comparator\ComparisonFailure(array(), $extra, '', print_r($extra, true))
);
}
}
public function testAddObject() {
$cs = new ChangeSet();
$cs->write();
$cs->addObject($this->objFromFixture(ChangeSetTest_End::class, 'end1'));
$cs->addObject($this->objFromFixture(ChangeSetTest_EndChild::class, 'endchild1'));
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_End.end1' => ChangeSetItem::EXPLICITLY,
'ChangeSetTest_EndChild.endchild1' => ChangeSetItem::EXPLICITLY
]);
}
public function testRepeatedSyncIsNOP() {
$this->publishAllFixtures();
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$cs->addObject($base);
$cs->sync();
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
]);
$cs->sync();
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
]);
}
public function testSync() {
$this->publishAllFixtures();
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$cs->addObject($base);
$cs->sync();
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
]);
$end = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
$end->Baz = 3;
$end->write();
$cs->sync();
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY,
'ChangeSetTest_End.end1' => ChangeSetItem::IMPLICITLY
]);
$baseItem = ChangeSetItem::get_for_object($base)->first();
$endItem = ChangeSetItem::get_for_object($end)->first();
$this->assertEquals(
[$baseItem->ID],
$endItem->ReferencedBy()->column("ID")
);
$this->assertDOSEquals([
[
'Added' => ChangeSetItem::EXPLICITLY,
'ObjectClass' => ChangeSetTest_Base::class,
'ObjectID' => $base->ID,
'ChangeSetID' => $cs->ID
]
], $endItem->ReferencedBy());
}
/**
* Test that sync includes implicit items
*/
public function testIsSynced() {
$this->publishAllFixtures();
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$cs->addObject($base);
$cs->sync();
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
]);
$this->assertTrue($cs->isSynced());
$end = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
$end->Baz = 3;
$end->write();
$this->assertFalse($cs->isSynced());
$cs->sync();
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY,
'ChangeSetTest_End.end1' => ChangeSetItem::IMPLICITLY
]);
$this->assertTrue($cs->isSynced());
}
public function testCanPublish() {
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Test un-authenticated user cannot publish
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canPublish());
// campaign admin only permission doesn't grant publishing rights
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertFalse($changeSet->canPublish());
// With model publish permissions only publish is allowed
$this->logInWithPermission('PERM_canPublish');
$this->assertTrue($changeSet->canPublish());
// Test user with the necessary minimum permissions can login
$this->logInWithPermission([
'CMS_ACCESS_CampaignAdmin',
'PERM_canPublish'
]);
$this->assertTrue($changeSet->canPublish());
}
public function testCanRevert() {
$this->markTestSkipped("Requires ChangeSet::revert to be implemented first");
}
public function testCanEdit() {
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Check canEdit
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canEdit());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse($changeSet->canEdit());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue($changeSet->canEdit());
}
public function testCanCreate() {
// Check canCreate
Session::clear("loggedInAs");
$this->assertFalse(ChangeSet::singleton()->canCreate());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse(ChangeSet::singleton()->canCreate());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue(ChangeSet::singleton()->canCreate());
}
public function testCanDelete() {
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Check canDelete
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canDelete());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse($changeSet->canDelete());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue($changeSet->canDelete());
}
public function testCanView() {
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Check canView
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canView());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse($changeSet->canView());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue($changeSet->canView());
}
public function testPublish() {
$this->publishAllFixtures();
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$baseID = $base->ID;
$baseBefore = $base->Version;
$end1 = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
$end1ID = $end1->ID;
$end1Before = $end1->Version;
// Create a new changest
$changeset = new ChangeSet();
$changeset->write();
$changeset->addObject($base);
$changeset->addObject($end1);
// Make a lot of changes
// - ChangeSetTest_Base.base modified
// - ChangeSetTest_End.end1 deleted
// - new ChangeSetTest_Mid added
$base->Foo = 343;
$base->write();
$baseAfter = $base->Version;
$midNew = new ChangeSetTest_Mid();
$midNew->Bar = 39;
$midNew->write();
$midNewID = $midNew->ID;
$midNewAfter = $midNew->Version;
$end1->delete();
$changeset->addObject($midNew);
// Publish
$this->logInWithPermission('ADMIN');
$this->assertTrue($changeset->canPublish());
$this->assertTrue($changeset->isSynced());
$changeset->publish();
$this->assertEquals(ChangeSet::STATE_PUBLISHED, $changeset->State);
// Check each item has the correct before/after version applied
$baseChange = $changeset->Changes()->filter([
'ObjectClass' => ChangeSetTest_Base::class,
'ObjectID' => $baseID,
])->first();
$this->assertEquals((int)$baseBefore, (int)$baseChange->VersionBefore);
$this->assertEquals((int)$baseAfter, (int)$baseChange->VersionAfter);
$this->assertEquals((int)$baseChange->VersionBefore + 1, (int)$baseChange->VersionAfter);
$this->assertEquals(
(int)$baseChange->VersionAfter,
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_Base::class, Versioned::LIVE, $baseID)
);
$end1Change = $changeset->Changes()->filter([
'ObjectClass' => ChangeSetTest_End::class,
'ObjectID' => $end1ID,
])->first();
$this->assertEquals((int)$end1Before, (int)$end1Change->VersionBefore);
$this->assertEquals(0, (int)$end1Change->VersionAfter);
$this->assertEquals(
0,
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_End::class, Versioned::LIVE, $end1ID)
);
$midNewChange = $changeset->Changes()->filter([
'ObjectClass' => ChangeSetTest_Mid::class,
'ObjectID' => $midNewID,
])->first();
$this->assertEquals(0, (int)$midNewChange->VersionBefore);
$this->assertEquals((int)$midNewAfter, (int)$midNewChange->VersionAfter);
$this->assertEquals(
(int)$midNewAfter,
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_Mid::class, Versioned::LIVE, $midNewID)
);
// Test trying to re-publish is blocked
$this->setExpectedException(
'BadMethodCallException',
"ChangeSet can't be published if it has been already published or reverted."
);
$changeset->publish();
}
/**
* Ensure that related objects are disassociated on live
*/
public function testUnlinkDisassociated() {
$this->publishAllFixtures();
/** @var ChangeSetTest_Base $base */
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
/** @var ChangeSetTest_Mid $mid1 $mid2 */
$mid1 = $this->objFromFixture(ChangeSetTest_Mid::class, 'mid1');
$mid2 = $this->objFromFixture(ChangeSetTest_Mid::class, 'mid2');
// Remove mid1 from stage
$this->assertEquals($base->ID, $mid1->BaseID);
$this->assertEquals($base->ID, $mid2->BaseID);
$mid1->deleteFromStage(Versioned::DRAFT);
// Publishing recursively should unlinkd this object
$changeset = new ChangeSet();
$changeset->write();
$changeset->addObject($base);
// Assert changeset only contains root object
$this->assertChangeSetLooksLike($changeset, [
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
]);
$changeset->publish();
// mid1 on live exists, but has BaseID set to zero
$mid1Live = Versioned::get_by_stage(ChangeSetTest_Mid::class, Versioned::LIVE)
->byID($mid1->ID);
$this->assertNotNull($mid1Live);
$this->assertEquals($mid1->ID, $mid1Live->ID);
$this->assertEquals(0, $mid1Live->BaseID);
// mid2 on live exists and retains BaseID
$mid2Live = Versioned::get_by_stage(ChangeSetTest_Mid::class, Versioned::LIVE)
->byID($mid2->ID);
$this->assertNotNull($mid2Live);
$this->assertEquals($mid2->ID, $mid2Live->ID);
$this->assertEquals($base->ID, $mid2Live->BaseID);
}
}