1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * API tests.
19 *
20 * @package    tool_dataprivacy
21 * @copyright  2018 Jun Pataleta
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25use core\invalid_persistent_exception;
26use core\task\manager;
27use tool_dataprivacy\context_instance;
28use tool_dataprivacy\api;
29use tool_dataprivacy\data_registry;
30use tool_dataprivacy\expired_context;
31use tool_dataprivacy\data_request;
32use tool_dataprivacy\purpose;
33use tool_dataprivacy\category;
34use tool_dataprivacy\local\helper;
35use tool_dataprivacy\task\process_data_request_task;
36
37defined('MOODLE_INTERNAL') || die();
38global $CFG;
39
40/**
41 * API tests.
42 *
43 * @package    tool_dataprivacy
44 * @copyright  2018 Jun Pataleta
45 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 */
47class tool_dataprivacy_api_testcase extends advanced_testcase {
48
49    /**
50     * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
51     * tested with the default context.
52     */
53    public function test_check_can_manage_data_registry_admin() {
54        $this->resetAfterTest();
55
56        $this->setAdminUser();
57        // Technically this actually returns void, but assertNull will suffice to avoid a pointless test.
58        $this->assertNull(api::check_can_manage_data_registry());
59    }
60
61    /**
62     * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
63     * tested with the default context.
64     */
65    public function test_check_can_manage_data_registry_without_cap_default() {
66        $this->resetAfterTest();
67
68        $user = $this->getDataGenerator()->create_user();
69        $this->setUser($user);
70
71        $this->expectException(required_capability_exception::class);
72        api::check_can_manage_data_registry();
73    }
74
75    /**
76     * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
77     * tested with the default context.
78     */
79    public function test_check_can_manage_data_registry_without_cap_system() {
80        $this->resetAfterTest();
81
82        $user = $this->getDataGenerator()->create_user();
83        $this->setUser($user);
84
85        $this->expectException(required_capability_exception::class);
86        api::check_can_manage_data_registry(\context_system::instance()->id);
87    }
88
89    /**
90     * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
91     * tested with the default context.
92     */
93    public function test_check_can_manage_data_registry_without_cap_own_user() {
94        $this->resetAfterTest();
95
96        $user = $this->getDataGenerator()->create_user();
97        $this->setUser($user);
98
99        $this->expectException(required_capability_exception::class);
100        api::check_can_manage_data_registry(\context_user::instance($user->id)->id);
101    }
102
103    /**
104     * Test for api::update_request_status().
105     */
106    public function test_update_request_status() {
107        $this->resetAfterTest();
108
109        $generator = new testing_data_generator();
110        $s1 = $generator->create_user();
111        $this->setUser($s1);
112
113        // Create the sample data request.
114        $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT);
115
116        $requestid = $datarequest->get('id');
117
118        // Update with a comment.
119        $comment = 'This is an example of a comment';
120        $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, $comment);
121        $this->assertTrue($result);
122        $datarequest = new data_request($requestid);
123        $this->assertStringEndsWith($comment, $datarequest->get('dpocomment'));
124
125        // Update with a comment which will be trimmed.
126        $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, '  ');
127        $this->assertTrue($result);
128        $datarequest = new data_request($requestid);
129        $this->assertStringEndsWith($comment, $datarequest->get('dpocomment'));
130
131        // Update with a comment.
132        $secondcomment = '  - More comments -  ';
133        $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, $secondcomment);
134        $this->assertTrue($result);
135        $datarequest = new data_request($requestid);
136        $this->assertRegExp("/.*{$comment}.*{$secondcomment}/s", $datarequest->get('dpocomment'));
137
138        // Update with a valid status.
139        $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_DOWNLOAD_READY);
140        $this->assertTrue($result);
141
142        // Fetch the request record again.
143        $datarequest = new data_request($requestid);
144        $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $datarequest->get('status'));
145
146        // Update with an invalid status.
147        $this->expectException(invalid_persistent_exception::class);
148        api::update_request_status($requestid, -1);
149    }
150
151    /**
152     * Test for api::get_site_dpos() when there are no users with the DPO role.
153     */
154    public function test_get_site_dpos_no_dpos() {
155        $this->resetAfterTest();
156
157        $admin = get_admin();
158
159        $dpos = api::get_site_dpos();
160        $this->assertCount(1, $dpos);
161        $dpo = reset($dpos);
162        $this->assertEquals($admin->id, $dpo->id);
163    }
164
165    /**
166     * Test for api::get_site_dpos() when there are no users with the DPO role.
167     */
168    public function test_get_site_dpos() {
169        global $DB;
170
171        $this->resetAfterTest();
172
173        $generator = new testing_data_generator();
174        $u1 = $generator->create_user();
175        $u2 = $generator->create_user();
176
177        $context = context_system::instance();
178
179        // Give the manager role with the capability to manage data requests.
180        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
181        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
182        // Assign u1 as a manager.
183        role_assign($managerroleid, $u1->id, $context->id);
184
185        // Give the editing teacher role with the capability to manage data requests.
186        $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
187        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true);
188        // Assign u1 as an editing teacher as well.
189        role_assign($editingteacherroleid, $u1->id, $context->id);
190        // Assign u2 as an editing teacher.
191        role_assign($editingteacherroleid, $u2->id, $context->id);
192
193        // Only map the manager role to the DPO role.
194        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
195
196        $dpos = api::get_site_dpos();
197        $this->assertCount(1, $dpos);
198        $dpo = reset($dpos);
199        $this->assertEquals($u1->id, $dpo->id);
200    }
201
202    /**
203     * Test for \tool_dataprivacy\api::get_assigned_privacy_officer_roles().
204     */
205    public function test_get_assigned_privacy_officer_roles() {
206        global $DB;
207
208        $this->resetAfterTest();
209
210        // Erroneously set the manager roles as the PO, even if it doesn't have the managedatarequests capability yet.
211        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
212        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
213        // Get the assigned PO roles when nothing has been set yet.
214        $roleids = api::get_assigned_privacy_officer_roles();
215        // Confirm that the returned list is empty.
216        $this->assertEmpty($roleids);
217
218        $context = context_system::instance();
219
220        // Give the manager role with the capability to manage data requests.
221        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
222
223        // Give the editing teacher role with the capability to manage data requests.
224        $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
225        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true);
226
227        // Get the non-editing teacher role ID.
228        $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
229
230        // Erroneously map the manager and the non-editing teacher roles to the PO role.
231        $badconfig = $managerroleid . ',' . $teacherroleid;
232        set_config('dporoles', $badconfig, 'tool_dataprivacy');
233
234        // Get the assigned PO roles.
235        $roleids = api::get_assigned_privacy_officer_roles();
236
237        // There should only be one PO role.
238        $this->assertCount(1, $roleids);
239        // Confirm it contains the manager role.
240        $this->assertContains($managerroleid, $roleids);
241        // And it does not contain the editing teacher role.
242        $this->assertNotContains($editingteacherroleid, $roleids);
243    }
244
245    /**
246     * Test for api::approve_data_request().
247     */
248    public function test_approve_data_request() {
249        global $DB;
250
251        $this->resetAfterTest();
252
253        $generator = new testing_data_generator();
254        $s1 = $generator->create_user();
255        $u1 = $generator->create_user();
256
257        $context = context_system::instance();
258
259        // Manager role.
260        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
261        // Give the manager role with the capability to manage data requests.
262        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
263        // Assign u1 as a manager.
264        role_assign($managerroleid, $u1->id, $context->id);
265
266        // Map the manager role to the DPO role.
267        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
268
269        // Create the sample data request.
270        $this->setUser($s1);
271        $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT);
272        $requestid = $datarequest->get('id');
273
274        // Make this ready for approval.
275        api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
276
277        $this->setUser($u1);
278        $result = api::approve_data_request($requestid);
279        $this->assertTrue($result);
280        $datarequest = new data_request($requestid);
281        $this->assertEquals($u1->id, $datarequest->get('dpo'));
282        $this->assertEquals(api::DATAREQUEST_STATUS_APPROVED, $datarequest->get('status'));
283
284        // Test adhoc task creation.
285        $adhoctasks = manager::get_adhoc_tasks(process_data_request_task::class);
286        $this->assertCount(1, $adhoctasks);
287    }
288
289    /**
290     * Test for api::approve_data_request() when called by a user who doesn't have the DPO role.
291     */
292    public function test_approve_data_request_non_dpo_user() {
293        $this->resetAfterTest();
294
295        $generator = new testing_data_generator();
296        $student = $generator->create_user();
297        $teacher = $generator->create_user();
298
299        // Create the sample data request.
300        $this->setUser($student);
301        $datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT);
302
303        $requestid = $datarequest->get('id');
304
305        // Login as a user without DPO role.
306        $this->setUser($teacher);
307        $this->expectException(required_capability_exception::class);
308        api::approve_data_request($requestid);
309    }
310
311    /**
312     * Test that deletion requests for the primary admin are rejected
313     */
314    public function test_reject_data_deletion_request_primary_admin() {
315        $this->resetAfterTest();
316        $this->setAdminUser();
317
318        $datarequest = api::create_data_request(get_admin()->id, api::DATAREQUEST_TYPE_DELETE);
319
320        // Approve the request and execute the ad-hoc process task.
321        ob_start();
322        api::approve_data_request($datarequest->get('id'));
323        $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
324        ob_end_clean();
325
326        $request = api::get_request($datarequest->get('id'));
327        $this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $request->get('status'));
328
329        // Confirm they weren't deleted.
330        $user = core_user::get_user($request->get('userid'));
331        core_user::require_active_user($user);
332    }
333
334    /**
335     * Test for api::can_contact_dpo()
336     */
337    public function test_can_contact_dpo() {
338        $this->resetAfterTest();
339
340        // Default ('contactdataprotectionofficer' is disabled by default).
341        $this->assertFalse(api::can_contact_dpo());
342
343        // Enable.
344        set_config('contactdataprotectionofficer', 1, 'tool_dataprivacy');
345        $this->assertTrue(api::can_contact_dpo());
346
347        // Disable again.
348        set_config('contactdataprotectionofficer', 0, 'tool_dataprivacy');
349        $this->assertFalse(api::can_contact_dpo());
350    }
351
352    /**
353     * Test for api::can_manage_data_requests()
354     */
355    public function test_can_manage_data_requests() {
356        global $DB;
357
358        $this->resetAfterTest();
359
360        // No configured site DPOs yet.
361        $admin = get_admin();
362        $this->assertTrue(api::can_manage_data_requests($admin->id));
363
364        $generator = new testing_data_generator();
365        $dpo = $generator->create_user();
366        $nondpocapable = $generator->create_user();
367        $nondpoincapable = $generator->create_user();
368
369        $context = context_system::instance();
370
371        // Manager role.
372        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
373        // Give the manager role with the capability to manage data requests.
374        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
375        // Assign u1 as a manager.
376        role_assign($managerroleid, $dpo->id, $context->id);
377
378        // Editing teacher role.
379        $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
380        // Give the editing teacher role with the capability to manage data requests.
381        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
382        // Assign u2 as an editing teacher.
383        role_assign($editingteacherroleid, $nondpocapable->id, $context->id);
384
385        // Map only the manager role to the DPO role.
386        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
387
388        // User with capability and has DPO role.
389        $this->assertTrue(api::can_manage_data_requests($dpo->id));
390        // User with capability but has no DPO role.
391        $this->assertFalse(api::can_manage_data_requests($nondpocapable->id));
392        // User without the capability and has no DPO role.
393        $this->assertFalse(api::can_manage_data_requests($nondpoincapable->id));
394    }
395
396    /**
397     * Test that a user who has no capability to make any data requests for children cannot create data requests for any
398     * other user.
399     */
400    public function test_can_create_data_request_for_user_no() {
401        $this->resetAfterTest();
402
403        $parent = $this->getDataGenerator()->create_user();
404        $otheruser = $this->getDataGenerator()->create_user();
405
406        $this->setUser($parent);
407        $this->assertFalse(api::can_create_data_request_for_user($otheruser->id));
408    }
409
410    /**
411     * Test that a user who has the capability to make any data requests for one other user cannot create data requests
412     * for any other user.
413     */
414    public function test_can_create_data_request_for_user_some() {
415        $this->resetAfterTest();
416
417        $parent = $this->getDataGenerator()->create_user();
418        $child = $this->getDataGenerator()->create_user();
419        $otheruser = $this->getDataGenerator()->create_user();
420
421        $systemcontext = \context_system::instance();
422        $parentrole = $this->getDataGenerator()->create_role();
423        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
424        role_assign($parentrole, $parent->id, \context_user::instance($child->id));
425
426        $this->setUser($parent);
427        $this->assertFalse(api::can_create_data_request_for_user($otheruser->id));
428    }
429
430    /**
431     * Test that a user who has the capability to make any data requests for one other user cannot create data requests
432     * for any other user.
433     */
434    public function test_can_create_data_request_for_user_own_child() {
435        $this->resetAfterTest();
436
437        $parent = $this->getDataGenerator()->create_user();
438        $child = $this->getDataGenerator()->create_user();
439
440        $systemcontext = \context_system::instance();
441        $parentrole = $this->getDataGenerator()->create_role();
442        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
443        role_assign($parentrole, $parent->id, \context_user::instance($child->id));
444
445        $this->setUser($parent);
446        $this->assertTrue(api::can_create_data_request_for_user($child->id));
447    }
448
449    /**
450     * Test that a user who has no capability to make any data requests for children cannot create data requests for any
451     * other user.
452     */
453    public function test_require_can_create_data_request_for_user_no() {
454        $this->resetAfterTest();
455
456        $parent = $this->getDataGenerator()->create_user();
457        $otheruser = $this->getDataGenerator()->create_user();
458
459        $this->setUser($parent);
460        $this->expectException('required_capability_exception');
461        api::require_can_create_data_request_for_user($otheruser->id);
462    }
463
464    /**
465     * Test that a user who has the capability to make any data requests for one other user cannot create data requests
466     * for any other user.
467     */
468    public function test_require_can_create_data_request_for_user_some() {
469        $this->resetAfterTest();
470
471        $parent = $this->getDataGenerator()->create_user();
472        $child = $this->getDataGenerator()->create_user();
473        $otheruser = $this->getDataGenerator()->create_user();
474
475        $systemcontext = \context_system::instance();
476        $parentrole = $this->getDataGenerator()->create_role();
477        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
478        role_assign($parentrole, $parent->id, \context_user::instance($child->id));
479
480        $this->setUser($parent);
481        $this->expectException('required_capability_exception');
482        api::require_can_create_data_request_for_user($otheruser->id);
483    }
484
485    /**
486     * Test that a user who has the capability to make any data requests for one other user cannot create data requests
487     * for any other user.
488     */
489    public function test_require_can_create_data_request_for_user_own_child() {
490        $this->resetAfterTest();
491
492        $parent = $this->getDataGenerator()->create_user();
493        $child = $this->getDataGenerator()->create_user();
494
495        $systemcontext = \context_system::instance();
496        $parentrole = $this->getDataGenerator()->create_role();
497        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
498        role_assign($parentrole, $parent->id, \context_user::instance($child->id));
499
500        $this->setUser($parent);
501        $this->assertTrue(api::require_can_create_data_request_for_user($child->id));
502    }
503
504    /**
505     * Test for api::can_download_data_request_for_user()
506     */
507    public function test_can_download_data_request_for_user() {
508        $this->resetAfterTest();
509
510        $generator = $this->getDataGenerator();
511
512        // Three victims.
513        $victim1 = $generator->create_user();
514        $victim2 = $generator->create_user();
515        $victim3 = $generator->create_user();
516
517        // Assign a user as victim 1's parent.
518        $systemcontext = \context_system::instance();
519        $parentrole = $generator->create_role();
520        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
521        $parent = $generator->create_user();
522        role_assign($parentrole, $parent->id, \context_user::instance($victim1->id));
523
524        // Assign another user as data access wonder woman.
525        $wonderrole = $generator->create_role();
526        assign_capability('tool/dataprivacy:downloadallrequests', CAP_ALLOW, $wonderrole, $systemcontext);
527        $staff = $generator->create_user();
528        role_assign($wonderrole, $staff->id, $systemcontext);
529
530        // Finally, victim 3 has been naughty; stop them accessing their own data.
531        $naughtyrole = $generator->create_role();
532        assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $naughtyrole, $systemcontext);
533        role_assign($naughtyrole, $victim3->id, $systemcontext);
534
535        // Victims 1 and 2 can access their own data, regardless of who requested it.
536        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $victim1->id));
537        $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $victim2->id));
538
539        // Victim 3 cannot access his own data.
540        $this->assertFalse(api::can_download_data_request_for_user($victim3->id, $victim3->id, $victim3->id));
541
542        // Victims 1 and 2 cannot access another victim's data.
543        $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $victim1->id, $victim1->id));
544        $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $victim2->id));
545
546        // Staff can access everyone's data.
547        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $staff->id));
548        $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $staff->id));
549        $this->assertTrue(api::can_download_data_request_for_user($victim3->id, $staff->id, $staff->id));
550
551        // Parent can access victim 1's data only if they requested it.
552        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $parent->id, $parent->id));
553        $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $parent->id));
554        $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $parent->id, $parent->id));
555    }
556
557    /**
558     * Data provider for data request creation tests.
559     *
560     * @return array
561     */
562    public function data_request_creation_provider() {
563        return [
564            'Export request by user, automatic approval off' => [
565                false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0,
566                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
567            ],
568            'Export request by user, automatic approval on' => [
569                false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 0,
570                api::DATAREQUEST_STATUS_APPROVED, 1
571            ],
572            'Export request by PO, automatic approval off' => [
573                true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0,
574                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
575            ],
576            'Export request by PO, automatic approval on' => [
577                true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 'dpo',
578                api::DATAREQUEST_STATUS_APPROVED, 1
579            ],
580            'Delete request by user, automatic approval off' => [
581                false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0,
582                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
583            ],
584            'Delete request by user, automatic approval on' => [
585                false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 0,
586                api::DATAREQUEST_STATUS_APPROVED, 1
587            ],
588            'Delete request by PO, automatic approval off' => [
589                true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0,
590                api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0
591            ],
592            'Delete request by PO, automatic approval on' => [
593                true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 'dpo',
594                api::DATAREQUEST_STATUS_APPROVED, 1
595            ],
596        ];
597    }
598
599    /**
600     * Test for api::create_data_request()
601     *
602     * @dataProvider data_request_creation_provider
603     * @param bool $asprivacyofficer Whether the request is made as the Privacy Officer or the user itself.
604     * @param string $type The data request type.
605     * @param string $setting The automatic approval setting.
606     * @param bool $automaticapproval Whether automatic data request approval is turned on or not.
607     * @param int|string $expecteddpoval The expected value for the 'dpo' field. 'dpo' means we'd the expected value would be the
608     *                                   user ID of the privacy officer which happens in the case where a PO requests on behalf of
609     *                                   someone else and automatic data request approval is turned on.
610     * @param int $expectedstatus The expected status of the data request.
611     * @param int $expectedtaskcount The number of expected queued data requests tasks.
612     * @throws coding_exception
613     * @throws invalid_persistent_exception
614     */
615    public function test_create_data_request($asprivacyofficer, $type, $setting, $automaticapproval, $expecteddpoval,
616                                             $expectedstatus, $expectedtaskcount) {
617        global $USER;
618
619        $this->resetAfterTest();
620
621        $generator = new testing_data_generator();
622        $user = $generator->create_user();
623        $comment = 'sample comment';
624
625        // Login.
626        if ($asprivacyofficer) {
627            $this->setAdminUser();
628        } else {
629            $this->setUser($user->id);
630        }
631
632        // Set the automatic data request approval setting value.
633        set_config($setting, $automaticapproval, 'tool_dataprivacy');
634
635        // If set to 'dpo' use the currently logged-in user's ID (which should be the admin user's ID).
636        if ($expecteddpoval === 'dpo') {
637            $expecteddpoval = $USER->id;
638        }
639
640        // Test data request creation.
641        $datarequest = api::create_data_request($user->id, $type, $comment);
642        $this->assertEquals($user->id, $datarequest->get('userid'));
643        $this->assertEquals($USER->id, $datarequest->get('requestedby'));
644        $this->assertEquals($expecteddpoval, $datarequest->get('dpo'));
645        $this->assertEquals($type, $datarequest->get('type'));
646        $this->assertEquals($expectedstatus, $datarequest->get('status'));
647        $this->assertEquals($comment, $datarequest->get('comments'));
648        $this->assertEquals($automaticapproval, $datarequest->get('systemapproved'));
649
650        // Test number of queued data request tasks.
651        $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class);
652        $this->assertCount($expectedtaskcount, $datarequesttasks);
653    }
654
655    /**
656     * Test for api::create_data_request() made by a parent.
657     */
658    public function test_create_data_request_by_parent() {
659        global $DB;
660
661        $this->resetAfterTest();
662
663        $generator = new testing_data_generator();
664        $user = $generator->create_user();
665        $parent = $generator->create_user();
666        $comment = 'sample comment';
667
668        // Get the teacher role pretend it's the parent roles ;).
669        $systemcontext = context_system::instance();
670        $usercontext = context_user::instance($user->id);
671        $parentroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
672        // Give the manager role with the capability to manage data requests.
673        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentroleid, $systemcontext->id, true);
674        // Assign the parent to user.
675        role_assign($parentroleid, $parent->id, $usercontext->id);
676
677        // Login as the user's parent.
678        $this->setUser($parent);
679
680        // Test data request creation.
681        $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
682        $this->assertEquals($user->id, $datarequest->get('userid'));
683        $this->assertEquals($parent->id, $datarequest->get('requestedby'));
684        $this->assertEquals(0, $datarequest->get('dpo'));
685        $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type'));
686        $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $datarequest->get('status'));
687        $this->assertEquals($comment, $datarequest->get('comments'));
688    }
689
690    /**
691     * Test for api::deny_data_request()
692     */
693    public function test_deny_data_request() {
694        $this->resetAfterTest();
695
696        $generator = new testing_data_generator();
697        $user = $generator->create_user();
698        $comment = 'sample comment';
699
700        // Login as user.
701        $this->setUser($user->id);
702
703        // Test data request creation.
704        $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
705
706        // Login as the admin (default DPO when no one is set).
707        $this->setAdminUser();
708
709        // Make this ready for approval.
710        api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
711
712        // Deny the data request.
713        $result = api::deny_data_request($datarequest->get('id'));
714        $this->assertTrue($result);
715    }
716
717    /**
718     * Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
719     *
720     * @return array
721     */
722    public function get_data_requests_provider() {
723        $completeonly = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_DELETED];
724        $completeandcancelled = array_merge($completeonly, [api::DATAREQUEST_STATUS_CANCELLED]);
725
726        return [
727            // Own data requests.
728            ['user', false, $completeonly],
729            // Non-DPO fetching all requets.
730            ['user', true, $completeonly],
731            // Admin fetching all completed and cancelled requests.
732            ['dpo', true, $completeandcancelled],
733            // Admin fetching all completed requests.
734            ['dpo', true, $completeonly],
735            // Guest fetching all requests.
736            ['guest', true, $completeonly],
737        ];
738    }
739
740    /**
741     * Test for api::get_data_requests()
742     *
743     * @dataProvider get_data_requests_provider
744     * @param string $usertype The type of the user logging in.
745     * @param boolean $fetchall Whether to fetch all records.
746     * @param int[] $statuses Status filters.
747     */
748    public function test_get_data_requests($usertype, $fetchall, $statuses) {
749        $this->resetAfterTest();
750
751        $generator = new testing_data_generator();
752        $user1 = $generator->create_user();
753        $user2 = $generator->create_user();
754        $user3 = $generator->create_user();
755        $user4 = $generator->create_user();
756        $user5 = $generator->create_user();
757        $users = [$user1, $user2, $user3, $user4, $user5];
758
759        switch ($usertype) {
760            case 'user':
761                $loggeduser = $user1;
762                break;
763            case 'dpo':
764                $loggeduser = get_admin();
765                break;
766            case 'guest':
767                $loggeduser = guest_user();
768                break;
769        }
770
771        $comment = 'Data %s request comment by user %d';
772        $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
773        $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
774        // Make a data requests for the users.
775        foreach ($users as $user) {
776            $this->setUser($user);
777            api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id));
778            api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id));
779        }
780
781        // Log in as the target user.
782        $this->setUser($loggeduser);
783        // Get records count based on the filters.
784        $userid = $loggeduser->id;
785        if ($fetchall) {
786            $userid = 0;
787        }
788        $count = api::get_data_requests_count($userid);
789        if (api::is_site_dpo($loggeduser->id)) {
790            // DPOs should see all the requests.
791            $this->assertEquals(count($users) * 2, $count);
792        } else {
793            if (empty($userid)) {
794                // There should be no data requests for this user available.
795                $this->assertEquals(0, $count);
796            } else {
797                // There should be only one (request with pending status).
798                $this->assertEquals(2, $count);
799            }
800        }
801        // Get data requests.
802        $requests = api::get_data_requests($userid);
803        // The number of requests should match the count.
804        $this->assertCount($count, $requests);
805
806        // Test filtering by status.
807        if ($count && !empty($statuses)) {
808            $filteredcount = api::get_data_requests_count($userid, $statuses);
809            // There should be none as they are all pending.
810            $this->assertEquals(0, $filteredcount);
811            $filteredrequests = api::get_data_requests($userid, $statuses);
812            $this->assertCount($filteredcount, $filteredrequests);
813
814            $statuscounts = [];
815            foreach ($statuses as $stat) {
816                $statuscounts[$stat] = 0;
817            }
818            $numstatus = count($statuses);
819            // Get all requests with status filter and update statuses, randomly.
820            foreach ($requests as $request) {
821                if (rand(0, 1)) {
822                    continue;
823                }
824
825                if ($numstatus > 1) {
826                    $index = rand(0, $numstatus - 1);
827                    $status = $statuses[$index];
828                } else {
829                    $status = reset($statuses);
830                }
831                $statuscounts[$status]++;
832                api::update_request_status($request->get('id'), $status);
833            }
834            $total = array_sum($statuscounts);
835            $filteredcount = api::get_data_requests_count($userid, $statuses);
836            $this->assertEquals($total, $filteredcount);
837            $filteredrequests = api::get_data_requests($userid, $statuses);
838            $this->assertCount($filteredcount, $filteredrequests);
839            // Confirm the filtered requests match the status filter(s).
840            foreach ($filteredrequests as $request) {
841                $this->assertContains($request->get('status'), $statuses);
842            }
843
844            if ($numstatus > 1) {
845                // Fetch by individual status to check the numbers match.
846                foreach ($statuses as $status) {
847                    $filteredcount = api::get_data_requests_count($userid, [$status]);
848                    $this->assertEquals($statuscounts[$status], $filteredcount);
849                    $filteredrequests = api::get_data_requests($userid, [$status]);
850                    $this->assertCount($filteredcount, $filteredrequests);
851                }
852            }
853        }
854    }
855
856    /**
857     * Data provider for test_has_ongoing_request.
858     */
859    public function status_provider() {
860        return [
861            [api::DATAREQUEST_STATUS_AWAITING_APPROVAL, true],
862            [api::DATAREQUEST_STATUS_APPROVED, true],
863            [api::DATAREQUEST_STATUS_PROCESSING, true],
864            [api::DATAREQUEST_STATUS_COMPLETE, false],
865            [api::DATAREQUEST_STATUS_CANCELLED, false],
866            [api::DATAREQUEST_STATUS_REJECTED, false],
867            [api::DATAREQUEST_STATUS_DOWNLOAD_READY, false],
868            [api::DATAREQUEST_STATUS_EXPIRED, false],
869            [api::DATAREQUEST_STATUS_DELETED, false],
870        ];
871    }
872
873    /**
874     * Test for api::has_ongoing_request()
875     *
876     * @dataProvider status_provider
877     * @param int $status The request status.
878     * @param bool $expected The expected result.
879     */
880    public function test_has_ongoing_request($status, $expected) {
881        $this->resetAfterTest();
882
883        $generator = new testing_data_generator();
884        $user1 = $generator->create_user();
885
886        // Make a data request as user 1.
887        $this->setUser($user1);
888        $request = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT);
889        // Set the status.
890        api::update_request_status($request->get('id'), $status);
891
892        // Check if this request is ongoing.
893        $result = api::has_ongoing_request($user1->id, api::DATAREQUEST_TYPE_EXPORT);
894        $this->assertEquals($expected, $result);
895    }
896
897    /**
898     * Test for api::is_active()
899     *
900     * @dataProvider status_provider
901     * @param int $status The request status
902     * @param bool $expected The expected result
903     */
904    public function test_is_active($status, $expected) {
905        // Check if this request is ongoing.
906        $result = api::is_active($status);
907        $this->assertEquals($expected, $result);
908    }
909
910    /**
911     * Test for api::is_site_dpo()
912     */
913    public function test_is_site_dpo() {
914        global $DB;
915
916        $this->resetAfterTest();
917
918        // No configured site DPOs yet.
919        $admin = get_admin();
920        $this->assertTrue(api::is_site_dpo($admin->id));
921
922        $generator = new testing_data_generator();
923        $dpo = $generator->create_user();
924        $nondpo = $generator->create_user();
925
926        $context = context_system::instance();
927
928        // Manager role.
929        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
930        // Give the manager role with the capability to manage data requests.
931        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
932        // Assign u1 as a manager.
933        role_assign($managerroleid, $dpo->id, $context->id);
934
935        // Map only the manager role to the DPO role.
936        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
937
938        // User is a DPO.
939        $this->assertTrue(api::is_site_dpo($dpo->id));
940        // User is not a DPO.
941        $this->assertFalse(api::is_site_dpo($nondpo->id));
942    }
943
944    /**
945     * Data provider function for test_notify_dpo
946     *
947     * @return array
948     */
949    public function notify_dpo_provider() {
950        return [
951            [false, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Export my user data'],
952            [false, api::DATAREQUEST_TYPE_DELETE, 'requesttypedelete', 'Delete my user data'],
953            [false, api::DATAREQUEST_TYPE_OTHERS, 'requesttypeothers', 'Nothing. Just wanna say hi'],
954            [true, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Admin export data of another user'],
955        ];
956    }
957
958    /**
959     * Test for api::notify_dpo()
960     *
961     * @dataProvider notify_dpo_provider
962     * @param bool $byadmin Whether the admin requests data on behalf of the user
963     * @param int $type The request type
964     * @param string $typestringid The request lang string identifier
965     * @param string $comments The requestor's message to the DPO.
966     */
967    public function test_notify_dpo($byadmin, $type, $typestringid, $comments) {
968        $this->resetAfterTest();
969
970        $generator = new testing_data_generator();
971        $user1 = $generator->create_user();
972        // Let's just use admin as DPO (It's the default if not set).
973        $dpo = get_admin();
974        if ($byadmin) {
975            $this->setAdminUser();
976            $requestedby = $dpo;
977        } else {
978            $this->setUser($user1);
979            $requestedby = $user1;
980        }
981
982        // Make a data request for user 1.
983        $request = api::create_data_request($user1->id, $type, $comments);
984
985        $sink = $this->redirectMessages();
986        $messageid = api::notify_dpo($dpo, $request);
987        $this->assertNotFalse($messageid);
988        $messages = $sink->get_messages();
989        $this->assertCount(1, $messages);
990        $message = reset($messages);
991
992        // Check some of the message properties.
993        $this->assertEquals($requestedby->id, $message->useridfrom);
994        $this->assertEquals($dpo->id, $message->useridto);
995        $typestring = get_string($typestringid, 'tool_dataprivacy');
996        $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typestring);
997        $this->assertEquals($subject, $message->subject);
998        $this->assertEquals('tool_dataprivacy', $message->component);
999        $this->assertEquals('contactdataprotectionofficer', $message->eventtype);
1000        $this->assertContains(fullname($dpo), $message->fullmessage);
1001        $this->assertContains(fullname($user1), $message->fullmessage);
1002    }
1003
1004    /**
1005     * Test data purposes CRUD actions.
1006     *
1007     * @return null
1008     */
1009    public function test_purpose_crud() {
1010        $this->resetAfterTest();
1011
1012        $this->setAdminUser();
1013
1014        // Add.
1015        $purpose = api::create_purpose((object)[
1016            'name' => 'bbb',
1017            'description' => '<b>yeah</b>',
1018            'descriptionformat' => 1,
1019            'retentionperiod' => 'PT1M',
1020            'lawfulbases' => 'gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e'
1021        ]);
1022        $this->assertInstanceOf('\tool_dataprivacy\purpose', $purpose);
1023        $this->assertEquals('bbb', $purpose->get('name'));
1024        $this->assertEquals('PT1M', $purpose->get('retentionperiod'));
1025        $this->assertEquals('gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e', $purpose->get('lawfulbases'));
1026
1027        // Update.
1028        $purpose->set('retentionperiod', 'PT2M');
1029        $purpose = api::update_purpose($purpose->to_record());
1030        $this->assertEquals('PT2M', $purpose->get('retentionperiod'));
1031
1032        // Retrieve.
1033        $purpose = api::create_purpose((object)['name' => 'aaa', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a']);
1034        $purposes = api::get_purposes();
1035        $this->assertCount(2, $purposes);
1036        $this->assertEquals('aaa', $purposes[0]->get('name'));
1037        $this->assertEquals('bbb', $purposes[1]->get('name'));
1038
1039        // Delete.
1040        api::delete_purpose($purposes[0]->get('id'));
1041        $this->assertCount(1, api::get_purposes());
1042        api::delete_purpose($purposes[1]->get('id'));
1043        $this->assertCount(0, api::get_purposes());
1044    }
1045
1046    /**
1047     * Test data categories CRUD actions.
1048     *
1049     * @return null
1050     */
1051    public function test_category_crud() {
1052        $this->resetAfterTest();
1053
1054        $this->setAdminUser();
1055
1056        // Add.
1057        $category = api::create_category((object)[
1058            'name' => 'bbb',
1059            'description' => '<b>yeah</b>',
1060            'descriptionformat' => 1
1061        ]);
1062        $this->assertInstanceOf('\tool_dataprivacy\category', $category);
1063        $this->assertEquals('bbb', $category->get('name'));
1064
1065        // Update.
1066        $category->set('name', 'bcd');
1067        $category = api::update_category($category->to_record());
1068        $this->assertEquals('bcd', $category->get('name'));
1069
1070        // Retrieve.
1071        $category = api::create_category((object)['name' => 'aaa']);
1072        $categories = api::get_categories();
1073        $this->assertCount(2, $categories);
1074        $this->assertEquals('aaa', $categories[0]->get('name'));
1075        $this->assertEquals('bcd', $categories[1]->get('name'));
1076
1077        // Delete.
1078        api::delete_category($categories[0]->get('id'));
1079        $this->assertCount(1, api::get_categories());
1080        api::delete_category($categories[1]->get('id'));
1081        $this->assertCount(0, api::get_categories());
1082    }
1083
1084    /**
1085     * Test context instances.
1086     *
1087     * @return null
1088     */
1089    public function test_context_instances() {
1090        global $DB;
1091
1092        $this->resetAfterTest();
1093
1094        $this->setAdminUser();
1095
1096        list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
1097
1098        $coursecontext1 = \context_course::instance($courses[0]->id);
1099        $coursecontext2 = \context_course::instance($courses[1]->id);
1100
1101        $record1 = (object)['contextid' => $coursecontext1->id, 'purposeid' => $purposes[0]->get('id'),
1102            'categoryid' => $categories[0]->get('id')];
1103        $contextinstance1 = api::set_context_instance($record1);
1104
1105        $record2 = (object)['contextid' => $coursecontext2->id, 'purposeid' => $purposes[1]->get('id'),
1106            'categoryid' => $categories[1]->get('id')];
1107        $contextinstance2 = api::set_context_instance($record2);
1108
1109        $this->assertCount(2, $DB->get_records('tool_dataprivacy_ctxinstance'));
1110
1111        api::unset_context_instance($contextinstance1);
1112        $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance'));
1113
1114        $update = (object)['id' => $contextinstance2->get('id'), 'contextid' => $coursecontext2->id,
1115            'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')];
1116        $contextinstance2 = api::set_context_instance($update);
1117        $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance'));
1118    }
1119
1120    /**
1121     * Test contextlevel.
1122     *
1123     * @return null
1124     */
1125    public function test_contextlevel() {
1126        global $DB;
1127
1128        $this->resetAfterTest();
1129
1130        $this->setAdminUser();
1131        list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
1132
1133        $record = (object)[
1134            'purposeid' => $purposes[0]->get('id'),
1135            'categoryid' => $categories[0]->get('id'),
1136            'contextlevel' => CONTEXT_SYSTEM,
1137        ];
1138        $contextlevel = api::set_contextlevel($record);
1139        $this->assertInstanceOf('\tool_dataprivacy\contextlevel', $contextlevel);
1140        $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel'));
1141        $this->assertEquals($record->purposeid, $contextlevel->get('purposeid'));
1142        $this->assertEquals($record->categoryid, $contextlevel->get('categoryid'));
1143
1144        // Now update it.
1145        $record->purposeid = $purposes[1]->get('id');
1146        $contextlevel = api::set_contextlevel($record);
1147        $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel'));
1148        $this->assertEquals($record->purposeid, $contextlevel->get('purposeid'));
1149        $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxlevel'));
1150
1151        $record->contextlevel = CONTEXT_USER;
1152        $contextlevel = api::set_contextlevel($record);
1153        $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxlevel'));
1154    }
1155
1156    /**
1157     * Test effective context levels purpose and category defaults.
1158     *
1159     * @return null
1160     */
1161    public function test_effective_contextlevel_defaults() {
1162        $this->setAdminUser();
1163
1164        $this->resetAfterTest();
1165
1166        list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
1167
1168        list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM);
1169        $this->assertEquals(false, $purposeid);
1170        $this->assertEquals(false, $categoryid);
1171
1172        list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1173            \context_helper::get_class_for_level(CONTEXT_SYSTEM)
1174        );
1175        set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy');
1176
1177        list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM);
1178        $this->assertEquals($purposes[0]->get('id'), $purposeid);
1179        $this->assertEquals(false, $categoryid);
1180
1181        // Course defined values should have preference.
1182        list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1183            \context_helper::get_class_for_level(CONTEXT_COURSE)
1184        );
1185        set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy');
1186        set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy');
1187
1188        list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE);
1189        $this->assertEquals($purposes[1]->get('id'), $purposeid);
1190        $this->assertEquals($categories[0]->get('id'), $categoryid);
1191
1192        // Context level defaults are also allowed to be set to 'inherit'.
1193        set_config($purposevar, context_instance::INHERIT, 'tool_dataprivacy');
1194    }
1195
1196    /**
1197     * Ensure that when nothing is configured, all values return false.
1198     */
1199    public function test_get_effective_contextlevel_unset() {
1200        // Before setup, get_effective_contextlevel_purpose will return false.
1201        $this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_SYSTEM));
1202        $this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM));
1203
1204        $this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_USER));
1205        $this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_USER));
1206    }
1207
1208    /**
1209     * Ensure that when nothing is configured, all values return false.
1210     */
1211    public function test_get_effective_context_unset() {
1212        // Before setup, get_effective_contextlevel_purpose will return false.
1213        $this->assertFalse(api::get_effective_context_category(\context_system::instance()));
1214        $this->assertFalse(api::get_effective_context_purpose(\context_system::instance()));
1215    }
1216
1217    /**
1218     * Ensure that fetching the effective value for context levels is only available to system, and user context levels.
1219     *
1220     * @dataProvider invalid_effective_contextlevel_provider
1221     * @param   int $contextlevel
1222     */
1223    public function test_set_contextlevel_invalid_contextlevels($contextlevel) {
1224
1225        $this->expectException(coding_exception::class);
1226        api::set_contextlevel((object) [
1227                'contextlevel' => $contextlevel,
1228            ]);
1229
1230    }
1231
1232    /**
1233     * Test effective contextlevel return.
1234     */
1235    public function test_effective_contextlevel() {
1236        $this->resetAfterTest();
1237
1238        // Set the initial purpose and category.
1239        $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
1240        $category1 = api::create_category((object)['name' => 'a']);
1241        api::set_contextlevel((object)[
1242            'contextlevel' => CONTEXT_SYSTEM,
1243            'purposeid' => $purpose1->get('id'),
1244            'categoryid' => $category1->get('id'),
1245        ]);
1246
1247        $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM));
1248        $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_SYSTEM));
1249
1250        // The user context inherits from the system context when not set.
1251        $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_USER));
1252        $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_USER));
1253
1254        // Forcing the behaviour to inherit will have the same result.
1255        api::set_contextlevel((object) [
1256                'contextlevel' => CONTEXT_USER,
1257                'purposeid' => context_instance::INHERIT,
1258                'categoryid' => context_instance::INHERIT,
1259            ]);
1260        $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_USER));
1261        $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_USER));
1262
1263        // Setting specific values will override the inheritance behaviour.
1264        $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_a']);
1265        $category2 = api::create_category((object)['name' => 'b']);
1266        // Set the system context level to purpose 1.
1267        api::set_contextlevel((object) [
1268                'contextlevel' => CONTEXT_USER,
1269                'purposeid' => $purpose2->get('id'),
1270                'categoryid' => $category2->get('id'),
1271            ]);
1272
1273        $this->assertEquals($purpose2, api::get_effective_contextlevel_purpose(CONTEXT_USER));
1274        $this->assertEquals($category2, api::get_effective_contextlevel_category(CONTEXT_USER));
1275    }
1276
1277    /**
1278     * Ensure that fetching the effective value for context levels is only available to system, and user context levels.
1279     *
1280     * @dataProvider invalid_effective_contextlevel_provider
1281     * @param   int $contextlevel
1282     */
1283    public function test_effective_contextlevel_invalid_contextlevels($contextlevel) {
1284        $this->resetAfterTest();
1285
1286        $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
1287        $category1 = api::create_category((object)['name' => 'a']);
1288        api::set_contextlevel((object)[
1289            'contextlevel' => CONTEXT_SYSTEM,
1290            'purposeid' => $purpose1->get('id'),
1291            'categoryid' => $category1->get('id'),
1292        ]);
1293
1294        $this->expectException(coding_exception::class);
1295        api::get_effective_contextlevel_purpose($contextlevel);
1296    }
1297
1298    /**
1299     * Data provider for invalid contextlevel fetchers.
1300     */
1301    public function invalid_effective_contextlevel_provider() {
1302        return [
1303            [CONTEXT_COURSECAT],
1304            [CONTEXT_COURSE],
1305            [CONTEXT_MODULE],
1306            [CONTEXT_BLOCK],
1307        ];
1308    }
1309
1310    /**
1311     * Ensure that context inheritance works up the context tree.
1312     */
1313    public function test_effective_context_inheritance() {
1314        $this->resetAfterTest();
1315
1316        $systemdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_SYSTEM);
1317
1318        /*
1319         * System
1320         * - Cat
1321         *   - Subcat
1322         *     - Course
1323         *       - Forum
1324         * - User
1325         *   - User block
1326         */
1327        $cat = $this->getDataGenerator()->create_category();
1328        $subcat = $this->getDataGenerator()->create_category(['parent' => $cat->id]);
1329        $course = $this->getDataGenerator()->create_course(['category' => $subcat->id]);
1330        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1331        list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
1332
1333        $user = $this->getDataGenerator()->create_user();
1334
1335        $contextsystem = \context_system::instance();
1336        $contextcat = \context_coursecat::instance($cat->id);
1337        $contextsubcat = \context_coursecat::instance($subcat->id);
1338        $contextcourse = \context_course::instance($course->id);
1339        $contextforum = \context_module::instance($forumcm->id);
1340        $contextuser = \context_user::instance($user->id);
1341
1342        // Initially everything is set to Inherit.
1343        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1344        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
1345        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1"));
1346        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "0"));
1347        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
1348        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "-1"));
1349        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "0"));
1350        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
1351        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "-1"));
1352        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "0"));
1353        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
1354        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "-1"));
1355        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "0"));
1356        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser));
1357        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "-1"));
1358        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "0"));
1359
1360        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1361        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
1362        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1"));
1363        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "0"));
1364        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
1365        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "-1"));
1366        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "0"));
1367        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
1368        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "-1"));
1369        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "0"));
1370        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
1371        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "-1"));
1372        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "0"));
1373        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextuser));
1374        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextuser, "-1"));
1375        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextuser, "0"));
1376
1377        // When actively set, user will use the specified value.
1378        $userdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_USER);
1379
1380        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1381        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
1382        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1"));
1383        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "0"));
1384        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
1385        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "-1"));
1386        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "0"));
1387        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
1388        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "-1"));
1389        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "0"));
1390        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
1391        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "-1"));
1392        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "0"));
1393        $this->assertEquals($userdata->purpose, api::get_effective_context_purpose($contextuser));
1394        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "-1"));
1395
1396        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1397        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
1398        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1"));
1399        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "0"));
1400        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
1401        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "-1"));
1402        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "0"));
1403        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
1404        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "-1"));
1405        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "0"));
1406        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
1407        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "-1"));
1408        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "0"));
1409        $this->assertEquals($userdata->category, api::get_effective_context_category($contextuser));
1410        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "-1"));
1411
1412        // Set a context for the top category.
1413        $catpurpose = new purpose(0, (object) [
1414                'name' => 'Purpose',
1415                'retentionperiod' => 'P1D',
1416                'lawfulbases' => 'gdpr_art_6_1_a',
1417            ]);
1418        $catpurpose->save();
1419        $catcategory = new category(0, (object) ['name' => 'Category']);
1420        $catcategory->save();
1421        api::set_context_instance((object) [
1422                'contextid' => $contextcat->id,
1423                'purposeid' => $catpurpose->get('id'),
1424                'categoryid' => $catcategory->get('id'),
1425            ]);
1426
1427        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1428        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
1429        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1"));
1430        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0"));
1431        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat));
1432        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1"));
1433        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "0"));
1434        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcourse));
1435        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcourse, "-1"));
1436        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcourse, "0"));
1437        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum));
1438        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum, "-1"));
1439        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum, "0"));
1440        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum, "0"));
1441
1442        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1443        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
1444        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1"));
1445        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0"));
1446        $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat));
1447        $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1"));
1448        $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "0"));
1449        $this->assertEquals($catcategory, api::get_effective_context_category($contextcourse));
1450        $this->assertEquals($catcategory, api::get_effective_context_category($contextcourse, "-1"));
1451        $this->assertEquals($catcategory, api::get_effective_context_category($contextcourse, "0"));
1452        $this->assertEquals($catcategory, api::get_effective_context_category($contextforum));
1453        $this->assertEquals($catcategory, api::get_effective_context_category($contextforum, "-1"));
1454        $this->assertEquals($catcategory, api::get_effective_context_category($contextforum, "0"));
1455
1456        // Set a context for the sub category.
1457        $subcatpurpose = new purpose(0, (object) [
1458                'name' => 'Purpose',
1459                'retentionperiod' => 'P1D',
1460                'lawfulbases' => 'gdpr_art_6_1_a',
1461            ]);
1462        $subcatpurpose->save();
1463        $subcatcategory = new category(0, (object) ['name' => 'Category']);
1464        $subcatcategory->save();
1465        api::set_context_instance((object) [
1466                'contextid' => $contextsubcat->id,
1467                'purposeid' => $subcatpurpose->get('id'),
1468                'categoryid' => $subcatcategory->get('id'),
1469            ]);
1470
1471        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1472        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
1473        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1"));
1474        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0"));
1475        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat));
1476        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1"));
1477        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat, "0"));
1478        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse));
1479        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "-1"));
1480        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "0"));
1481        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextforum));
1482        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextforum, "-1"));
1483        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextforum, "0"));
1484
1485        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1486        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
1487        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1"));
1488        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0"));
1489        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat));
1490        $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1"));
1491        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat, "0"));
1492        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse));
1493        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "-1"));
1494        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "0"));
1495        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextforum));
1496        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextforum, "-1"));
1497        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextforum, "0"));
1498
1499        // Set a context for the course.
1500        $coursepurpose = new purpose(0, (object) [
1501                'name' => 'Purpose',
1502                'retentionperiod' => 'P1D',
1503                'lawfulbases' => 'gdpr_art_6_1_a',
1504            ]);
1505        $coursepurpose->save();
1506        $coursecategory = new category(0, (object) ['name' => 'Category']);
1507        $coursecategory->save();
1508        api::set_context_instance((object) [
1509                'contextid' => $contextcourse->id,
1510                'purposeid' => $coursepurpose->get('id'),
1511                'categoryid' => $coursecategory->get('id'),
1512            ]);
1513
1514        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1515        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
1516        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1"));
1517        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0"));
1518        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat));
1519        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1"));
1520        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat, "0"));
1521        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse));
1522        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "-1"));
1523        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse, "0"));
1524        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum));
1525        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum, "-1"));
1526        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum, "0"));
1527
1528        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1529        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
1530        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1"));
1531        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0"));
1532        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat));
1533        $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1"));
1534        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat, "0"));
1535        $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse));
1536        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "-1"));
1537        $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse, "0"));
1538        $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum));
1539        $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum, "-1"));
1540        $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum, "0"));
1541
1542        // Set a context for the forum.
1543        $forumpurpose = new purpose(0, (object) [
1544                'name' => 'Purpose',
1545                'retentionperiod' => 'P1D',
1546                'lawfulbases' => 'gdpr_art_6_1_a',
1547            ]);
1548        $forumpurpose->save();
1549        $forumcategory = new category(0, (object) ['name' => 'Category']);
1550        $forumcategory->save();
1551        api::set_context_instance((object) [
1552                'contextid' => $contextforum->id,
1553                'purposeid' => $forumpurpose->get('id'),
1554                'categoryid' => $forumcategory->get('id'),
1555            ]);
1556
1557        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1558        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
1559        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1"));
1560        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0"));
1561        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat));
1562        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1"));
1563        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat, "0"));
1564        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse));
1565        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "-1"));
1566        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse, "0"));
1567        $this->assertEquals($forumpurpose, api::get_effective_context_purpose($contextforum));
1568        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum, "-1"));
1569        $this->assertEquals($forumpurpose, api::get_effective_context_purpose($contextforum, "0"));
1570
1571        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1572        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
1573        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1"));
1574        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0"));
1575        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat));
1576        $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1"));
1577        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat, "0"));
1578        $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse));
1579        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "-1"));
1580        $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse, "0"));
1581        $this->assertEquals($forumcategory, api::get_effective_context_category($contextforum));
1582        $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum, "-1"));
1583        $this->assertEquals($forumcategory, api::get_effective_context_category($contextforum, "0"));
1584    }
1585
1586    /**
1587     * Ensure that context inheritance works up the context tree when inherit values are explicitly set at the
1588     * contextlevel.
1589     *
1590     * Although it should not be possible to set hard INHERIT values at this level, there may be legacy data which still
1591     * contains this.
1592     */
1593    public function test_effective_context_inheritance_explicitly_set() {
1594        $this->resetAfterTest();
1595
1596        $systemdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_SYSTEM);
1597
1598        /*
1599         * System
1600         * - Cat
1601         *   - Subcat
1602         *     - Course
1603         *       - Forum
1604         * - User
1605         *   - User block
1606         */
1607        $cat = $this->getDataGenerator()->create_category();
1608        $subcat = $this->getDataGenerator()->create_category(['parent' => $cat->id]);
1609        $course = $this->getDataGenerator()->create_course(['category' => $subcat->id]);
1610        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1611        list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
1612
1613        $contextsystem = \context_system::instance();
1614        $contextcat = \context_coursecat::instance($cat->id);
1615        $contextsubcat = \context_coursecat::instance($subcat->id);
1616        $contextcourse = \context_course::instance($course->id);
1617        $contextforum = \context_module::instance($forumcm->id);
1618
1619        // Initially everything is set to Inherit.
1620        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1621        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
1622        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
1623        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
1624        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
1625
1626        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1627        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
1628        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
1629        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
1630        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
1631
1632        // Set a default value of inherit for CONTEXT_COURSECAT.
1633        $classname = \context_helper::get_class_for_level(CONTEXT_COURSECAT);
1634        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
1635        set_config($purposevar, '-1', 'tool_dataprivacy');
1636        set_config($categoryvar, '-1', 'tool_dataprivacy');
1637
1638        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1639        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
1640        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
1641        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
1642        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
1643
1644        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1645        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
1646        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
1647        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
1648        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
1649
1650        // Set a default value of inherit for CONTEXT_COURSE.
1651        $classname = \context_helper::get_class_for_level(CONTEXT_COURSE);
1652        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
1653        set_config($purposevar, '-1', 'tool_dataprivacy');
1654        set_config($categoryvar, '-1', 'tool_dataprivacy');
1655
1656        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1657        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
1658        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
1659        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
1660        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
1661
1662        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1663        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
1664        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
1665        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
1666        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
1667
1668        // Set a default value of inherit for CONTEXT_MODULE.
1669        $classname = \context_helper::get_class_for_level(CONTEXT_MODULE);
1670        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
1671        set_config($purposevar, '-1', 'tool_dataprivacy');
1672        set_config($categoryvar, '-1', 'tool_dataprivacy');
1673
1674        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
1675        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
1676        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
1677        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
1678        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
1679
1680        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
1681        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
1682        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
1683        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
1684        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
1685    }
1686
1687    /**
1688     * Creates test purposes and categories.
1689     *
1690     * @return null
1691     */
1692    protected function add_purposes_and_categories() {
1693        $this->resetAfterTest();
1694
1695        $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
1696        $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_b']);
1697        $purpose3 = api::create_purpose((object)['name' => 'p3', 'retentionperiod' => 'PT3H', 'lawfulbases' => 'gdpr_art_6_1_c']);
1698
1699        $cat1 = api::create_category((object)['name' => 'a']);
1700        $cat2 = api::create_category((object)['name' => 'b']);
1701        $cat3 = api::create_category((object)['name' => 'c']);
1702
1703        $course1 = $this->getDataGenerator()->create_course();
1704        $course2 = $this->getDataGenerator()->create_course();
1705
1706        $module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1));
1707        $module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2));
1708
1709        return [
1710            [$purpose1, $purpose2, $purpose3],
1711            [$cat1, $cat2, $cat3],
1712            [$course1, $course2],
1713            [$module1, $module2]
1714        ];
1715    }
1716
1717    /**
1718     * Test that delete requests do not filter out protected purpose contexts if the the site is properly configured.
1719     */
1720    public function test_get_approved_contextlist_collection_for_collection_delete_course_no_site_config() {
1721        $this->resetAfterTest();
1722
1723        $user = $this->getDataGenerator()->create_user();
1724
1725        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
1726        $coursecontext = \context_course::instance($course->id);
1727
1728        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1729        list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
1730        $contextforum = \context_module::instance($forumcm->id);
1731
1732        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
1733
1734        // Create the initial contextlist.
1735        $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
1736
1737        $contextlist = new \core_privacy\local\request\contextlist();
1738        $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]);
1739        $contextlist->set_component('tool_dataprivacy');
1740        $initialcollection->add_contextlist($contextlist);
1741
1742        $contextlist = new \core_privacy\local\request\contextlist();
1743        $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $contextforum->id]);
1744        $contextlist->set_component('mod_forum');
1745        $initialcollection->add_contextlist($contextlist);
1746
1747        $collection = api::get_approved_contextlist_collection_for_collection(
1748                $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE);
1749
1750        $this->assertCount(2, $collection);
1751
1752        $list = $collection->get_contextlist_for_component('tool_dataprivacy');
1753        $this->assertCount(1, $list);
1754
1755        $list = $collection->get_contextlist_for_component('mod_forum');
1756        $this->assertCount(1, $list);
1757    }
1758
1759    /**
1760     * Test that delete requests do not filter out protected purpose contexts if they are already expired.
1761     */
1762    public function test_get_approved_contextlist_collection_for_collection_delete_course_expired_protected() {
1763        $this->resetAfterTest();
1764
1765        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
1766        $purposes->course->purpose->set('protected', 1)->save();
1767
1768        $user = $this->getDataGenerator()->create_user();
1769        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
1770        $coursecontext = \context_course::instance($course->id);
1771
1772        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
1773
1774        // Create the initial contextlist.
1775        $contextlist = new \core_privacy\local\request\contextlist();
1776        $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]);
1777        $contextlist->set_component('tool_dataprivacy');
1778
1779        $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
1780        $initialcollection->add_contextlist($contextlist);
1781
1782        $purposes->course->purpose->set('protected', 1)->save();
1783        $collection = api::get_approved_contextlist_collection_for_collection(
1784                $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE);
1785
1786        $this->assertCount(1, $collection);
1787
1788        $list = $collection->get_contextlist_for_component('tool_dataprivacy');
1789        $this->assertCount(1, $list);
1790    }
1791
1792    /**
1793     * Test that delete requests does filter out protected purpose contexts which are not expired.
1794     */
1795    public function test_get_approved_contextlist_collection_for_collection_delete_course_unexpired_protected() {
1796        $this->resetAfterTest();
1797
1798        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
1799        $purposes->course->purpose->set('protected', 1)->save();
1800
1801        $user = $this->getDataGenerator()->create_user();
1802        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
1803        $coursecontext = \context_course::instance($course->id);
1804
1805        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
1806
1807        // Create the initial contextlist.
1808        $contextlist = new \core_privacy\local\request\contextlist();
1809        $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]);
1810        $contextlist->set_component('tool_dataprivacy');
1811
1812        $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
1813        $initialcollection->add_contextlist($contextlist);
1814
1815        $purposes->course->purpose->set('protected', 1)->save();
1816        $collection = api::get_approved_contextlist_collection_for_collection(
1817                $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE);
1818
1819        $this->assertCount(0, $collection);
1820
1821        $list = $collection->get_contextlist_for_component('tool_dataprivacy');
1822        $this->assertEmpty($list);
1823    }
1824
1825    /**
1826     * Test that delete requests do not filter out unexpired contexts if they are not protected.
1827     */
1828    public function test_get_approved_contextlist_collection_for_collection_delete_course_unexpired_unprotected() {
1829        $this->resetAfterTest();
1830
1831        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
1832        $purposes->course->purpose->set('protected', 1)->save();
1833
1834        $user = $this->getDataGenerator()->create_user();
1835        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
1836        $coursecontext = \context_course::instance($course->id);
1837
1838        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
1839
1840        // Create the initial contextlist.
1841        $contextlist = new \core_privacy\local\request\contextlist();
1842        $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]);
1843        $contextlist->set_component('tool_dataprivacy');
1844
1845        $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
1846        $initialcollection->add_contextlist($contextlist);
1847
1848        $purposes->course->purpose->set('protected', 0)->save();
1849        $collection = api::get_approved_contextlist_collection_for_collection(
1850                $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE);
1851
1852        $this->assertCount(1, $collection);
1853
1854        $list = $collection->get_contextlist_for_component('tool_dataprivacy');
1855        $this->assertCount(1, $list);
1856    }
1857
1858    /**
1859     * Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults
1860     */
1861    public function set_context_defaults_provider() {
1862        $contextlevels = [
1863            [CONTEXT_COURSECAT],
1864            [CONTEXT_COURSE],
1865            [CONTEXT_MODULE],
1866            [CONTEXT_BLOCK],
1867        ];
1868        $paramsets = [
1869            [true, true, false, false], // Inherit category and purpose, Not for activity, Don't override.
1870            [true, false, false, false], // Inherit category but not purpose, Not for activity, Don't override.
1871            [false, true, false, false], // Inherit purpose but not category, Not for activity, Don't override.
1872            [false, false, false, false], // Don't inherit both category and purpose, Not for activity, Don't override.
1873            [false, false, false, true], // Don't inherit both category and purpose, Not for activity, Override instances.
1874        ];
1875        $data = [];
1876        foreach ($contextlevels as $level) {
1877            foreach ($paramsets as $set) {
1878                $data[] = array_merge($level, $set);
1879            }
1880            if ($level == CONTEXT_MODULE) {
1881                // Add a combination where defaults for activity is being set.
1882                $data[] = [CONTEXT_MODULE, false, false, true, false];
1883                $data[] = [CONTEXT_MODULE, false, false, true, true];
1884            }
1885        }
1886        return $data;
1887    }
1888
1889    /**
1890     * Test for \tool_dataprivacy\api::set_context_defaults()
1891     *
1892     * @dataProvider set_context_defaults_provider
1893     * @param int $contextlevel The context level
1894     * @param bool $inheritcategory Whether to set category value as INHERIT.
1895     * @param bool $inheritpurpose Whether to set purpose value as INHERIT.
1896     * @param bool $foractivity Whether to set defaults for an activity.
1897     * @param bool $override Whether to override instances.
1898     */
1899    public function test_set_context_defaults($contextlevel, $inheritcategory, $inheritpurpose, $foractivity, $override) {
1900        $this->resetAfterTest();
1901
1902        $generator = $this->getDataGenerator();
1903
1904        // Generate course cat, course, block, assignment, forum instances.
1905        $coursecat = $generator->create_category();
1906        $course = $generator->create_course(['category' => $coursecat->id]);
1907        $block = $generator->create_block('online_users');
1908        $assign = $generator->create_module('assign', ['course' => $course->id]);
1909        $forum = $generator->create_module('forum', ['course' => $course->id]);
1910
1911        $coursecatcontext = context_coursecat::instance($coursecat->id);
1912        $coursecontext = context_course::instance($course->id);
1913        $blockcontext = context_block::instance($block->id);
1914
1915        list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
1916        list($course, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
1917        $assigncontext = context_module::instance($assigncm->id);
1918        $forumcontext = context_module::instance($forumcm->id);
1919
1920        // Generate purposes and categories.
1921        $category1 = api::create_category((object)['name' => 'Test category 1']);
1922        $category2 = api::create_category((object)['name' => 'Test category 2']);
1923        $purpose1 = api::create_purpose((object)[
1924            'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
1925        ]);
1926        $purpose2 = api::create_purpose((object)[
1927            'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
1928        ]);
1929
1930        // Assign purposes and categories to contexts.
1931        $coursecatctxinstance = api::set_context_instance((object) [
1932            'contextid' => $coursecatcontext->id,
1933            'purposeid' => $purpose1->get('id'),
1934            'categoryid' => $category1->get('id'),
1935        ]);
1936        $coursectxinstance = api::set_context_instance((object) [
1937            'contextid' => $coursecontext->id,
1938            'purposeid' => $purpose1->get('id'),
1939            'categoryid' => $category1->get('id'),
1940        ]);
1941        $blockctxinstance = api::set_context_instance((object) [
1942            'contextid' => $blockcontext->id,
1943            'purposeid' => $purpose1->get('id'),
1944            'categoryid' => $category1->get('id'),
1945        ]);
1946        $assignctxinstance = api::set_context_instance((object) [
1947            'contextid' => $assigncontext->id,
1948            'purposeid' => $purpose1->get('id'),
1949            'categoryid' => $category1->get('id'),
1950        ]);
1951        $forumctxinstance = api::set_context_instance((object) [
1952            'contextid' => $forumcontext->id,
1953            'purposeid' => $purpose1->get('id'),
1954            'categoryid' => $category1->get('id'),
1955        ]);
1956
1957        $categoryid = $inheritcategory ? context_instance::INHERIT : $category2->get('id');
1958        $purposeid = $inheritpurpose ? context_instance::INHERIT : $purpose2->get('id');
1959        $activity = '';
1960        if ($contextlevel == CONTEXT_MODULE && $foractivity) {
1961            $activity = 'assign';
1962        }
1963        $result = api::set_context_defaults($contextlevel, $categoryid, $purposeid, $activity, $override);
1964        $this->assertTrue($result);
1965
1966        $targetctxinstance = false;
1967        switch ($contextlevel) {
1968            case CONTEXT_COURSECAT:
1969                $targetctxinstance = $coursecatctxinstance;
1970                break;
1971            case CONTEXT_COURSE:
1972                $targetctxinstance = $coursectxinstance;
1973                break;
1974            case CONTEXT_MODULE:
1975                $targetctxinstance = $assignctxinstance;
1976                break;
1977            case CONTEXT_BLOCK:
1978                $targetctxinstance = $blockctxinstance;
1979                break;
1980        }
1981        $this->assertNotFalse($targetctxinstance);
1982
1983        // Check the context instances.
1984        $instanceexists = context_instance::record_exists($targetctxinstance->get('id'));
1985        if ($override) {
1986            // If overridden, context instances on this context level would have been deleted.
1987            $this->assertFalse($instanceexists);
1988
1989            // Check forum context instance.
1990            $forumctxexists = context_instance::record_exists($forumctxinstance->get('id'));
1991            if ($contextlevel != CONTEXT_MODULE || $foractivity) {
1992                // The forum context instance won't be affected in this test if:
1993                // - The overridden defaults are not for context modules.
1994                // - Only the defaults for assign have been set.
1995                $this->assertTrue($forumctxexists);
1996            } else {
1997                // If we're overriding for the whole course module context level,
1998                // then this forum context instance will be deleted as well.
1999                $this->assertFalse($forumctxexists);
2000            }
2001        } else {
2002            // Otherwise, the context instance record remains.
2003            $this->assertTrue($instanceexists);
2004        }
2005
2006        // Check defaults.
2007        list($defaultpurpose, $defaultcategory) = data_registry::get_defaults($contextlevel, $activity);
2008        if (!$inheritpurpose) {
2009            $this->assertEquals($purposeid, $defaultpurpose);
2010        }
2011        if (!$inheritcategory) {
2012            $this->assertEquals($categoryid, $defaultcategory);
2013        }
2014    }
2015
2016    /**
2017     * Setup the basics with the specified retention period.
2018     *
2019     * @param   string  $system Retention policy for the system.
2020     * @param   string  $user Retention policy for users.
2021     * @param   string  $course Retention policy for courses.
2022     * @param   string  $activity Retention policy for activities.
2023     */
2024    protected function setup_basics(string $system, string $user, string $course = null, string $activity = null) : \stdClass {
2025        $this->resetAfterTest();
2026
2027        $purposes = (object) [
2028            'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM),
2029            'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER),
2030        ];
2031
2032        if (null !== $course) {
2033            $purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
2034        }
2035
2036        if (null !== $activity) {
2037            $purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
2038        }
2039
2040        return $purposes;
2041    }
2042
2043    /**
2044     * Create a retention period and set it for the specified context level.
2045     *
2046     * @param   string  $retention
2047     * @param   int     $contextlevel
2048     */
2049    protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) {
2050        $purpose = new purpose(0, (object) [
2051            'name' => 'Test purpose ' . rand(1, 1000),
2052            'retentionperiod' => $retention,
2053            'lawfulbases' => 'gdpr_art_6_1_a',
2054        ]);
2055        $purpose->create();
2056
2057        $cat = new category(0, (object) ['name' => 'Test category']);
2058        $cat->create();
2059
2060        if ($contextlevel <= CONTEXT_USER) {
2061            $record = (object) [
2062                'purposeid'     => $purpose->get('id'),
2063                'categoryid'    => $cat->get('id'),
2064                'contextlevel'  => $contextlevel,
2065            ];
2066            api::set_contextlevel($record);
2067        } else {
2068            list($purposevar, ) = data_registry::var_names_from_context(
2069                    \context_helper::get_class_for_level(CONTEXT_COURSE)
2070                );
2071            set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
2072        }
2073
2074        return (object) [
2075            'purpose' => $purpose,
2076            'category' => $cat,
2077        ];
2078    }
2079
2080    /**
2081     * Ensure that the find_ongoing_request_types_for_users only returns requests which are active.
2082     */
2083    public function test_find_ongoing_request_types_for_users() {
2084        $this->resetAfterTest();
2085
2086        // Create users and their requests:.
2087        // - u1 has no requests of any type.
2088        // - u2 has one rejected export request.
2089        // - u3 has one rejected other request.
2090        // - u4 has one rejected delete request.
2091        // - u5 has one active and one rejected export request.
2092        // - u6 has one active and one rejected other request.
2093        // - u7 has one active and one rejected delete request.
2094        // - u8 has one active export, and one active delete request.
2095        $u1 = $this->getDataGenerator()->create_user();
2096        $u1expect = (object) [];
2097
2098        $u2 = $this->getDataGenerator()->create_user();
2099        $this->create_request_with_type_and_status($u2->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_REJECTED);
2100        $u2expect = (object) [];
2101
2102        $u3 = $this->getDataGenerator()->create_user();
2103        $this->create_request_with_type_and_status($u3->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_REJECTED);
2104        $u3expect = (object) [];
2105
2106        $u4 = $this->getDataGenerator()->create_user();
2107        $this->create_request_with_type_and_status($u4->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_REJECTED);
2108        $u4expect = (object) [];
2109
2110        $u5 = $this->getDataGenerator()->create_user();
2111        $this->create_request_with_type_and_status($u5->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_REJECTED);
2112        $this->create_request_with_type_and_status($u5->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_APPROVED);
2113        $u5expect = (object) [
2114            api::DATAREQUEST_TYPE_EXPORT => true,
2115        ];
2116
2117        $u6 = $this->getDataGenerator()->create_user();
2118        $this->create_request_with_type_and_status($u6->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_REJECTED);
2119        $this->create_request_with_type_and_status($u6->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_APPROVED);
2120        $u6expect = (object) [
2121            api::DATAREQUEST_TYPE_OTHERS => true,
2122        ];
2123
2124        $u7 = $this->getDataGenerator()->create_user();
2125        $this->create_request_with_type_and_status($u7->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_REJECTED);
2126        $this->create_request_with_type_and_status($u7->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_APPROVED);
2127        $u7expect = (object) [
2128            api::DATAREQUEST_TYPE_DELETE => true,
2129        ];
2130
2131        $u8 = $this->getDataGenerator()->create_user();
2132        $this->create_request_with_type_and_status($u8->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_APPROVED);
2133        $this->create_request_with_type_and_status($u8->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_APPROVED);
2134        $u8expect = (object) [
2135            api::DATAREQUEST_TYPE_EXPORT => true,
2136            api::DATAREQUEST_TYPE_DELETE => true,
2137        ];
2138
2139        // Test with no users specified.
2140        $result = api::find_ongoing_request_types_for_users([]);
2141        $this->assertEquals([], $result);
2142
2143        // Fetch a subset of the users.
2144        $result = api::find_ongoing_request_types_for_users([$u3->id, $u4->id, $u5->id]);
2145        $this->assertEquals([
2146                $u3->id => $u3expect,
2147                $u4->id => $u4expect,
2148                $u5->id => $u5expect,
2149            ], $result);
2150
2151        // Fetch the empty user.
2152        $result = api::find_ongoing_request_types_for_users([$u1->id]);
2153        $this->assertEquals([
2154                $u1->id => $u1expect,
2155            ], $result);
2156
2157        // Fetch all.
2158        $result = api::find_ongoing_request_types_for_users(
2159            [$u1->id, $u2->id, $u3->id, $u4->id, $u5->id, $u6->id, $u7->id, $u8->id]);
2160        $this->assertEquals([
2161                $u1->id => $u1expect,
2162                $u2->id => $u2expect,
2163                $u3->id => $u3expect,
2164                $u4->id => $u4expect,
2165                $u5->id => $u5expect,
2166                $u6->id => $u6expect,
2167                $u7->id => $u7expect,
2168                $u8->id => $u8expect,
2169            ], $result);
2170    }
2171
2172    /**
2173     * Create  a new data request for the user with the type and status specified.
2174     *
2175     * @param   int     $userid
2176     * @param   int     $type
2177     * @param   int     $status
2178     * @return  \tool_dataprivacy\data_request
2179     */
2180    protected function create_request_with_type_and_status(int $userid, int $type, int $status) : \tool_dataprivacy\data_request {
2181        $request = new \tool_dataprivacy\data_request(0, (object) [
2182            'userid' => $userid,
2183            'type' => $type,
2184            'status' => $status,
2185        ]);
2186
2187        $request->save();
2188
2189        return $request;
2190    }
2191
2192    /**
2193     * Test user cannot create data deletion request for themselves if they don't have
2194     * "tool/dataprivacy:requestdelete" capability.
2195     *
2196     * @throws coding_exception
2197     */
2198    public function test_can_create_data_deletion_request_for_self_no() {
2199        $this->resetAfterTest();
2200        $userid = $this->getDataGenerator()->create_user()->id;
2201        $roleid = $this->getDataGenerator()->create_role();
2202        assign_capability('tool/dataprivacy:requestdelete', CAP_PROHIBIT, $roleid, context_user::instance($userid));
2203        role_assign($roleid, $userid, context_user::instance($userid));
2204        $this->setUser($userid);
2205        $this->assertFalse(api::can_create_data_deletion_request_for_self());
2206    }
2207
2208    /**
2209     * Test primary admin cannot create data deletion request for themselves
2210     */
2211    public function test_can_create_data_deletion_request_for_self_primary_admin() {
2212        $this->resetAfterTest();
2213        $this->setAdminUser();
2214        $this->assertFalse(api::can_create_data_deletion_request_for_self());
2215    }
2216
2217    /**
2218     * Test secondary admin can create data deletion request for themselves
2219     */
2220    public function test_can_create_data_deletion_request_for_self_secondary_admin() {
2221        $this->resetAfterTest();
2222
2223        $admin1 = $this->getDataGenerator()->create_user();
2224        $admin2 = $this->getDataGenerator()->create_user();
2225
2226        // The primary admin is the one listed first in the 'siteadmins' config.
2227        set_config('siteadmins', implode(',', [$admin1->id, $admin2->id]));
2228
2229        // Set the current user as the second admin (non-primary).
2230        $this->setUser($admin2);
2231
2232        $this->assertTrue(api::can_create_data_deletion_request_for_self());
2233    }
2234
2235    /**
2236     * Test user can create data deletion request for themselves if they have
2237     * "tool/dataprivacy:requestdelete" capability.
2238     *
2239     * @throws coding_exception
2240     */
2241    public function test_can_create_data_deletion_request_for_self_yes() {
2242        $this->resetAfterTest();
2243        $userid = $this->getDataGenerator()->create_user()->id;
2244        $this->setUser($userid);
2245        $this->assertTrue(api::can_create_data_deletion_request_for_self());
2246    }
2247
2248    /**
2249     * Test user cannot create data deletion request for another user if they
2250     * don't have "tool/dataprivacy:requestdeleteforotheruser" capability.
2251     *
2252     * @throws coding_exception
2253     * @throws dml_exception
2254     */
2255    public function test_can_create_data_deletion_request_for_other_no() {
2256        $this->resetAfterTest();
2257        $userid = $this->getDataGenerator()->create_user()->id;
2258        $this->setUser($userid);
2259        $this->assertFalse(api::can_create_data_deletion_request_for_other());
2260    }
2261
2262    /**
2263     * Test user can create data deletion request for another user if they
2264     * don't have "tool/dataprivacy:requestdeleteforotheruser" capability.
2265     *
2266     * @throws coding_exception
2267     */
2268    public function test_can_create_data_deletion_request_for_other_yes() {
2269        $this->resetAfterTest();
2270        $userid = $this->getDataGenerator()->create_user()->id;
2271        $roleid = $this->getDataGenerator()->create_role();
2272        $contextsystem = context_system::instance();
2273        assign_capability('tool/dataprivacy:requestdeleteforotheruser', CAP_ALLOW, $roleid, $contextsystem);
2274        role_assign($roleid, $userid, $contextsystem);
2275        $this->setUser($userid);
2276        $this->assertTrue(api::can_create_data_deletion_request_for_other($userid));
2277    }
2278
2279    /**
2280     * Check parents can create data deletion request for their children (unless the child is the primary admin),
2281     * but not other users.
2282     *
2283     * @throws coding_exception
2284     * @throws dml_exception
2285     */
2286    public function test_can_create_data_deletion_request_for_children() {
2287        $this->resetAfterTest();
2288
2289        $parent = $this->getDataGenerator()->create_user();
2290        $child = $this->getDataGenerator()->create_user();
2291        $otheruser = $this->getDataGenerator()->create_user();
2292
2293        $contextsystem = \context_system::instance();
2294        $parentrole = $this->getDataGenerator()->create_role();
2295        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW,
2296            $parentrole, $contextsystem);
2297        assign_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', CAP_ALLOW,
2298            $parentrole, $contextsystem);
2299        role_assign($parentrole, $parent->id, \context_user::instance($child->id));
2300
2301        $this->setUser($parent);
2302        $this->assertTrue(api::can_create_data_deletion_request_for_children($child->id));
2303        $this->assertFalse(api::can_create_data_deletion_request_for_children($otheruser->id));
2304
2305        // Now make child the primary admin, confirm parent can't make deletion request.
2306        set_config('siteadmins', $child->id);
2307        $this->assertFalse(api::can_create_data_deletion_request_for_children($child->id));
2308    }
2309
2310    /**
2311     * Data provider function for testing \tool_dataprivacy\api::queue_data_request_task().
2312     *
2313     * @return array
2314     */
2315    public function queue_data_request_task_provider() {
2316        return [
2317            'With user ID provided' => [true],
2318            'Without user ID provided' => [false],
2319        ];
2320    }
2321
2322    /**
2323     * Test for \tool_dataprivacy\api::queue_data_request_task().
2324     *
2325     * @dataProvider queue_data_request_task_provider
2326     * @param bool $withuserid
2327     */
2328    public function test_queue_data_request_task(bool $withuserid) {
2329        $this->resetAfterTest();
2330
2331        $this->setAdminUser();
2332
2333        if ($withuserid) {
2334            $user = $this->getDataGenerator()->create_user();
2335            api::queue_data_request_task(1, $user->id);
2336            $expecteduserid = $user->id;
2337        } else {
2338            api::queue_data_request_task(1);
2339            $expecteduserid = null;
2340        }
2341
2342        // Test number of queued data request tasks.
2343        $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class);
2344        $this->assertCount(1, $datarequesttasks);
2345        $requesttask = reset($datarequesttasks);
2346        $this->assertEquals($expecteduserid, $requesttask->get_userid());
2347    }
2348
2349    /**
2350     * Data provider for test_is_automatic_request_approval_on().
2351     */
2352    public function automatic_request_approval_setting_provider() {
2353        return [
2354            'Data export, not set' => [
2355                'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, null, false
2356            ],
2357            'Data export, turned on' => [
2358                'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, true, true
2359            ],
2360            'Data export, turned off' => [
2361                'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, false, false
2362            ],
2363            'Data deletion, not set' => [
2364                'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, null, false
2365            ],
2366            'Data deletion, turned on' => [
2367                'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, true, true
2368            ],
2369            'Data deletion, turned off' => [
2370                'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, false, false
2371            ],
2372        ];
2373    }
2374
2375    /**
2376     * Test for \tool_dataprivacy\api::is_automatic_request_approval_on().
2377     *
2378     * @dataProvider automatic_request_approval_setting_provider
2379     * @param string $setting The automatic approval setting.
2380     * @param int $type The data request type.
2381     * @param bool $value The setting's value.
2382     * @param bool $expected The expected result.
2383     */
2384    public function test_is_automatic_request_approval_on($setting, $type, $value, $expected) {
2385        $this->resetAfterTest();
2386
2387        if ($value !== null) {
2388            set_config($setting, $value, 'tool_dataprivacy');
2389        }
2390
2391        $this->assertEquals($expected, api::is_automatic_request_approval_on($type));
2392    }
2393}
2394