2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
20 * @package tool_dataprivacy
21 * @copyright 2018 Jun Pataleta
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 use core\invalid_persistent_exception;
26 use core\task\manager;
27 use tool_dataprivacy\context_instance;
28 use tool_dataprivacy\api;
29 use tool_dataprivacy\data_registry;
30 use tool_dataprivacy\expired_context;
31 use tool_dataprivacy\data_request;
32 use tool_dataprivacy\local\helper;
33 use tool_dataprivacy\task\initiate_data_request_task;
34 use tool_dataprivacy\task\process_data_request_task;
36 defined('MOODLE_INTERNAL') || die();
42 * @package tool_dataprivacy
43 * @copyright 2018 Jun Pataleta
44 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 class tool_dataprivacy_api_testcase extends advanced_testcase {
51 public function setUp() {
52 $this->resetAfterTest();
56 * Test for api::update_request_status().
58 public function test_update_request_status() {
59 $generator = new testing_data_generator();
60 $s1 = $generator->create_user();
63 // Create the sample data request.
64 $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT);
66 $requestid = $datarequest->get('id');
68 // Update with a valid status.
69 $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
70 $this->assertTrue($result);
72 // Fetch the request record again.
73 $datarequest = new data_request($requestid);
74 $this->assertEquals(api::DATAREQUEST_STATUS_COMPLETE, $datarequest->get('status'));
76 // Update with an invalid status.
77 $this->expectException(invalid_persistent_exception::class);
78 api::update_request_status($requestid, -1);
82 * Test for api::get_site_dpos() when there are no users with the DPO role.
84 public function test_get_site_dpos_no_dpos() {
87 $dpos = api::get_site_dpos();
88 $this->assertCount(1, $dpos);
90 $this->assertEquals($admin->id, $dpo->id);
94 * Test for api::get_site_dpos() when there are no users with the DPO role.
96 public function test_get_site_dpos() {
98 $generator = new testing_data_generator();
99 $u1 = $generator->create_user();
100 $u2 = $generator->create_user();
102 $context = context_system::instance();
104 // Give the manager role with the capability to manage data requests.
105 $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
106 assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
107 // Assign u1 as a manager.
108 role_assign($managerroleid, $u1->id, $context->id);
110 // Give the editing teacher role with the capability to manage data requests.
111 $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
112 assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true);
113 // Assign u1 as an editing teacher as well.
114 role_assign($editingteacherroleid, $u1->id, $context->id);
115 // Assign u2 as an editing teacher.
116 role_assign($editingteacherroleid, $u2->id, $context->id);
118 // Only map the manager role to the DPO role.
119 set_config('dporoles', $managerroleid, 'tool_dataprivacy');
121 $dpos = api::get_site_dpos();
122 $this->assertCount(1, $dpos);
124 $this->assertEquals($u1->id, $dpo->id);
128 * Test for api::approve_data_request().
130 public function test_approve_data_request() {
133 $generator = new testing_data_generator();
134 $s1 = $generator->create_user();
135 $u1 = $generator->create_user();
137 $context = context_system::instance();
140 $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
141 // Give the manager role with the capability to manage data requests.
142 assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
143 // Assign u1 as a manager.
144 role_assign($managerroleid, $u1->id, $context->id);
146 // Map the manager role to the DPO role.
147 set_config('dporoles', $managerroleid, 'tool_dataprivacy');
149 // Create the sample data request.
151 $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT);
152 $requestid = $datarequest->get('id');
154 // Make this ready for approval.
155 api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
158 $result = api::approve_data_request($requestid);
159 $this->assertTrue($result);
160 $datarequest = new data_request($requestid);
161 $this->assertEquals($u1->id, $datarequest->get('dpo'));
162 $this->assertEquals(api::DATAREQUEST_STATUS_APPROVED, $datarequest->get('status'));
164 // Test adhoc task creation.
165 $adhoctasks = manager::get_adhoc_tasks(process_data_request_task::class);
166 $this->assertCount(1, $adhoctasks);
170 * Test for api::approve_data_request() with the request not yet waiting for approval.
172 public function test_approve_data_request_not_yet_ready() {
175 $generator = new testing_data_generator();
176 $s1 = $generator->create_user();
177 $u1 = $generator->create_user();
179 $context = context_system::instance();
182 $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
183 // Give the manager role with the capability to manage data requests.
184 assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
185 // Assign u1 as a manager.
186 role_assign($managerroleid, $u1->id, $context->id);
188 // Map the manager role to the DPO role.
189 set_config('dporoles', $managerroleid, 'tool_dataprivacy');
191 // Create the sample data request.
193 $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT);
194 $requestid = $datarequest->get('id');
197 $this->expectException(moodle_exception::class);
198 api::approve_data_request($requestid);
202 * Test for api::approve_data_request() when called by a user who doesn't have the DPO role.
204 public function test_approve_data_request_non_dpo_user() {
205 $generator = new testing_data_generator();
206 $student = $generator->create_user();
207 $teacher = $generator->create_user();
209 // Create the sample data request.
210 $this->setUser($student);
211 $datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT);
213 $requestid = $datarequest->get('id');
215 // Login as a user without DPO role.
216 $this->setUser($teacher);
217 $this->expectException(required_capability_exception::class);
218 api::approve_data_request($requestid);
222 * Test for api::can_contact_dpo()
224 public function test_can_contact_dpo() {
225 // Default ('contactdataprotectionofficer' is disabled by default).
226 $this->assertFalse(api::can_contact_dpo());
229 set_config('contactdataprotectionofficer', 1, 'tool_dataprivacy');
230 $this->assertTrue(api::can_contact_dpo());
233 set_config('contactdataprotectionofficer', 0, 'tool_dataprivacy');
234 $this->assertFalse(api::can_contact_dpo());
238 * Test for api::can_manage_data_requests()
240 public function test_can_manage_data_requests() {
243 // No configured site DPOs yet.
244 $admin = get_admin();
245 $this->assertTrue(api::can_manage_data_requests($admin->id));
247 $generator = new testing_data_generator();
248 $dpo = $generator->create_user();
249 $nondpocapable = $generator->create_user();
250 $nondpoincapable = $generator->create_user();
252 $context = context_system::instance();
255 $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
256 // Give the manager role with the capability to manage data requests.
257 assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
258 // Assign u1 as a manager.
259 role_assign($managerroleid, $dpo->id, $context->id);
261 // Editing teacher role.
262 $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
263 // Give the editing teacher role with the capability to manage data requests.
264 assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
265 // Assign u2 as an editing teacher.
266 role_assign($editingteacherroleid, $nondpocapable->id, $context->id);
268 // Map only the manager role to the DPO role.
269 set_config('dporoles', $managerroleid, 'tool_dataprivacy');
271 // User with capability and has DPO role.
272 $this->assertTrue(api::can_manage_data_requests($dpo->id));
273 // User with capability but has no DPO role.
274 $this->assertFalse(api::can_manage_data_requests($nondpocapable->id));
275 // User without the capability and has no DPO role.
276 $this->assertFalse(api::can_manage_data_requests($nondpoincapable->id));
280 * Test for api::create_data_request()
282 public function test_create_data_request() {
283 $generator = new testing_data_generator();
284 $user = $generator->create_user();
285 $comment = 'sample comment';
288 $this->setUser($user->id);
290 // Test data request creation.
291 $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
292 $this->assertEquals($user->id, $datarequest->get('userid'));
293 $this->assertEquals($user->id, $datarequest->get('requestedby'));
294 $this->assertEquals(0, $datarequest->get('dpo'));
295 $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type'));
296 $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status'));
297 $this->assertEquals($comment, $datarequest->get('comments'));
299 // Test adhoc task creation.
300 $adhoctasks = manager::get_adhoc_tasks(initiate_data_request_task::class);
301 $this->assertCount(1, $adhoctasks);
305 * Test for api::create_data_request() made by DPO.
307 public function test_create_data_request_by_dpo() {
310 $generator = new testing_data_generator();
311 $user = $generator->create_user();
312 $comment = 'sample comment';
314 // Login as DPO (Admin is DPO by default).
315 $this->setAdminUser();
317 // Test data request creation.
318 $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
319 $this->assertEquals($user->id, $datarequest->get('userid'));
320 $this->assertEquals($USER->id, $datarequest->get('requestedby'));
321 $this->assertEquals($USER->id, $datarequest->get('dpo'));
322 $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type'));
323 $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status'));
324 $this->assertEquals($comment, $datarequest->get('comments'));
326 // Test adhoc task creation.
327 $adhoctasks = manager::get_adhoc_tasks(initiate_data_request_task::class);
328 $this->assertCount(1, $adhoctasks);
332 * Test for api::create_data_request() made by a parent.
334 public function test_create_data_request_by_parent() {
337 $generator = new testing_data_generator();
338 $user = $generator->create_user();
339 $parent = $generator->create_user();
340 $comment = 'sample comment';
342 // Get the teacher role pretend it's the parent roles ;).
343 $systemcontext = context_system::instance();
344 $usercontext = context_user::instance($user->id);
345 $parentroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
346 // Give the manager role with the capability to manage data requests.
347 assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentroleid, $systemcontext->id, true);
348 // Assign the parent to user.
349 role_assign($parentroleid, $parent->id, $usercontext->id);
351 // Login as the user's parent.
352 $this->setUser($parent);
354 // Test data request creation.
355 $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
356 $this->assertEquals($user->id, $datarequest->get('userid'));
357 $this->assertEquals($parent->id, $datarequest->get('requestedby'));
358 $this->assertEquals(0, $datarequest->get('dpo'));
359 $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type'));
360 $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status'));
361 $this->assertEquals($comment, $datarequest->get('comments'));
363 // Test adhoc task creation.
364 $adhoctasks = manager::get_adhoc_tasks(initiate_data_request_task::class);
365 $this->assertCount(1, $adhoctasks);
369 * Test for api::deny_data_request()
371 public function test_deny_data_request() {
372 $generator = new testing_data_generator();
373 $user = $generator->create_user();
374 $comment = 'sample comment';
377 $this->setUser($user->id);
379 // Test data request creation.
380 $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
382 // Login as the admin (default DPO when no one is set).
383 $this->setAdminUser();
385 // Make this ready for approval.
386 api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
388 // Deny the data request.
389 $result = api::deny_data_request($datarequest->get('id'));
390 $this->assertTrue($result);
394 * Test for api::deny_data_request()
396 public function test_deny_data_request_without_permissions() {
397 $generator = new testing_data_generator();
398 $user = $generator->create_user();
399 $comment = 'sample comment';
402 $this->setUser($user->id);
404 // Test data request creation.
405 $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
407 // Login as a non-DPO user and try to call deny_data_request.
408 $user2 = $generator->create_user();
409 $this->setUser($user2);
410 $this->expectException(required_capability_exception::class);
411 api::deny_data_request($datarequest->get('id'));
415 * Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
419 public function get_data_requests_provider() {
420 $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
421 $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
424 // Own data requests.
425 ['user', false, $completeonly],
426 // Non-DPO fetching all requets.
427 ['user', true, $completeonly],
428 // Admin fetching all completed and cancelled requests.
429 ['dpo', true, $completeandcancelled],
430 // Admin fetching all completed requests.
431 ['dpo', true, $completeonly],
432 // Guest fetching all requests.
433 ['guest', true, $completeonly],
438 * Test for api::get_data_requests()
440 * @dataProvider get_data_requests_provider
441 * @param string $usertype The type of the user logging in.
442 * @param boolean $fetchall Whether to fetch all records.
443 * @param int[] $statuses Status filters.
445 public function test_get_data_requests($usertype, $fetchall, $statuses) {
446 $generator = new testing_data_generator();
447 $user1 = $generator->create_user();
448 $user2 = $generator->create_user();
449 $user3 = $generator->create_user();
450 $user4 = $generator->create_user();
451 $user5 = $generator->create_user();
452 $users = [$user1, $user2, $user3, $user4, $user5];
456 $loggeduser = $user1;
459 $loggeduser = get_admin();
462 $loggeduser = guest_user();
466 $comment = 'Data %s request comment by user %d';
467 $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
468 $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
469 // Make a data requests for the users.
470 foreach ($users as $user) {
471 $this->setUser($user);
472 api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id));
473 api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id));
476 // Log in as the target user.
477 $this->setUser($loggeduser);
478 // Get records count based on the filters.
479 $userid = $loggeduser->id;
483 $count = api::get_data_requests_count($userid);
484 if (api::is_site_dpo($loggeduser->id)) {
485 // DPOs should see all the requests.
486 $this->assertEquals(count($users) * 2, $count);
488 if (empty($userid)) {
489 // There should be no data requests for this user available.
490 $this->assertEquals(0, $count);
492 // There should be only one (request with pending status).
493 $this->assertEquals(2, $count);
496 // Get data requests.
497 $requests = api::get_data_requests($userid);
498 // The number of requests should match the count.
499 $this->assertCount($count, $requests);
501 // Test filtering by status.
502 if ($count && !empty($statuses)) {
503 $filteredcount = api::get_data_requests_count($userid, $statuses);
504 // There should be none as they are all pending.
505 $this->assertEquals(0, $filteredcount);
506 $filteredrequests = api::get_data_requests($userid, $statuses);
507 $this->assertCount($filteredcount, $filteredrequests);
510 foreach ($statuses as $stat) {
511 $statuscounts[$stat] = 0;
513 $numstatus = count($statuses);
514 // Get all requests with status filter and update statuses, randomly.
515 foreach ($requests as $request) {
520 if ($numstatus > 1) {
521 $index = rand(0, $numstatus - 1);
522 $status = $statuses[$index];
524 $status = reset($statuses);
526 $statuscounts[$status]++;
527 api::update_request_status($request->get('id'), $status);
529 $total = array_sum($statuscounts);
530 $filteredcount = api::get_data_requests_count($userid, $statuses);
531 $this->assertEquals($total, $filteredcount);
532 $filteredrequests = api::get_data_requests($userid, $statuses);
533 $this->assertCount($filteredcount, $filteredrequests);
534 // Confirm the filtered requests match the status filter(s).
535 foreach ($filteredrequests as $request) {
536 $this->assertContains($request->get('status'), $statuses);
539 if ($numstatus > 1) {
540 // Fetch by individual status to check the numbers match.
541 foreach ($statuses as $status) {
542 $filteredcount = api::get_data_requests_count($userid, [$status]);
543 $this->assertEquals($statuscounts[$status], $filteredcount);
544 $filteredrequests = api::get_data_requests($userid, [$status]);
545 $this->assertCount($filteredcount, $filteredrequests);
552 * Data provider for test_has_ongoing_request.
554 public function status_provider() {
556 [api::DATAREQUEST_STATUS_PENDING, true],
557 [api::DATAREQUEST_STATUS_PREPROCESSING, true],
558 [api::DATAREQUEST_STATUS_AWAITING_APPROVAL, true],
559 [api::DATAREQUEST_STATUS_APPROVED, true],
560 [api::DATAREQUEST_STATUS_PROCESSING, true],
561 [api::DATAREQUEST_STATUS_COMPLETE, false],
562 [api::DATAREQUEST_STATUS_CANCELLED, false],
563 [api::DATAREQUEST_STATUS_REJECTED, false],
568 * Test for api::has_ongoing_request()
570 * @dataProvider status_provider
571 * @param int $status The request status.
572 * @param bool $expected The expected result.
574 public function test_has_ongoing_request($status, $expected) {
575 $generator = new testing_data_generator();
576 $user1 = $generator->create_user();
578 // Make a data request as user 1.
579 $this->setUser($user1);
580 $request = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT);
582 api::update_request_status($request->get('id'), $status);
584 // Check if this request is ongoing.
585 $result = api::has_ongoing_request($user1->id, api::DATAREQUEST_TYPE_EXPORT);
586 $this->assertEquals($expected, $result);
590 * Test for api::is_active()
592 * @dataProvider status_provider
593 * @param int $status The request status
594 * @param bool $expected The expected result
596 public function test_is_active($status, $expected) {
597 // Check if this request is ongoing.
598 $result = api::is_active($status);
599 $this->assertEquals($expected, $result);
603 * Test for api::is_site_dpo()
605 public function test_is_site_dpo() {
608 // No configured site DPOs yet.
609 $admin = get_admin();
610 $this->assertTrue(api::is_site_dpo($admin->id));
612 $generator = new testing_data_generator();
613 $dpo = $generator->create_user();
614 $nondpo = $generator->create_user();
616 $context = context_system::instance();
619 $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
620 // Give the manager role with the capability to manage data requests.
621 assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
622 // Assign u1 as a manager.
623 role_assign($managerroleid, $dpo->id, $context->id);
625 // Map only the manager role to the DPO role.
626 set_config('dporoles', $managerroleid, 'tool_dataprivacy');
629 $this->assertTrue(api::is_site_dpo($dpo->id));
630 // User is not a DPO.
631 $this->assertFalse(api::is_site_dpo($nondpo->id));
635 * Data provider function for test_notify_dpo
639 public function notify_dpo_provider() {
641 [false, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Export my user data'],
642 [false, api::DATAREQUEST_TYPE_DELETE, 'requesttypedelete', 'Delete my user data'],
643 [false, api::DATAREQUEST_TYPE_OTHERS, 'requesttypeothers', 'Nothing. Just wanna say hi'],
644 [true, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Admin export data of another user'],
649 * Test for api::notify_dpo()
651 * @dataProvider notify_dpo_provider
652 * @param bool $byadmin Whether the admin requests data on behalf of the user
653 * @param int $type The request type
654 * @param string $typestringid The request lang string identifier
655 * @param string $comments The requestor's message to the DPO.
657 public function test_notify_dpo($byadmin, $type, $typestringid, $comments) {
658 $generator = new testing_data_generator();
659 $user1 = $generator->create_user();
660 // Let's just use admin as DPO (It's the default if not set).
663 $this->setAdminUser();
666 $this->setUser($user1);
667 $requestedby = $user1;
670 // Make a data request for user 1.
671 $request = api::create_data_request($user1->id, $type, $comments);
673 $sink = $this->redirectMessages();
674 $messageid = api::notify_dpo($dpo, $request);
675 $this->assertNotFalse($messageid);
676 $messages = $sink->get_messages();
677 $this->assertCount(1, $messages);
678 $message = reset($messages);
680 // Check some of the message properties.
681 $this->assertEquals($requestedby->id, $message->useridfrom);
682 $this->assertEquals($dpo->id, $message->useridto);
683 $typestring = get_string($typestringid, 'tool_dataprivacy');
684 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typestring);
685 $this->assertEquals($subject, $message->subject);
686 $this->assertEquals('tool_dataprivacy', $message->component);
687 $this->assertEquals('contactdataprotectionofficer', $message->eventtype);
688 $this->assertContains(fullname($dpo), $message->fullmessage);
689 $this->assertContains(fullname($user1), $message->fullmessage);
693 * Test of creating purpose as a user without privileges.
695 public function test_create_purpose_non_dpo_user() {
696 $pleb = $this->getDataGenerator()->create_user();
698 $this->setUser($pleb);
699 $this->expectException(required_capability_exception::class);
700 api::create_purpose((object)[
702 'description' => '<b>yeah</b>',
703 'descriptionformat' => 1,
704 'retentionperiod' => 'PT1M'
709 * Test fetching of purposes as a user without privileges.
711 public function test_get_purposes_non_dpo_user() {
712 $pleb = $this->getDataGenerator()->create_user();
713 $this->setAdminUser();
714 api::create_purpose((object)[
716 'description' => '<b>yeah</b>',
717 'descriptionformat' => 1,
718 'retentionperiod' => 'PT1M',
719 'lawfulbases' => 'gdpr_art_6_1_a'
722 $this->setUser($pleb);
723 $this->expectException(required_capability_exception::class);
728 * Test updating of purpose as a user without privileges.
730 public function test_update_purposes_non_dpo_user() {
731 $pleb = $this->getDataGenerator()->create_user();
732 $this->setAdminUser();
733 $purpose = api::create_purpose((object)[
735 'description' => '<b>yeah</b>',
736 'descriptionformat' => 1,
737 'retentionperiod' => 'PT1M',
738 'lawfulbases' => 'gdpr_art_6_1_a'
741 $this->setUser($pleb);
742 $this->expectException(required_capability_exception::class);
743 $purpose->set('retentionperiod', 'PT2M');
744 api::update_purpose($purpose->to_record());
748 * Test purpose deletion as a user without privileges.
750 public function test_delete_purpose_non_dpo_user() {
751 $pleb = $this->getDataGenerator()->create_user();
752 $this->setAdminUser();
753 $purpose = api::create_purpose((object)[
755 'description' => '<b>yeah</b>',
756 'descriptionformat' => 1,
757 'retentionperiod' => 'PT1M',
758 'lawfulbases' => 'gdpr_art_6_1_a'
761 $this->setUser($pleb);
762 $this->expectException(required_capability_exception::class);
763 api::delete_purpose($purpose->get('id'));
767 * Test data purposes CRUD actions.
771 public function test_purpose_crud() {
773 $this->setAdminUser();
776 $purpose = api::create_purpose((object)[
778 'description' => '<b>yeah</b>',
779 'descriptionformat' => 1,
780 'retentionperiod' => 'PT1M',
781 'lawfulbases' => 'gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e'
783 $this->assertInstanceOf('\tool_dataprivacy\purpose', $purpose);
784 $this->assertEquals('bbb', $purpose->get('name'));
785 $this->assertEquals('PT1M', $purpose->get('retentionperiod'));
786 $this->assertEquals('gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e', $purpose->get('lawfulbases'));
789 $purpose->set('retentionperiod', 'PT2M');
790 $purpose = api::update_purpose($purpose->to_record());
791 $this->assertEquals('PT2M', $purpose->get('retentionperiod'));
794 $purpose = api::create_purpose((object)['name' => 'aaa', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a']);
795 $purposes = api::get_purposes();
796 $this->assertCount(2, $purposes);
797 $this->assertEquals('aaa', $purposes[0]->get('name'));
798 $this->assertEquals('bbb', $purposes[1]->get('name'));
801 api::delete_purpose($purposes[0]->get('id'));
802 $this->assertCount(1, api::get_purposes());
803 api::delete_purpose($purposes[1]->get('id'));
804 $this->assertCount(0, api::get_purposes());
808 * Test creation of data categories as a user without privileges.
810 public function test_create_category_non_dpo_user() {
811 $pleb = $this->getDataGenerator()->create_user();
813 $this->setUser($pleb);
814 $this->expectException(required_capability_exception::class);
815 api::create_category((object)[
817 'description' => '<b>yeah</b>',
818 'descriptionformat' => 1
823 * Test fetching of data categories as a user without privileges.
825 public function test_get_categories_non_dpo_user() {
826 $pleb = $this->getDataGenerator()->create_user();
828 $this->setAdminUser();
829 api::create_category((object)[
831 'description' => '<b>yeah</b>',
832 'descriptionformat' => 1
835 // Back to a regular user.
836 $this->setUser($pleb);
837 $this->expectException(required_capability_exception::class);
838 api::get_categories();
842 * Test updating of data category as a user without privileges.
844 public function test_update_category_non_dpo_user() {
845 $pleb = $this->getDataGenerator()->create_user();
847 $this->setAdminUser();
848 $category = api::create_category((object)[
850 'description' => '<b>yeah</b>',
851 'descriptionformat' => 1
854 // Back to a regular user.
855 $this->setUser($pleb);
856 $this->expectException(required_capability_exception::class);
857 $category->set('name', 'yeah');
858 api::update_category($category->to_record());
862 * Test deletion of data category as a user without privileges.
864 public function test_delete_category_non_dpo_user() {
865 $pleb = $this->getDataGenerator()->create_user();
867 $this->setAdminUser();
868 $category = api::create_category((object)[
870 'description' => '<b>yeah</b>',
871 'descriptionformat' => 1
874 // Back to a regular user.
875 $this->setUser($pleb);
876 $this->expectException(required_capability_exception::class);
877 api::delete_category($category->get('id'));
878 $this->fail('Users shouldn\'t be allowed to manage categories by default');
882 * Test data categories CRUD actions.
886 public function test_category_crud() {
888 $this->setAdminUser();
891 $category = api::create_category((object)[
893 'description' => '<b>yeah</b>',
894 'descriptionformat' => 1
896 $this->assertInstanceOf('\tool_dataprivacy\category', $category);
897 $this->assertEquals('bbb', $category->get('name'));
900 $category->set('name', 'bcd');
901 $category = api::update_category($category->to_record());
902 $this->assertEquals('bcd', $category->get('name'));
905 $category = api::create_category((object)['name' => 'aaa']);
906 $categories = api::get_categories();
907 $this->assertCount(2, $categories);
908 $this->assertEquals('aaa', $categories[0]->get('name'));
909 $this->assertEquals('bcd', $categories[1]->get('name'));
912 api::delete_category($categories[0]->get('id'));
913 $this->assertCount(1, api::get_categories());
914 api::delete_category($categories[1]->get('id'));
915 $this->assertCount(0, api::get_categories());
919 * Test context instances.
923 public function test_context_instances() {
926 $this->setAdminUser();
928 list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
930 $coursecontext1 = \context_course::instance($courses[0]->id);
931 $coursecontext2 = \context_course::instance($courses[1]->id);
933 $record1 = (object)['contextid' => $coursecontext1->id, 'purposeid' => $purposes[0]->get('id'),
934 'categoryid' => $categories[0]->get('id')];
935 $contextinstance1 = api::set_context_instance($record1);
937 $record2 = (object)['contextid' => $coursecontext2->id, 'purposeid' => $purposes[1]->get('id'),
938 'categoryid' => $categories[1]->get('id')];
939 $contextinstance2 = api::set_context_instance($record2);
941 $this->assertCount(2, $DB->get_records('tool_dataprivacy_ctxinstance'));
943 api::unset_context_instance($contextinstance1);
944 $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance'));
946 $update = (object)['id' => $contextinstance2->get('id'), 'contextid' => $coursecontext2->id,
947 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')];
948 $contextinstance2 = api::set_context_instance($update);
949 $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance'));
957 public function test_contextlevel() {
960 $this->setAdminUser();
961 list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
964 'purposeid' => $purposes[0]->get('id'),
965 'categoryid' => $categories[0]->get('id'),
966 'contextlevel' => CONTEXT_SYSTEM,
968 $contextlevel = api::set_contextlevel($record);
969 $this->assertInstanceOf('\tool_dataprivacy\contextlevel', $contextlevel);
970 $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel'));
971 $this->assertEquals($record->purposeid, $contextlevel->get('purposeid'));
972 $this->assertEquals($record->categoryid, $contextlevel->get('categoryid'));
975 $record->purposeid = $purposes[1]->get('id');
976 $contextlevel = api::set_contextlevel($record);
977 $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel'));
978 $this->assertEquals($record->purposeid, $contextlevel->get('purposeid'));
979 $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxlevel'));
981 $record->contextlevel = CONTEXT_USER;
982 $contextlevel = api::set_contextlevel($record);
983 $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxlevel'));
987 * Test effective context levels purpose and category defaults.
991 public function test_effective_contextlevel_defaults() {
992 $this->setAdminUser();
994 list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
996 list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM);
997 $this->assertEquals(false, $purposeid);
998 $this->assertEquals(false, $categoryid);
1000 list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1001 \context_helper::get_class_for_level(CONTEXT_SYSTEM)
1003 set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy');
1005 list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM);
1006 $this->assertEquals($purposes[0]->get('id'), $purposeid);
1007 $this->assertEquals(false, $categoryid);
1009 // Course inherits from system if not defined.
1010 list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE);
1011 $this->assertEquals($purposes[0]->get('id'), $purposeid);
1012 $this->assertEquals(false, $categoryid);
1014 // Course defined values should have preference.
1015 list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1016 \context_helper::get_class_for_level(CONTEXT_COURSE)
1018 set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy');
1019 set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy');
1021 list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE);
1022 $this->assertEquals($purposes[1]->get('id'), $purposeid);
1023 $this->assertEquals($categories[0]->get('id'), $categoryid);
1025 // Context level defaults are also allowed to be set to 'inherit'.
1026 set_config($purposevar, context_instance::INHERIT, 'tool_dataprivacy');
1028 list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE);
1029 $this->assertEquals($purposes[0]->get('id'), $purposeid);
1030 $this->assertEquals($categories[0]->get('id'), $categoryid);
1032 list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_MODULE);
1033 $this->assertEquals($purposes[0]->get('id'), $purposeid);
1034 $this->assertEquals($categories[0]->get('id'), $categoryid);
1038 * Test effective contextlevel return.
1042 public function test_effective_contextlevel() {
1043 $this->setAdminUser();
1045 list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
1047 // Set the system context level to purpose 1.
1049 'contextlevel' => CONTEXT_SYSTEM,
1050 'purposeid' => $purposes[1]->get('id'),
1051 'categoryid' => $categories[1]->get('id'),
1053 api::set_contextlevel($record);
1055 $purpose = api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM);
1056 $this->assertEquals($purposes[1]->get('id'), $purpose->get('id'));
1058 // Value 'not set' will get the default value for the context level. For context level defaults
1059 // both 'not set' and 'inherit' result in inherit, so the parent context (system) default
1060 // will be retrieved.
1061 $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
1062 $this->assertEquals($purposes[1]->get('id'), $purpose->get('id'));
1064 // The behaviour forcing an inherit from context system should result in the same effective
1066 $record->purposeid = context_instance::INHERIT;
1067 $record->contextlevel = CONTEXT_USER;
1068 api::set_contextlevel($record);
1069 $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
1070 $this->assertEquals($purposes[1]->get('id'), $purpose->get('id'));
1072 $record->purposeid = $purposes[2]->get('id');
1073 $record->contextlevel = CONTEXT_USER;
1074 api::set_contextlevel($record);
1076 $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
1077 $this->assertEquals($purposes[2]->get('id'), $purpose->get('id'));
1079 // Only system and user allowed.
1080 $this->expectException(coding_exception::class);
1081 $record->contextlevel = CONTEXT_COURSE;
1082 $record->purposeid = $purposes[1]->get('id');
1083 api::set_contextlevel($record);
1087 * Test effective context purposes and categories.
1091 public function test_effective_context() {
1092 $this->setAdminUser();
1094 list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
1096 // Define system defaults (all context levels below will inherit).
1097 list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1098 \context_helper::get_class_for_level(CONTEXT_SYSTEM)
1100 set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy');
1101 set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy');
1103 // Define course defaults.
1104 list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1105 \context_helper::get_class_for_level(CONTEXT_COURSE)
1107 set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy');
1108 set_config($categoryvar, $categories[1]->get('id'), 'tool_dataprivacy');
1110 $course0context = \context_course::instance($courses[0]->id);
1111 $course1context = \context_course::instance($courses[1]->id);
1112 $mod0context = \context_module::instance($modules[0]->cmid);
1113 $mod1context = \context_module::instance($modules[1]->cmid);
1115 // Set course instance values.
1117 'contextid' => $course0context->id,
1118 'purposeid' => $purposes[1]->get('id'),
1119 'categoryid' => $categories[2]->get('id'),
1121 api::set_context_instance($record);
1122 $category = api::get_effective_context_category($course0context);
1123 $this->assertEquals($record->categoryid, $category->get('id'));
1125 // Module instances get the context level default if nothing specified.
1126 $category = api::get_effective_context_category($mod0context);
1127 $this->assertEquals($categories[1]->get('id'), $category->get('id'));
1129 // Module instances get the parent context category if they inherit.
1130 $record->contextid = $mod0context->id;
1131 $record->categoryid = context_instance::INHERIT;
1132 api::set_context_instance($record);
1133 $category = api::get_effective_context_category($mod0context);
1134 $this->assertEquals($categories[2]->get('id'), $category->get('id'));
1136 // The $forcedvalue param allows us to override the actual value (method php-docs for more info).
1137 $category = api::get_effective_context_category($mod0context, $categories[1]->get('id'));
1138 $this->assertEquals($categories[1]->get('id'), $category->get('id'));
1139 $category = api::get_effective_context_category($mod0context, $categories[0]->get('id'));
1140 $this->assertEquals($categories[0]->get('id'), $category->get('id'));
1142 // Module instances get the parent context category if they inherit; in
1143 // this case the parent context category is not set so it should use the
1144 // context level default (see 'Define course defaults' above).
1145 $record->contextid = $mod1context->id;
1146 $record->categoryid = context_instance::INHERIT;
1147 api::set_context_instance($record);
1148 $category = api::get_effective_context_category($mod1context);
1149 $this->assertEquals($categories[1]->get('id'), $category->get('id'));
1151 // User instances use the value set at user context level instead of the user default.
1153 // User defaults to cat 0 and user context level to 1.
1154 list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1155 \context_helper::get_class_for_level(CONTEXT_USER)
1157 set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy');
1158 set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy');
1159 $usercontextlevel = (object)[
1160 'contextlevel' => CONTEXT_USER,
1161 'purposeid' => $purposes[1]->get('id'),
1162 'categoryid' => $categories[1]->get('id'),
1164 api::set_contextlevel($usercontextlevel);
1166 $newuser = $this->getDataGenerator()->create_user();
1167 $usercontext = \context_user::instance($newuser->id);
1168 $category = api::get_effective_context_category($usercontext);
1169 $this->assertEquals($categories[1]->get('id'), $category->get('id'));
1173 * Tests the deletion of expired contexts.
1177 public function test_expired_context_deletion() {
1180 $this->setAdminUser();
1182 list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
1184 $course0context = \context_course::instance($courses[0]->id);
1185 $course1context = \context_course::instance($courses[1]->id);
1187 $expiredcontext0 = api::create_expired_context($course0context->id);
1188 $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired'));
1189 $expiredcontext1 = api::create_expired_context($course1context->id);
1190 $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired'));
1192 api::delete_expired_context($expiredcontext0->get('id'));
1193 $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired'));
1197 * Tests the status of expired contexts.
1201 public function test_expired_context_status() {
1204 $this->setAdminUser();
1206 list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
1208 $course0context = \context_course::instance($courses[0]->id);
1210 $expiredcontext = api::create_expired_context($course0context->id);
1213 $this->assertEquals(expired_context::STATUS_EXPIRED, $expiredcontext->get('status'));
1215 api::set_expired_context_status($expiredcontext, expired_context::STATUS_APPROVED);
1216 $this->assertEquals(expired_context::STATUS_APPROVED, $expiredcontext->get('status'));
1220 * Creates test purposes and categories.
1224 protected function add_purposes_and_categories() {
1226 $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
1227 $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_b']);
1228 $purpose3 = api::create_purpose((object)['name' => 'p3', 'retentionperiod' => 'PT3H', 'lawfulbases' => 'gdpr_art_6_1_c']);
1230 $cat1 = api::create_category((object)['name' => 'a']);
1231 $cat2 = api::create_category((object)['name' => 'b']);
1232 $cat3 = api::create_category((object)['name' => 'c']);
1234 $course1 = $this->getDataGenerator()->create_course();
1235 $course2 = $this->getDataGenerator()->create_course();
1237 $module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1));
1238 $module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2));
1241 [$purpose1, $purpose2, $purpose3],
1242 [$cat1, $cat2, $cat3],
1243 [$course1, $course2],
1244 [$module1, $module2]
1249 * Test that delete requests filter out protected purpose contexts.
1251 public function test_add_request_contexts_with_status_delete() {
1252 $data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_DELETE);
1253 $contextids = $data->list->get_contextids();
1255 $this->assertCount(1, $contextids);
1256 $this->assertEquals($data->contexts->unprotected, $contextids);
1260 * Test that export requests don't filter out protected purpose contexts.
1262 public function test_add_request_contexts_with_status_export() {
1263 $data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_EXPORT);
1264 $contextids = $data->list->get_contextids();
1266 $this->assertCount(2, $contextids);
1267 $this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
1271 * Perform setup for the test_add_request_contexts_with_status_xxxxx tests.
1273 * @param int $type The type of request to create
1276 protected function setup_test_add_request_contexts_with_status($type) {
1277 $this->setAdminUser();
1280 $s1 = $this->getDataGenerator()->create_user();
1282 // Create three sample contexts.
1283 // 1 which should not be returned; and
1284 // 1 which will be returned and is not protected; and
1285 // 1 which will be returned and is protected.
1287 $c1 = $this->getDataGenerator()->create_course();
1288 $c2 = $this->getDataGenerator()->create_course();
1289 $c3 = $this->getDataGenerator()->create_course();
1291 $ctx1 = \context_course::instance($c1->id);
1292 $ctx2 = \context_course::instance($c2->id);
1293 $ctx3 = \context_course::instance($c3->id);
1295 $unprotected = api::create_purpose((object)[
1296 'name' => 'Unprotected', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a']);
1297 $protected = api::create_purpose((object) [
1298 'name' => 'Protected', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a', 'protected' => true]);
1300 $cat1 = api::create_category((object)['name' => 'a']);
1302 // Set the defaults.
1303 list($purposevar, $categoryvar) = data_registry::var_names_from_context(
1304 \context_helper::get_class_for_level(CONTEXT_SYSTEM)
1306 set_config($purposevar, $unprotected->get('id'), 'tool_dataprivacy');
1307 set_config($categoryvar, $cat1->get('id'), 'tool_dataprivacy');
1309 $contextinstance1 = api::set_context_instance((object) [
1310 'contextid' => $ctx1->id,
1311 'purposeid' => $unprotected->get('id'),
1312 'categoryid' => $cat1->get('id'),
1315 $contextinstance2 = api::set_context_instance((object) [
1316 'contextid' => $ctx2->id,
1317 'purposeid' => $unprotected->get('id'),
1318 'categoryid' => $cat1->get('id'),
1321 $contextinstance3 = api::set_context_instance((object) [
1322 'contextid' => $ctx3->id,
1323 'purposeid' => $protected->get('id'),
1324 'categoryid' => $cat1->get('id'),
1327 $collection = new \core_privacy\local\request\contextlist_collection($s1->id);
1328 $contextlist = new \core_privacy\local\request\contextlist();
1329 $contextlist->set_component('tool_dataprivacy');
1330 $contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx2, :ctx3)', [
1331 'ctx2' => $ctx2->id,
1332 'ctx3' => $ctx3->id,
1335 $collection->add_contextlist($contextlist);
1337 // Create the sample data request.
1338 $datarequest = api::create_data_request($s1->id, $type);
1339 $requestid = $datarequest->get('id');
1341 // Add the full collection with contexts 2, and 3.
1342 api::add_request_contexts_with_status($collection, $requestid, \tool_dataprivacy\contextlist_context::STATUS_PENDING);
1344 // Mark it as approved.
1345 api::update_request_contexts_with_status($requestid, \tool_dataprivacy\contextlist_context::STATUS_APPROVED);
1348 $approvedcollection = api::get_approved_contextlist_collection_for_request($datarequest);
1351 'contexts' => (object) [
1366 'list' => $approvedcollection->get_contextlist_for_component('tool_dataprivacy'),