Completed
Push — master ( a6ff96...8afff1 )
by Daniel
10:22
created

ChangeSetTest   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 398
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
dl 0
loc 398
rs 10
c 0
b 0
f 0
wmc 23
lcom 1
cbo 12

14 Methods

Rating   Name   Duplication   Size   Complexity  
A publishAllFixtures() 0 10 3
C assertChangeSetLooksLike() 0 32 8
A testAddObject() 0 12 1
A testRepeatedSyncIsNOP() 0 19 1
B testSync() 0 43 1
B testIsSynced() 0 28 1
B testCanPublish() 0 27 1
A testCanRevert() 0 3 1
A testCanEdit() 0 18 1
A testCanCreate() 0 9 1
A testCanDelete() 0 18 1
A testCanView() 0 18 1
B testPublish() 0 81 1
B testUnlinkDisassociated() 0 39 1
1
<?php
2
3
use SilverStripe\ORM\DataObject;
4
use SilverStripe\ORM\Versioning\ChangeSet;
5
use SilverStripe\ORM\Versioning\ChangeSetItem;
6
use SilverStripe\ORM\Versioning\Versioned;
7
use SilverStripe\Security\Permission;
8
use SilverStripe\Dev\TestOnly;
9
use SilverStripe\Dev\SapphireTest;
10
use SilverStripe\Control\Session;
11
12
13
/**
14
 * Provides a set of targettable permissions for tested models
15
 *
16
 * @mixin Versioned
17
 * @mixin DataObject
18
 */
19
trait ChangeSetTest_Permissions {
20
	public function canEdit($member = null) {
21
		return $this->can(__FUNCTION__, $member);
22
	}
23
24
	public function canDelete($member = null) {
25
		return $this->can(__FUNCTION__, $member);
26
	}
27
28
	public function canCreate($member = null, $context = array()) {
29
		return $this->can(__FUNCTION__, $member, $context);
30
	}
31
32
	public function canPublish($member = null, $context = array()) {
33
		return $this->can(__FUNCTION__, $member, $context);
34
	}
35
36
	public function canUnpublish($member = null, $context = array()) {
37
		return $this->can(__FUNCTION__, $member, $context);
38
	}
39
40
	public function can($perm, $member = null, $context = array()) {
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
41
		$perms = [
42
			"PERM_{$perm}",
43
			'CAN_ALL',
44
		];
45
		return Permission::checkMember($member, $perms);
46
	}
47
}
48
49
/**
50
 * @mixin Versioned
51
 */
52
class ChangeSetTest_Base extends DataObject implements TestOnly {
53
	use ChangeSetTest_Permissions;
54
55
	private static $db = [
56
		'Foo' => 'Int',
57
	];
58
59
	private static $has_many = [
60
		'Mids' => ChangeSetTest_Mid::class,
61
	];
62
63
	private static $owns = [
64
		'Mids',
65
	];
66
67
	private static $extensions = [
68
		"SilverStripe\\ORM\\Versioning\\Versioned",
69
	];
70
}
71
72
/**
73
 * @mixin Versioned
74
 */
75
class ChangeSetTest_Mid extends DataObject implements TestOnly {
76
	use ChangeSetTest_Permissions;
77
78
	private static $db = [
79
		'Bar' => 'Int',
80
	];
81
82
	private static $has_one = [
83
		'Base' => ChangeSetTest_Base::class,
84
		'End' => ChangeSetTest_End::class,
85
	];
86
87
	private static $owns = [
88
		'End',
89
	];
90
91
	private static $extensions = [
92
		"SilverStripe\\ORM\\Versioning\\Versioned",
93
	];
94
}
95
96
/**
97
 * @mixin Versioned
98
 */
99
class ChangeSetTest_End extends DataObject implements TestOnly {
100
	use ChangeSetTest_Permissions;
101
102
	private static $db = [
103
		'Baz' => 'Int',
104
	];
105
106
	private static $extensions = [
107
		"SilverStripe\\ORM\\Versioning\\Versioned",
108
	];
109
}
110
111
/**
112
 * @mixin Versioned
113
 */
114
class ChangeSetTest_EndChild extends ChangeSetTest_End implements TestOnly {
115
116
	private static $db = [
117
		'Qux' => 'Int',
118
	];
119
}
120
121
/**
122
 * Test {@see ChangeSet} and {@see ChangeSetItem} models
123
 */
124
class ChangeSetTest extends SapphireTest {
125
126
	protected static $fixture_file = 'ChangeSetTest.yml';
127
128
	protected $extraDataObjects = [
129
		ChangeSetTest_Base::class,
130
		ChangeSetTest_Mid::class,
131
		ChangeSetTest_End::class,
132
		ChangeSetTest_EndChild::class,
133
	];
134
135
	/**
136
	 * Automatically publish all objects
137
	 */
138
	protected function publishAllFixtures() {
139
		$this->logInWithPermission('ADMIN');
140
		foreach($this->fixtureFactory->getFixtures() as $class => $fixtures) {
141
			foreach ($fixtures as $handle => $id) {
142
				/** @var Versioned|DataObject $object */
143
				$object = $this->objFromFixture($class, $handle);
144
				$object->publishSingle();
0 ignored issues
show
Bug introduced by
The method publishSingle does only exist in SilverStripe\ORM\Versioning\Versioned, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
145
			}
146
		}
147
	}
148
149
	/**
150
	 * Check that the changeset includes the given items
151
	 *
152
	 * @param ChangeSet $cs
153
	 * @param array $match Array of object fixture keys with change type values
154
	 */
155
	protected function assertChangeSetLooksLike($cs, $match) {
156
		$items = $cs->Changes()->toArray();
157
158
		foreach($match as $key => $mode) {
159
			list($class, $identifier) = explode('.', $key);
160
			$object = $this->objFromFixture($class, $identifier);
161
162
			foreach($items as $i => $item) {
163
				if ( $item->ObjectClass == $object->baseClass()
164
					&& $item->ObjectID == $object->ID
165
					&& $item->Added == $mode
166
				) {
167
					unset($items[$i]);
168
					continue 2;
169
				}
170
			}
171
172
			throw new PHPUnit_Framework_ExpectationFailedException(
173
				'Change set didn\'t include expected item',
174
				new \SebastianBergmann\Comparator\ComparisonFailure(array('Class' => $class, 'ID' => $object->ID, 'Added' => $mode), null, "$key => $mode", '')
175
			);
176
		}
177
178
		if (count($items)) {
179
			$extra = [];
180
			foreach ($items as $item) $extra[] = ['Class' => $item->ObjectClass, 'ID' => $item->ObjectID, 'Added' => $item->Added, 'ChangeType' => $item->getChangeType()];
181
			throw new PHPUnit_Framework_ExpectationFailedException(
182
				'Change set included items that weren\'t expected',
183
				new \SebastianBergmann\Comparator\ComparisonFailure(array(), $extra, '', print_r($extra, true))
184
			);
185
		}
186
	}
187
188
	public function testAddObject() {
189
		$cs = new ChangeSet();
190
		$cs->write();
191
192
		$cs->addObject($this->objFromFixture(ChangeSetTest_End::class, 'end1'));
0 ignored issues
show
Bug introduced by
It seems like $this->objFromFixture(\C...est_End::class, 'end1') can be null; however, addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
193
		$cs->addObject($this->objFromFixture(ChangeSetTest_EndChild::class, 'endchild1'));
0 ignored issues
show
Bug introduced by
It seems like $this->objFromFixture(\C...ld::class, 'endchild1') can be null; however, addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
194
195
		$this->assertChangeSetLooksLike($cs, [
196
			'ChangeSetTest_End.end1' => ChangeSetItem::EXPLICITLY,
197
			'ChangeSetTest_EndChild.endchild1' => ChangeSetItem::EXPLICITLY
198
		]);
199
	}
200
201
	public function testRepeatedSyncIsNOP() {
202
		$this->publishAllFixtures();
203
204
		$cs = new ChangeSet();
205
		$cs->write();
206
207
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
208
		$cs->addObject($base);
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 207 can be null; however, SilverStripe\ORM\Versioning\ChangeSet::addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
209
210
		$cs->sync();
211
		$this->assertChangeSetLooksLike($cs, [
212
			'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
213
		]);
214
215
		$cs->sync();
216
		$this->assertChangeSetLooksLike($cs, [
217
			'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
218
		]);
219
	}
220
221
	public function testSync() {
222
		$this->publishAllFixtures();
223
224
		$cs = new ChangeSet();
225
		$cs->write();
226
227
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
228
229
		$cs->addObject($base);
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 227 can be null; however, SilverStripe\ORM\Versioning\ChangeSet::addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
230
		$cs->sync();
231
232
		$this->assertChangeSetLooksLike($cs, [
233
			'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
234
		]);
235
236
		$end = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
237
		$end->Baz = 3;
0 ignored issues
show
Documentation introduced by
The property Baz does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
238
		$end->write();
239
240
		$cs->sync();
241
242
		$this->assertChangeSetLooksLike($cs, [
243
			'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY,
244
			'ChangeSetTest_End.end1' => ChangeSetItem::IMPLICITLY
245
		]);
246
247
		$baseItem = ChangeSetItem::get_for_object($base)->first();
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 227 can be null; however, SilverStripe\ORM\Version...tItem::get_for_object() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
248
		$endItem = ChangeSetItem::get_for_object($end)->first();
249
250
		$this->assertEquals(
251
			[$baseItem->ID],
252
			$endItem->ReferencedBy()->column("ID")
253
		);
254
255
		$this->assertDOSEquals([
256
			[
257
				'Added' => ChangeSetItem::EXPLICITLY,
258
				'ObjectClass' => ChangeSetTest_Base::class,
259
				'ObjectID' => $base->ID,
260
				'ChangeSetID' => $cs->ID
261
			]
262
		], $endItem->ReferencedBy());
263
	}
264
265
	/**
266
	 * Test that sync includes implicit items
267
	 */
268
	public function testIsSynced() {
269
		$this->publishAllFixtures();
270
271
		$cs = new ChangeSet();
272
		$cs->write();
273
274
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
275
		$cs->addObject($base);
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 274 can be null; however, SilverStripe\ORM\Versioning\ChangeSet::addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
276
277
		$cs->sync();
278
		$this->assertChangeSetLooksLike($cs, [
279
			'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
280
		]);
281
		$this->assertTrue($cs->isSynced());
282
283
		$end = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
284
		$end->Baz = 3;
0 ignored issues
show
Documentation introduced by
The property Baz does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
285
		$end->write();
286
		$this->assertFalse($cs->isSynced());
287
288
		$cs->sync();
289
290
		$this->assertChangeSetLooksLike($cs, [
291
			'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY,
292
			'ChangeSetTest_End.end1' => ChangeSetItem::IMPLICITLY
293
		]);
294
		$this->assertTrue($cs->isSynced());
295
	}
296
297
	public function testCanPublish() {
298
		// Create changeset containing all items (unpublished)
299
		$this->logInWithPermission('ADMIN');
300
		$changeSet = new ChangeSet();
301
		$changeSet->write();
302
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
303
		$changeSet->addObject($base);
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 302 can be null; however, SilverStripe\ORM\Versioning\ChangeSet::addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
304
		$changeSet->sync();
305
		$this->assertEquals(5, $changeSet->Changes()->count());
306
307
		// Test un-authenticated user cannot publish
308
		Session::clear("loggedInAs");
309
		$this->assertFalse($changeSet->canPublish());
310
311
		// User with only one of the necessary permissions cannot publish
312
		$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
313
		$this->assertFalse($changeSet->canPublish());
314
		$this->logInWithPermission('PERM_canPublish');
315
		$this->assertFalse($changeSet->canPublish());
316
317
		// Test user with the necessary minimum permissions can login
318
		$this->logInWithPermission([
319
			'CMS_ACCESS_CampaignAdmin',
320
			'PERM_canPublish'
321
		]);
322
		$this->assertTrue($changeSet->canPublish());
323
	}
324
325
	public function testCanRevert() {
326
		$this->markTestSkipped("Requires ChangeSet::revert to be implemented first");
327
	}
328
329
	public function testCanEdit() {
330
		// Create changeset containing all items (unpublished)
331
		$this->logInWithPermission('ADMIN');
332
		$changeSet = new ChangeSet();
333
		$changeSet->write();
334
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
335
		$changeSet->addObject($base);
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 334 can be null; however, SilverStripe\ORM\Versioning\ChangeSet::addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
336
		$changeSet->sync();
337
		$this->assertEquals(5, $changeSet->Changes()->count());
338
339
		// Check canEdit
340
		Session::clear("loggedInAs");
341
		$this->assertFalse($changeSet->canEdit());
342
		$this->logInWithPermission('SomeWrongPermission');
343
		$this->assertFalse($changeSet->canEdit());
344
		$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
345
		$this->assertTrue($changeSet->canEdit());
346
	}
347
348
	public function testCanCreate() {
349
		// Check canCreate
350
		Session::clear("loggedInAs");
351
		$this->assertFalse(ChangeSet::singleton()->canCreate());
352
		$this->logInWithPermission('SomeWrongPermission');
353
		$this->assertFalse(ChangeSet::singleton()->canCreate());
354
		$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
355
		$this->assertTrue(ChangeSet::singleton()->canCreate());
356
	}
357
358
	public function testCanDelete() {
359
		// Create changeset containing all items (unpublished)
360
		$this->logInWithPermission('ADMIN');
361
		$changeSet = new ChangeSet();
362
		$changeSet->write();
363
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
364
		$changeSet->addObject($base);
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 363 can be null; however, SilverStripe\ORM\Versioning\ChangeSet::addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
365
		$changeSet->sync();
366
		$this->assertEquals(5, $changeSet->Changes()->count());
367
368
		// Check canDelete
369
		Session::clear("loggedInAs");
370
		$this->assertFalse($changeSet->canDelete());
371
		$this->logInWithPermission('SomeWrongPermission');
372
		$this->assertFalse($changeSet->canDelete());
373
		$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
374
		$this->assertTrue($changeSet->canDelete());
375
	}
376
377
	public function testCanView() {
378
		// Create changeset containing all items (unpublished)
379
		$this->logInWithPermission('ADMIN');
380
		$changeSet = new ChangeSet();
381
		$changeSet->write();
382
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
383
		$changeSet->addObject($base);
0 ignored issues
show
Bug introduced by
It seems like $base defined by $this->objFromFixture(\C...st_Base::class, 'base') on line 382 can be null; however, SilverStripe\ORM\Versioning\ChangeSet::addObject() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
384
		$changeSet->sync();
385
		$this->assertEquals(5, $changeSet->Changes()->count());
386
387
		// Check canView
388
		Session::clear("loggedInAs");
389
		$this->assertFalse($changeSet->canView());
390
		$this->logInWithPermission('SomeWrongPermission');
391
		$this->assertFalse($changeSet->canView());
392
		$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
393
		$this->assertTrue($changeSet->canView());
394
	}
395
396
	public function testPublish() {
397
		$this->publishAllFixtures();
398
399
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
400
		$baseID = $base->ID;
401
		$baseBefore = $base->Version;
0 ignored issues
show
Documentation introduced by
The property Version does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
402
		$end1 = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
403
		$end1ID = $end1->ID;
404
		$end1Before = $end1->Version;
0 ignored issues
show
Documentation introduced by
The property Version does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
405
406
		// Create a new changest
407
		$changeset = new ChangeSet();
408
		$changeset->write();
409
		$changeset->addObject($base);
410
		$changeset->addObject($end1);
411
412
		// Make a lot of changes
413
		// - ChangeSetTest_Base.base modified
414
		// - ChangeSetTest_End.end1 deleted
415
		// - new ChangeSetTest_Mid added
416
		$base->Foo = 343;
0 ignored issues
show
Documentation introduced by
The property Foo does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
417
		$base->write();
418
		$baseAfter = $base->Version;
0 ignored issues
show
Documentation introduced by
The property Version does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
419
		$midNew = new ChangeSetTest_Mid();
420
		$midNew->Bar = 39;
0 ignored issues
show
Documentation introduced by
The property Bar does not exist on object<ChangeSetTest_Mid>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
421
		$midNew->write();
422
		$midNewID = $midNew->ID;
423
		$midNewAfter = $midNew->Version;
0 ignored issues
show
Documentation introduced by
The property Version does not exist on object<ChangeSetTest_Mid>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
424
		$end1->delete();
425
426
		$changeset->addObject($midNew);
427
428
		// Publish
429
		$this->logInWithPermission('ADMIN');
430
		$this->assertTrue($changeset->canPublish());
431
		$this->assertTrue($changeset->isSynced());
432
		$changeset->publish();
433
		$this->assertEquals(ChangeSet::STATE_PUBLISHED, $changeset->State);
434
435
		// Check each item has the correct before/after version applied
436
		$baseChange = $changeset->Changes()->filter([
437
			'ObjectClass' => ChangeSetTest_Base::class,
438
			'ObjectID' => $baseID,
439
		])->first();
440
		$this->assertEquals((int)$baseBefore, (int)$baseChange->VersionBefore);
441
		$this->assertEquals((int)$baseAfter, (int)$baseChange->VersionAfter);
442
		$this->assertEquals((int)$baseChange->VersionBefore + 1, (int)$baseChange->VersionAfter);
443
		$this->assertEquals(
444
			(int)$baseChange->VersionAfter,
445
			(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_Base::class, Versioned::LIVE, $baseID)
446
		);
447
448
		$end1Change = $changeset->Changes()->filter([
449
			'ObjectClass' => ChangeSetTest_End::class,
450
			'ObjectID' => $end1ID,
451
		])->first();
452
		$this->assertEquals((int)$end1Before, (int)$end1Change->VersionBefore);
453
		$this->assertEquals(0, (int)$end1Change->VersionAfter);
454
		$this->assertEquals(
455
			0,
456
			(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_End::class, Versioned::LIVE, $end1ID)
457
		);
458
459
		$midNewChange = $changeset->Changes()->filter([
460
			'ObjectClass' => ChangeSetTest_Mid::class,
461
			'ObjectID' => $midNewID,
462
		])->first();
463
		$this->assertEquals(0, (int)$midNewChange->VersionBefore);
464
		$this->assertEquals((int)$midNewAfter, (int)$midNewChange->VersionAfter);
465
		$this->assertEquals(
466
			(int)$midNewAfter,
467
			(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_Mid::class, Versioned::LIVE, $midNewID)
468
		);
469
470
		// Test trying to re-publish is blocked
471
		$this->setExpectedException(
472
			'BadMethodCallException',
473
			"ChangeSet can't be published if it has been already published or reverted."
474
		);
475
		$changeset->publish();
476
	}
477
478
	/**
479
	 * Ensure that related objects are disassociated on live
480
	 */
481
	public function testUnlinkDisassociated() {
482
		$this->publishAllFixtures();
483
		/** @var ChangeSetTest_Base $base */
484
		$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
485
		/** @var ChangeSetTest_Mid $mid1 $mid2 */
486
		$mid1 = $this->objFromFixture(ChangeSetTest_Mid::class, 'mid1');
487
		$mid2 = $this->objFromFixture(ChangeSetTest_Mid::class, 'mid2');
488
489
		// Remove mid1 from stage
490
		$this->assertEquals($base->ID, $mid1->BaseID);
491
		$this->assertEquals($base->ID, $mid2->BaseID);
492
		$mid1->deleteFromStage(Versioned::DRAFT);
493
494
		// Publishing recursively should unlinkd this object
495
		$changeset = new ChangeSet();
496
		$changeset->write();
497
		$changeset->addObject($base);
498
499
		// Assert changeset only contains root object
500
		$this->assertChangeSetLooksLike($changeset, [
501
			'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
502
		]);
503
504
		$changeset->publish();
505
506
		// mid1 on live exists, but has BaseID set to zero
507
		$mid1Live = Versioned::get_by_stage(ChangeSetTest_Mid::class, Versioned::LIVE)
508
			->byID($mid1->ID);
509
		$this->assertNotNull($mid1Live);
510
		$this->assertEquals($mid1->ID, $mid1Live->ID);
511
		$this->assertEquals(0, $mid1Live->BaseID);
512
513
		// mid2 on live exists and retains BaseID
514
		$mid2Live = Versioned::get_by_stage(ChangeSetTest_Mid::class, Versioned::LIVE)
515
			->byID($mid2->ID);
516
		$this->assertNotNull($mid2Live);
517
		$this->assertEquals($mid2->ID, $mid2Live->ID);
518
		$this->assertEquals($base->ID, $mid2Live->BaseID);
519
	}
520
521
}
522