Completed
Push — master ( d7cfe2...c69a56 )
by Daniel
08:24
created

GroupTest   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
dl 0
loc 264
rs 10
c 0
b 0
f 0
wmc 10
lcom 1
cbo 14

10 Methods

Rating   Name   Duplication   Size   Complexity  
A testGroupCodeDefaultsToTitle() 0 18 1
A testMemberGroupRelationForm() 0 49 1
A testUnsavedGroups() 0 16 1
B testCollateAncestorIDs() 0 32 1
A testCollateFamilyIds() 0 8 1
A testCannotCollateUnsavedGroupFamilyIds() 0 7 1
A testGetAllChildren() 0 8 1
B testGroupInGroupMethods() 0 30 1
B testDelete() 0 28 1
B testValidatesPrivilegeLevelOfParent() 0 38 1
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use InvalidArgumentException;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Dev\FunctionalTest;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\Security\Group;
11
use SilverStripe\Security\Member;
12
use SilverStripe\Security\Permission;
13
use SilverStripe\Security\Tests\GroupTest\TestMember;
14
15
class GroupTest extends FunctionalTest
16
{
17
    protected static $fixture_file = 'GroupTest.yml';
18
19
    protected static $extra_dataobjects = [
20
        TestMember::class
21
    ];
22
23
    public function testGroupCodeDefaultsToTitle()
24
    {
25
        $g1 = new Group();
26
        $g1->Title = "My Title";
27
        $g1->write();
28
        $this->assertEquals('my-title', $g1->Code, 'Custom title gets converted to code if none exists already');
29
30
        $g2 = new Group();
31
        $g2->Title = "My Title";
32
        $g2->Code = "my-code";
33
        $g2->write();
34
        $this->assertEquals('my-code', $g2->Code, 'Custom attributes are not overwritten by Title field');
35
36
        $g3 = new Group();
37
        $g3->Title = _t('SilverStripe\\Admin\\SecurityAdmin.NEWGROUP', "New Group");
38
        $g3->write();
39
        $this->assertNull($g3->Code, 'Default title doesnt trigger attribute setting');
40
    }
41
42
    /**
43
     * @skipUpgrade
44
     */
45
    public function testMemberGroupRelationForm()
46
    {
47
        $this->logInAs($this->idFromFixture(TestMember::class, 'admin'));
48
49
        $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
50
        $parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
51
52
        // Test single group relation through checkboxsetfield
53
        $form = new GroupTest\MemberForm(Controller::curr(), 'Form');
54
        /** @var Member $member */
55
        $member = $this->objFromFixture(TestMember::class, 'admin');
56
        $form->loadDataFrom($member);
57
        $checkboxSetField = $form->Fields()->fieldByName('Groups');
58
        $checkboxSetField->setValue(
59
            array(
60
            $adminGroup->ID => $adminGroup->ID, // keep existing relation
61
            $parentGroup->ID => $parentGroup->ID, // add new relation
62
            )
63
        );
64
        $form->saveInto($member);
65
        $updatedGroups = $member->Groups();
66
67
        $this->assertEquals(
68
            2,
69
            count($updatedGroups->column()),
70
            "Adding a toplevel group works"
71
        );
72
        $this->assertContains($adminGroup->ID, $updatedGroups->column('ID'));
73
        $this->assertContains($parentGroup->ID, $updatedGroups->column('ID'));
74
75
        // Test unsetting relationship
76
        $form->loadDataFrom($member);
77
        $checkboxSetField = $form->Fields()->fieldByName('Groups');
78
        $checkboxSetField->setValue(
79
            array(
80
            $adminGroup->ID => $adminGroup->ID, // keep existing relation
81
            //$parentGroup->ID => $parentGroup->ID, // remove previously set relation
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% 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...
82
            )
83
        );
84
        $form->saveInto($member);
85
        $member->flushCache();
86
        $updatedGroups = $member->Groups();
87
        $this->assertEquals(
88
            1,
89
            count($updatedGroups->column()),
90
            "Removing a previously added toplevel group works"
91
        );
92
        $this->assertContains($adminGroup->ID, $updatedGroups->column('ID'));
93
    }
94
95
    public function testUnsavedGroups()
96
    {
97
        $member = $this->objFromFixture(TestMember::class, 'admin');
98
        $group = new Group();
99
100
        // Can save user to unsaved group
101
        $group->Members()->add($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture(\S...Member::class, 'admin') on line 97 can be null; however, SilverStripe\ORM\ManyManyList::add() 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...
102
        $this->assertEquals(array($member->ID), array_values($group->Members()->getIDList()));
103
104
        // Persists after writing to DB
105
        $group->write();
106
107
        /** @var Group $group */
108
        $group = Group::get()->byID($group->ID);
109
        $this->assertEquals(array($member->ID), array_values($group->Members()->getIDList()));
110
    }
111
112
    public function testCollateAncestorIDs()
113
    {
114
        /** @var Group $parentGroup */
115
        $parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
116
        /** @var Group $childGroup */
117
        $childGroup = $this->objFromFixture(Group::class, 'childgroup');
118
        $orphanGroup = new Group();
119
        $orphanGroup->ParentID = 99999;
120
        $orphanGroup->write();
121
122
        $this->assertEquals(
123
            1,
124
            count($parentGroup->collateAncestorIDs()),
125
            'Root node only contains itself'
126
        );
127
        $this->assertContains($parentGroup->ID, $parentGroup->collateAncestorIDs());
128
129
        $this->assertEquals(
130
            2,
131
            count($childGroup->collateAncestorIDs()),
132
            'Contains parent nodes, with child node first'
133
        );
134
        $this->assertContains($parentGroup->ID, $childGroup->collateAncestorIDs());
135
        $this->assertContains($childGroup->ID, $childGroup->collateAncestorIDs());
136
137
        $this->assertEquals(
138
            1,
139
            count($orphanGroup->collateAncestorIDs()),
140
            'Orphaned nodes dont contain invalid parent IDs'
141
        );
142
        $this->assertContains($orphanGroup->ID, $orphanGroup->collateAncestorIDs());
143
    }
144
145
    /**
146
     * Test that Groups including their children (recursively) are collated and returned
147
     */
148
    public function testCollateFamilyIds()
149
    {
150
        /** @var Group $group */
151
        $group = $this->objFromFixture(Group::class, 'parentgroup');
152
        $groupIds = $this->allFixtureIDs(Group::class);
153
        $ids = array_intersect_key($groupIds, array_flip(['parentgroup', 'childgroup', 'grandchildgroup']));
154
        $this->assertEquals(array_values($ids), $group->collateFamilyIDs());
155
    }
156
157
    /**
158
     * Test that an exception is thrown if collateFamilyIDs is called on an unsaved Group
159
     */
160
    public function testCannotCollateUnsavedGroupFamilyIds()
161
    {
162
        $this->expectException(InvalidArgumentException::class);
163
        $this->expectExceptionMessage('Cannot call collateFamilyIDs on unsaved Group.');
164
        $group = new Group;
165
        $group->collateFamilyIDs();
166
    }
167
168
    /**
169
     * Test that a Group's children can be retrieved
170
     */
171
    public function testGetAllChildren()
172
    {
173
        /** @var Group $group */
174
        $group = $this->objFromFixture(Group::class, 'parentgroup');
175
        $children = $group->getAllChildren();
176
        $this->assertInstanceOf(ArrayList::class, $children);
177
        $this->assertSame(['childgroup', 'grandchildgroup'], $children->column('Code'));
178
    }
179
180
    public function testGroupInGroupMethods()
181
    {
182
        $parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
183
        $childGroup = $this->objFromFixture(Group::class, 'childgroup');
184
        $grandchildGroup = $this->objFromFixture(Group::class, 'grandchildgroup');
185
        $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
186
        $group1 = $this->objFromFixture(Group::class, 'group1');
187
188
        $this->assertTrue($grandchildGroup->inGroup($childGroup));
189
        $this->assertTrue($grandchildGroup->inGroup($childGroup->ID));
190
        $this->assertTrue($grandchildGroup->inGroup($childGroup->Code));
191
192
        $this->assertTrue($grandchildGroup->inGroup($parentGroup));
193
        $this->assertTrue($grandchildGroup->inGroups([$parentGroup, $childGroup]));
194
        $this->assertTrue($grandchildGroup->inGroups([$childGroup, $parentGroup]));
195
        $this->assertTrue($grandchildGroup->inGroups([$parentGroup, $childGroup], true));
196
197
        $this->assertFalse($grandchildGroup->inGroup($adminGroup));
198
        $this->assertFalse($grandchildGroup->inGroups([$adminGroup, $group1]));
199
        $this->assertFalse($grandchildGroup->inGroups([$adminGroup, $childGroup], true));
200
201
        $this->assertFalse($grandchildGroup->inGroup('NotARealGroup'));
202
        $this->assertFalse($grandchildGroup->inGroup(99999999999));
203
        $this->assertFalse($grandchildGroup->inGroup(new TestMember()));
204
205
        // Edgecases
206
        $this->assertTrue($grandchildGroup->inGroup($grandchildGroup));
207
        $this->assertFalse($grandchildGroup->inGroups([]));
208
        $this->assertFalse($grandchildGroup->inGroups([], true));
209
    }
210
211
    public function testDelete()
212
    {
213
        $group = $this->objFromFixture(Group::class, 'parentgroup');
214
        $groupID = $group->ID;
215
        $childGroupID = $this->idFromFixture(Group::class, 'childgroup');
216
        $group->delete();
217
218
        $this->assertEquals(
219
            0,
220
            DataObject::get(Group::class, "\"ID\" = {$groupID}")->count(),
221
            'Group is removed'
222
        );
223
        $this->assertEquals(
224
            0,
225
            DataObject::get(Permission::class, "\"GroupID\" = {$groupID}")->count(),
226
            'Permissions removed along with the group'
227
        );
228
        $this->assertEquals(
229
            0,
230
            DataObject::get(Group::class, "\"ParentID\" = {$groupID}")->count(),
231
            'Child groups are removed'
232
        );
233
        $this->assertEquals(
234
            0,
235
            DataObject::get(Group::class, "\"ParentID\" = {$childGroupID}")->count(),
236
            'Grandchild groups are removed'
237
        );
238
    }
239
240
    public function testValidatesPrivilegeLevelOfParent()
241
    {
242
        /** @var Group $nonAdminGroup */
243
        $nonAdminGroup = $this->objFromFixture(Group::class, 'childgroup');
244
        /** @var Group $adminGroup */
245
        $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
246
247
        // Making admin group parent of a non-admin group, effectively expanding is privileges
248
        $nonAdminGroup->ParentID = $adminGroup->ID;
249
250
        $this->logInWithPermission('APPLY_ROLES');
251
        $result = $nonAdminGroup->validate();
252
        $this->assertFalse(
253
            $result->isValid(),
254
            'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions'
255
        );
256
257
        $this->logInWithPermission('ADMIN');
258
        $result = $nonAdminGroup->validate();
259
        $this->assertTrue(
260
            $result->isValid(),
261
            'Members with ADMIN can assign parent groups with direct ADMIN permissions'
262
        );
263
        $nonAdminGroup->write();
264
265
        $this->logInWithPermission('ADMIN');
266
        /** @var Group $inheritedAdminGroup */
267
        $inheritedAdminGroup = $this->objFromFixture(Group::class, 'group1');
268
        $inheritedAdminGroup->ParentID = $adminGroup->ID;
269
        $inheritedAdminGroup->write(); // only works with ADMIN login
270
271
        $this->logInWithPermission('APPLY_ROLES');
272
        $result = $nonAdminGroup->validate();
273
        $this->assertFalse(
274
            $result->isValid(),
275
            'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission'
276
        );
277
    }
278
}
279