Completed
Push — 3.2 ( 55e1b6...66b3a6 )
by Damian
12:25
created

GroupTest::testUnsavedGroups()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 13
rs 9.4286
cc 1
eloc 8
nc 1
nop 0
1
<?php
2
/**
3
 * @package framework
4
 * @subpackage tests
5
 */
6
class GroupTest extends FunctionalTest {
7
8
	protected static $fixture_file = 'GroupTest.yml';
9
10
	public function testGroupCodeDefaultsToTitle() {
11
		$g1 = new Group();
12
		$g1->Title = "My Title";
13
		$g1->write();
14
		$this->assertEquals('my-title', $g1->Code, 'Custom title gets converted to code if none exists already');
15
16
		$g2 = new Group();
17
		$g2->Title = "My Title";
18
		$g2->Code = "my-code";
19
		$g2->write();
20
		$this->assertEquals('my-code', $g2->Code, 'Custom attributes are not overwritten by Title field');
21
22
		$g3 = new Group();
23
		$g3->Title = _t('SecurityAdmin.NEWGROUP',"New Group");
24
		$g3->write();
25
		$this->assertNull($g3->Code, 'Default title doesnt trigger attribute setting');
26
	}
27
28
	public function testMemberGroupRelationForm() {
29
		Session::set('loggedInAs', $this->idFromFixture('GroupTest_Member', 'admin'));
30
31
		$adminGroup = $this->objFromFixture('Group', 'admingroup');
32
		$parentGroup = $this->objFromFixture('Group', 'parentgroup');
33
		$childGroup = $this->objFromFixture('Group', 'childgroup');
34
35
		// Test single group relation through checkboxsetfield
36
		$form = new GroupTest_MemberForm($this, 'Form');
37
		$member = $this->objFromFixture('GroupTest_Member', 'admin');
38
		$form->loadDataFrom($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture('G...pTest_Member', 'admin') on line 37 can be null; however, Form::loadDataFrom() 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...
39
		$checkboxSetField = $form->Fields()->fieldByName('Groups');
40
		$checkboxSetField->setValue(array(
41
			$adminGroup->ID => $adminGroup->ID, // keep existing relation
42
			$parentGroup->ID => $parentGroup->ID, // add new relation
43
		));
44
		$form->saveInto($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture('G...pTest_Member', 'admin') on line 37 can be null; however, Form::saveInto() 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...
45
		$updatedGroups = $member->Groups();
46
47
		$this->assertEquals(2, count($updatedGroups->column()),
48
			"Adding a toplevel group works"
49
		);
50
		$this->assertContains($adminGroup->ID, $updatedGroups->column('ID'));
51
		$this->assertContains($parentGroup->ID, $updatedGroups->column('ID'));
52
53
		// Test unsetting relationship
54
		$form->loadDataFrom($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture('G...pTest_Member', 'admin') on line 37 can be null; however, Form::loadDataFrom() 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...
55
		$checkboxSetField = $form->Fields()->fieldByName('Groups');
56
		$checkboxSetField->setValue(array(
57
			$adminGroup->ID => $adminGroup->ID, // keep existing relation
58
			//$parentGroup->ID => $parentGroup->ID, // remove previously set relation
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
59
		));
60
		$form->saveInto($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture('G...pTest_Member', 'admin') on line 37 can be null; however, Form::saveInto() 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...
61
		$member->flushCache();
62
		$updatedGroups = $member->Groups();
63
		$this->assertEquals(1, count($updatedGroups->column()),
64
			"Removing a previously added toplevel group works"
65
		);
66
		$this->assertContains($adminGroup->ID, $updatedGroups->column('ID'));
67
68
		// Test adding child group
69
70
	}
71
72
	public function testUnsavedGroups() {
73
		$member = $this->objFromFixture('GroupTest_Member', 'admin');
74
		$group = new Group();
75
76
		// Can save user to unsaved group
77
		$group->Members()->add($member);
78
		$this->assertEquals(array($member->ID), array_values($group->Members()->getIDList()));
79
80
		// Persists after writing to DB
81
		$group->write();
82
		$group = Group::get()->byID($group->ID);
83
		$this->assertEquals(array($member->ID), array_values($group->Members()->getIDList()));
84
	}
85
86
	public function testCollateAncestorIDs() {
87
		$parentGroup = $this->objFromFixture('Group', 'parentgroup');
88
		$childGroup = $this->objFromFixture('Group', 'childgroup');
89
		$orphanGroup = new Group();
90
		$orphanGroup->ParentID = 99999;
91
		$orphanGroup->write();
92
93
		$this->assertEquals(1, count($parentGroup->collateAncestorIDs()),
94
			'Root node only contains itself'
95
		);
96
		$this->assertContains($parentGroup->ID, $parentGroup->collateAncestorIDs());
97
98
		$this->assertEquals(2, count($childGroup->collateAncestorIDs()),
99
			'Contains parent nodes, with child node first'
100
		);
101
		$this->assertContains($parentGroup->ID, $childGroup->collateAncestorIDs());
102
		$this->assertContains($childGroup->ID, $childGroup->collateAncestorIDs());
103
104
		$this->assertEquals(1, count($orphanGroup->collateAncestorIDs()),
105
			'Orphaned nodes dont contain invalid parent IDs'
106
		);
107
		$this->assertContains($orphanGroup->ID, $orphanGroup->collateAncestorIDs());
108
	}
109
110
	public function testDelete() {
111
		$group = $this->objFromFixture('Group', 'parentgroup');
112
		$groupID = $group->ID;
113
		$childGroupID = $this->idFromFixture('Group', 'childgroup');
114
		$group->delete();
115
116
		$this->assertEquals(0, DataObject::get('Group', "\"ID\" = {$groupID}")->Count(),
117
			'Group is removed');
118
		$this->assertEquals(0, DataObject::get('Permission', "\"GroupID\" = {$groupID}")->Count(),
119
			'Permissions removed along with the group');
120
		$this->assertEquals(0, DataObject::get('Group', "\"ParentID\" = {$groupID}")->Count(),
121
			'Child groups are removed');
122
		$this->assertEquals(0, DataObject::get('Group', "\"ParentID\" = {$childGroupID}")->Count(),
123
			'Grandchild groups are removed');
124
	}
125
126
	public function testValidatesPrivilegeLevelOfParent() {
127
		$nonAdminUser = $this->objFromFixture('GroupTest_Member', 'childgroupuser');
128
		$adminUser = $this->objFromFixture('GroupTest_Member', 'admin');
129
		$nonAdminGroup = $this->objFromFixture('Group', 'childgroup');
130
		$adminGroup = $this->objFromFixture('Group', 'admingroup');
131
132
		$nonAdminValidateMethod = new ReflectionMethod($nonAdminGroup, 'validate');
133
		$nonAdminValidateMethod->setAccessible(true);
134
135
		// Making admin group parent of a non-admin group, effectively expanding is privileges
136
		$nonAdminGroup->ParentID = $adminGroup->ID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<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...
137
138
		$this->logInWithPermission('APPLY_ROLES');
139
		$result = $nonAdminValidateMethod->invoke($nonAdminGroup);
140
		$this->assertFalse(
141
			$result->valid(),
142
			'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions'
143
		);
144
145
		$this->logInWithPermission('ADMIN');
146
		$result = $nonAdminValidateMethod->invoke($nonAdminGroup);
147
		$this->assertTrue(
148
			$result->valid(),
149
			'Members with ADMIN can assign parent groups with direct ADMIN permissions'
150
		);
151
		$nonAdminGroup->write();
152
		$newlyAdminGroup = $nonAdminGroup;
153
154
		$this->logInWithPermission('ADMIN');
155
		$inheritedAdminGroup = $this->objFromFixture('Group', 'group1');
156
		$inheritedAdminMethod = new ReflectionMethod($inheritedAdminGroup, 'validate');
157
		$inheritedAdminMethod->setAccessible(true);
158
		$inheritedAdminGroup->ParentID = $adminGroup->ID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<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...
159
		$inheritedAdminGroup->write(); // only works with ADMIN login
160
161
		$this->logInWithPermission('APPLY_ROLES');
162
		$result = $inheritedAdminMethod->invoke($nonAdminGroup);
163
		$this->assertFalse(
164
			$result->valid(),
165
			'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission'
166
		);
167
	}
168
169
}
170
171
class GroupTest_Member extends Member implements TestOnly {
172
173
	public function getCMSFields() {
174
		$groups = DataObject::get('Group');
175
		$groupsMap = ($groups) ? $groups->map() : false;
176
		$fields = new FieldList(
177
			new HiddenField('ID', 'ID'),
178
			new CheckboxSetField(
179
				'Groups',
180
				'Groups',
181
				$groupsMap
0 ignored issues
show
Security Bug introduced by
It seems like $groupsMap defined by $groups ? $groups->map() : false on line 175 can also be of type false; however, DropdownField::__construct() does only seem to accept array|object<ArrayAccess>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
182
			)
183
		);
184
185
		return $fields;
186
	}
187
188
}
189
190
class GroupTest_MemberForm extends Form {
191
192
	public function __construct($controller, $name) {
193
		$fields = singleton('GroupTest_Member')->getCMSFields();
194
		$actions = new FieldList(
195
			new FormAction('doSave','save')
196
		);
197
198
		parent::__construct($controller, $name, $fields, $actions);
199
	}
200
201
	public function doSave($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $data 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...
Unused Code introduced by
The parameter $form 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...
202
		// done in testing methods
203
	}
204
205
}
206