. /** * API tests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\invalid_persistent_exception; use core\task\manager; use tool_dataprivacy\context_instance; use tool_dataprivacy\api; use tool_dataprivacy\data_registry; use tool_dataprivacy\expired_context; use tool_dataprivacy\data_request; use tool_dataprivacy\local\helper; use tool_dataprivacy\task\initiate_data_request_task; use tool_dataprivacy\task\process_data_request_task; defined('MOODLE_INTERNAL') || die(); global $CFG; /** * API tests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_dataprivacy_api_testcase extends advanced_testcase { /** * setUp. */ public function setUp() { $this->resetAfterTest(); } /** * Test for api::update_request_status(). */ public function test_update_request_status() { $generator = new testing_data_generator(); $s1 = $generator->create_user(); $this->setUser($s1); // Create the sample data request. $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); // Update with a valid status. $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE); $this->assertTrue($result); // Fetch the request record again. $datarequest = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_COMPLETE, $datarequest->get('status')); // Update with an invalid status. $this->expectException(invalid_persistent_exception::class); api::update_request_status($requestid, -1); } /** * Test for api::get_site_dpos() when there are no users with the DPO role. */ public function test_get_site_dpos_no_dpos() { $admin = get_admin(); $dpos = api::get_site_dpos(); $this->assertCount(1, $dpos); $dpo = reset($dpos); $this->assertEquals($admin->id, $dpo->id); } /** * Test for api::get_site_dpos() when there are no users with the DPO role. */ public function test_get_site_dpos() { global $DB; $generator = new testing_data_generator(); $u1 = $generator->create_user(); $u2 = $generator->create_user(); $context = context_system::instance(); // Give the manager role with the capability to manage data requests. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $u1->id, $context->id); // Give the editing teacher role with the capability to manage data requests. $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true); // Assign u1 as an editing teacher as well. role_assign($editingteacherroleid, $u1->id, $context->id); // Assign u2 as an editing teacher. role_assign($editingteacherroleid, $u2->id, $context->id); // Only map the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); $dpos = api::get_site_dpos(); $this->assertCount(1, $dpos); $dpo = reset($dpos); $this->assertEquals($u1->id, $dpo->id); } /** * Test for api::approve_data_request(). */ public function test_approve_data_request() { global $DB; $generator = new testing_data_generator(); $s1 = $generator->create_user(); $u1 = $generator->create_user(); $context = context_system::instance(); // Manager role. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $u1->id, $context->id); // Map the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // Create the sample data request. $this->setUser($s1); $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); // Make this ready for approval. api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); $this->setUser($u1); $result = api::approve_data_request($requestid); $this->assertTrue($result); $datarequest = new data_request($requestid); $this->assertEquals($u1->id, $datarequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_STATUS_APPROVED, $datarequest->get('status')); // Test adhoc task creation. $adhoctasks = manager::get_adhoc_tasks(process_data_request_task::class); $this->assertCount(1, $adhoctasks); } /** * Test for api::approve_data_request() with the request not yet waiting for approval. */ public function test_approve_data_request_not_yet_ready() { global $DB; $generator = new testing_data_generator(); $s1 = $generator->create_user(); $u1 = $generator->create_user(); $context = context_system::instance(); // Manager role. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $u1->id, $context->id); // Map the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // Create the sample data request. $this->setUser($s1); $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); $this->setUser($u1); $this->expectException(moodle_exception::class); api::approve_data_request($requestid); } /** * Test for api::approve_data_request() when called by a user who doesn't have the DPO role. */ public function test_approve_data_request_non_dpo_user() { $generator = new testing_data_generator(); $student = $generator->create_user(); $teacher = $generator->create_user(); // Create the sample data request. $this->setUser($student); $datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); // Login as a user without DPO role. $this->setUser($teacher); $this->expectException(required_capability_exception::class); api::approve_data_request($requestid); } /** * Test for api::can_contact_dpo() */ public function test_can_contact_dpo() { // Default ('contactdataprotectionofficer' is disabled by default). $this->assertFalse(api::can_contact_dpo()); // Enable. set_config('contactdataprotectionofficer', 1, 'tool_dataprivacy'); $this->assertTrue(api::can_contact_dpo()); // Disable again. set_config('contactdataprotectionofficer', 0, 'tool_dataprivacy'); $this->assertFalse(api::can_contact_dpo()); } /** * Test for api::can_manage_data_requests() */ public function test_can_manage_data_requests() { global $DB; // No configured site DPOs yet. $admin = get_admin(); $this->assertTrue(api::can_manage_data_requests($admin->id)); $generator = new testing_data_generator(); $dpo = $generator->create_user(); $nondpocapable = $generator->create_user(); $nondpoincapable = $generator->create_user(); $context = context_system::instance(); // Manager role. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $dpo->id, $context->id); // Editing teacher role. $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); // Give the editing teacher role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u2 as an editing teacher. role_assign($editingteacherroleid, $nondpocapable->id, $context->id); // Map only the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // User with capability and has DPO role. $this->assertTrue(api::can_manage_data_requests($dpo->id)); // User with capability but has no DPO role. $this->assertFalse(api::can_manage_data_requests($nondpocapable->id)); // User without the capability and has no DPO role. $this->assertFalse(api::can_manage_data_requests($nondpoincapable->id)); } /** * Test for api::can_download_data_request_for_user() */ public function test_can_download_data_request_for_user() { $generator = $this->getDataGenerator(); // Three victims. $victim1 = $generator->create_user(); $victim2 = $generator->create_user(); $victim3 = $generator->create_user(); // Assign a user as victim 1's parent. $systemcontext = \context_system::instance(); $parentrole = $generator->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); $parent = $generator->create_user(); role_assign($parentrole, $parent->id, \context_user::instance($victim1->id)); // Assign another user as data access wonder woman. $wonderrole = $generator->create_role(); assign_capability('tool/dataprivacy:downloadallrequests', CAP_ALLOW, $wonderrole, $systemcontext); $staff = $generator->create_user(); role_assign($wonderrole, $staff->id, $systemcontext); // Finally, victim 3 has been naughty; stop them accessing their own data. $naughtyrole = $generator->create_role(); assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $naughtyrole, $systemcontext); role_assign($naughtyrole, $victim3->id, $systemcontext); // Victims 1 and 2 can access their own data, regardless of who requested it. $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $victim1->id)); $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $victim2->id)); // Victim 3 cannot access his own data. $this->assertFalse(api::can_download_data_request_for_user($victim3->id, $victim3->id, $victim3->id)); // Victims 1 and 2 cannot access another victim's data. $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $victim1->id, $victim1->id)); $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $victim2->id)); // Staff can access everyone's data. $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $staff->id)); $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $staff->id)); $this->assertTrue(api::can_download_data_request_for_user($victim3->id, $staff->id, $staff->id)); // Parent can access victim 1's data only if they requested it. $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $parent->id, $parent->id)); $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $parent->id)); $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $parent->id, $parent->id)); } /** * Test for api::create_data_request() */ public function test_create_data_request() { $generator = new testing_data_generator(); $user = $generator->create_user(); $comment = 'sample comment'; // Login as user. $this->setUser($user->id); // Test data request creation. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $this->assertEquals($user->id, $datarequest->get('userid')); $this->assertEquals($user->id, $datarequest->get('requestedby')); $this->assertEquals(0, $datarequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type')); $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status')); $this->assertEquals($comment, $datarequest->get('comments')); // Test adhoc task creation. $adhoctasks = manager::get_adhoc_tasks(initiate_data_request_task::class); $this->assertCount(1, $adhoctasks); } /** * Test for api::create_data_request() made by DPO. */ public function test_create_data_request_by_dpo() { global $USER; $generator = new testing_data_generator(); $user = $generator->create_user(); $comment = 'sample comment'; // Login as DPO (Admin is DPO by default). $this->setAdminUser(); // Test data request creation. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $this->assertEquals($user->id, $datarequest->get('userid')); $this->assertEquals($USER->id, $datarequest->get('requestedby')); $this->assertEquals($USER->id, $datarequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type')); $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status')); $this->assertEquals($comment, $datarequest->get('comments')); // Test adhoc task creation. $adhoctasks = manager::get_adhoc_tasks(initiate_data_request_task::class); $this->assertCount(1, $adhoctasks); } /** * Test for api::create_data_request() made by a parent. */ public function test_create_data_request_by_parent() { global $DB; $generator = new testing_data_generator(); $user = $generator->create_user(); $parent = $generator->create_user(); $comment = 'sample comment'; // Get the teacher role pretend it's the parent roles ;). $systemcontext = context_system::instance(); $usercontext = context_user::instance($user->id); $parentroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentroleid, $systemcontext->id, true); // Assign the parent to user. role_assign($parentroleid, $parent->id, $usercontext->id); // Login as the user's parent. $this->setUser($parent); // Test data request creation. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $this->assertEquals($user->id, $datarequest->get('userid')); $this->assertEquals($parent->id, $datarequest->get('requestedby')); $this->assertEquals(0, $datarequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type')); $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status')); $this->assertEquals($comment, $datarequest->get('comments')); // Test adhoc task creation. $adhoctasks = manager::get_adhoc_tasks(initiate_data_request_task::class); $this->assertCount(1, $adhoctasks); } /** * Test for api::deny_data_request() */ public function test_deny_data_request() { $generator = new testing_data_generator(); $user = $generator->create_user(); $comment = 'sample comment'; // Login as user. $this->setUser($user->id); // Test data request creation. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Login as the admin (default DPO when no one is set). $this->setAdminUser(); // Make this ready for approval. api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL); // Deny the data request. $result = api::deny_data_request($datarequest->get('id')); $this->assertTrue($result); } /** * Test for api::deny_data_request() */ public function test_deny_data_request_without_permissions() { $generator = new testing_data_generator(); $user = $generator->create_user(); $comment = 'sample comment'; // Login as user. $this->setUser($user->id); // Test data request creation. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Login as a non-DPO user and try to call deny_data_request. $user2 = $generator->create_user(); $this->setUser($user2); $this->expectException(required_capability_exception::class); api::deny_data_request($datarequest->get('id')); } /** * Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests(). * * @return array */ public function get_data_requests_provider() { $completeonly = [api::DATAREQUEST_STATUS_COMPLETE]; $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED]; return [ // Own data requests. ['user', false, $completeonly], // Non-DPO fetching all requets. ['user', true, $completeonly], // Admin fetching all completed and cancelled requests. ['dpo', true, $completeandcancelled], // Admin fetching all completed requests. ['dpo', true, $completeonly], // Guest fetching all requests. ['guest', true, $completeonly], ]; } /** * Test for api::get_data_requests() * * @dataProvider get_data_requests_provider * @param string $usertype The type of the user logging in. * @param boolean $fetchall Whether to fetch all records. * @param int[] $statuses Status filters. */ public function test_get_data_requests($usertype, $fetchall, $statuses) { $generator = new testing_data_generator(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $user3 = $generator->create_user(); $user4 = $generator->create_user(); $user5 = $generator->create_user(); $users = [$user1, $user2, $user3, $user4, $user5]; switch ($usertype) { case 'user': $loggeduser = $user1; break; case 'dpo': $loggeduser = get_admin(); break; case 'guest': $loggeduser = guest_user(); break; } $comment = 'Data %s request comment by user %d'; $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT); $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE); // Make a data requests for the users. foreach ($users as $user) { $this->setUser($user); api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id)); api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id)); } // Log in as the target user. $this->setUser($loggeduser); // Get records count based on the filters. $userid = $loggeduser->id; if ($fetchall) { $userid = 0; } $count = api::get_data_requests_count($userid); if (api::is_site_dpo($loggeduser->id)) { // DPOs should see all the requests. $this->assertEquals(count($users) * 2, $count); } else { if (empty($userid)) { // There should be no data requests for this user available. $this->assertEquals(0, $count); } else { // There should be only one (request with pending status). $this->assertEquals(2, $count); } } // Get data requests. $requests = api::get_data_requests($userid); // The number of requests should match the count. $this->assertCount($count, $requests); // Test filtering by status. if ($count && !empty($statuses)) { $filteredcount = api::get_data_requests_count($userid, $statuses); // There should be none as they are all pending. $this->assertEquals(0, $filteredcount); $filteredrequests = api::get_data_requests($userid, $statuses); $this->assertCount($filteredcount, $filteredrequests); $statuscounts = []; foreach ($statuses as $stat) { $statuscounts[$stat] = 0; } $numstatus = count($statuses); // Get all requests with status filter and update statuses, randomly. foreach ($requests as $request) { if (rand(0, 1)) { continue; } if ($numstatus > 1) { $index = rand(0, $numstatus - 1); $status = $statuses[$index]; } else { $status = reset($statuses); } $statuscounts[$status]++; api::update_request_status($request->get('id'), $status); } $total = array_sum($statuscounts); $filteredcount = api::get_data_requests_count($userid, $statuses); $this->assertEquals($total, $filteredcount); $filteredrequests = api::get_data_requests($userid, $statuses); $this->assertCount($filteredcount, $filteredrequests); // Confirm the filtered requests match the status filter(s). foreach ($filteredrequests as $request) { $this->assertContains($request->get('status'), $statuses); } if ($numstatus > 1) { // Fetch by individual status to check the numbers match. foreach ($statuses as $status) { $filteredcount = api::get_data_requests_count($userid, [$status]); $this->assertEquals($statuscounts[$status], $filteredcount); $filteredrequests = api::get_data_requests($userid, [$status]); $this->assertCount($filteredcount, $filteredrequests); } } } } /** * Data provider for test_has_ongoing_request. */ public function status_provider() { return [ [api::DATAREQUEST_STATUS_PENDING, true], [api::DATAREQUEST_STATUS_PREPROCESSING, true], [api::DATAREQUEST_STATUS_AWAITING_APPROVAL, true], [api::DATAREQUEST_STATUS_APPROVED, true], [api::DATAREQUEST_STATUS_PROCESSING, true], [api::DATAREQUEST_STATUS_COMPLETE, false], [api::DATAREQUEST_STATUS_CANCELLED, false], [api::DATAREQUEST_STATUS_REJECTED, false], ]; } /** * Test for api::has_ongoing_request() * * @dataProvider status_provider * @param int $status The request status. * @param bool $expected The expected result. */ public function test_has_ongoing_request($status, $expected) { $generator = new testing_data_generator(); $user1 = $generator->create_user(); // Make a data request as user 1. $this->setUser($user1); $request = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT); // Set the status. api::update_request_status($request->get('id'), $status); // Check if this request is ongoing. $result = api::has_ongoing_request($user1->id, api::DATAREQUEST_TYPE_EXPORT); $this->assertEquals($expected, $result); } /** * Test for api::is_active() * * @dataProvider status_provider * @param int $status The request status * @param bool $expected The expected result */ public function test_is_active($status, $expected) { // Check if this request is ongoing. $result = api::is_active($status); $this->assertEquals($expected, $result); } /** * Test for api::is_site_dpo() */ public function test_is_site_dpo() { global $DB; // No configured site DPOs yet. $admin = get_admin(); $this->assertTrue(api::is_site_dpo($admin->id)); $generator = new testing_data_generator(); $dpo = $generator->create_user(); $nondpo = $generator->create_user(); $context = context_system::instance(); // Manager role. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $dpo->id, $context->id); // Map only the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // User is a DPO. $this->assertTrue(api::is_site_dpo($dpo->id)); // User is not a DPO. $this->assertFalse(api::is_site_dpo($nondpo->id)); } /** * Data provider function for test_notify_dpo * * @return array */ public function notify_dpo_provider() { return [ [false, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Export my user data'], [false, api::DATAREQUEST_TYPE_DELETE, 'requesttypedelete', 'Delete my user data'], [false, api::DATAREQUEST_TYPE_OTHERS, 'requesttypeothers', 'Nothing. Just wanna say hi'], [true, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Admin export data of another user'], ]; } /** * Test for api::notify_dpo() * * @dataProvider notify_dpo_provider * @param bool $byadmin Whether the admin requests data on behalf of the user * @param int $type The request type * @param string $typestringid The request lang string identifier * @param string $comments The requestor's message to the DPO. */ public function test_notify_dpo($byadmin, $type, $typestringid, $comments) { $generator = new testing_data_generator(); $user1 = $generator->create_user(); // Let's just use admin as DPO (It's the default if not set). $dpo = get_admin(); if ($byadmin) { $this->setAdminUser(); $requestedby = $dpo; } else { $this->setUser($user1); $requestedby = $user1; } // Make a data request for user 1. $request = api::create_data_request($user1->id, $type, $comments); $sink = $this->redirectMessages(); $messageid = api::notify_dpo($dpo, $request); $this->assertNotFalse($messageid); $messages = $sink->get_messages(); $this->assertCount(1, $messages); $message = reset($messages); // Check some of the message properties. $this->assertEquals($requestedby->id, $message->useridfrom); $this->assertEquals($dpo->id, $message->useridto); $typestring = get_string($typestringid, 'tool_dataprivacy'); $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typestring); $this->assertEquals($subject, $message->subject); $this->assertEquals('tool_dataprivacy', $message->component); $this->assertEquals('contactdataprotectionofficer', $message->eventtype); $this->assertContains(fullname($dpo), $message->fullmessage); $this->assertContains(fullname($user1), $message->fullmessage); } /** * Test of creating purpose as a user without privileges. */ public function test_create_purpose_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setUser($pleb); $this->expectException(required_capability_exception::class); api::create_purpose((object)[ 'name' => 'aaa', 'description' => 'yeah', 'descriptionformat' => 1, 'retentionperiod' => 'PT1M' ]); } /** * Test fetching of purposes as a user without privileges. */ public function test_get_purposes_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setAdminUser(); api::create_purpose((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); $this->setUser($pleb); $this->expectException(required_capability_exception::class); api::get_purposes(); } /** * Test updating of purpose as a user without privileges. */ public function test_update_purposes_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setAdminUser(); $purpose = api::create_purpose((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); $this->setUser($pleb); $this->expectException(required_capability_exception::class); $purpose->set('retentionperiod', 'PT2M'); api::update_purpose($purpose->to_record()); } /** * Test purpose deletion as a user without privileges. */ public function test_delete_purpose_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setAdminUser(); $purpose = api::create_purpose((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); $this->setUser($pleb); $this->expectException(required_capability_exception::class); api::delete_purpose($purpose->get('id')); } /** * Test data purposes CRUD actions. * * @return null */ public function test_purpose_crud() { $this->setAdminUser(); // Add. $purpose = api::create_purpose((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e' ]); $this->assertInstanceOf('\tool_dataprivacy\purpose', $purpose); $this->assertEquals('bbb', $purpose->get('name')); $this->assertEquals('PT1M', $purpose->get('retentionperiod')); $this->assertEquals('gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e', $purpose->get('lawfulbases')); // Update. $purpose->set('retentionperiod', 'PT2M'); $purpose = api::update_purpose($purpose->to_record()); $this->assertEquals('PT2M', $purpose->get('retentionperiod')); // Retrieve. $purpose = api::create_purpose((object)['name' => 'aaa', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a']); $purposes = api::get_purposes(); $this->assertCount(2, $purposes); $this->assertEquals('aaa', $purposes[0]->get('name')); $this->assertEquals('bbb', $purposes[1]->get('name')); // Delete. api::delete_purpose($purposes[0]->get('id')); $this->assertCount(1, api::get_purposes()); api::delete_purpose($purposes[1]->get('id')); $this->assertCount(0, api::get_purposes()); } /** * Test creation of data categories as a user without privileges. */ public function test_create_category_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setUser($pleb); $this->expectException(required_capability_exception::class); api::create_category((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1 ]); } /** * Test fetching of data categories as a user without privileges. */ public function test_get_categories_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setAdminUser(); api::create_category((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1 ]); // Back to a regular user. $this->setUser($pleb); $this->expectException(required_capability_exception::class); api::get_categories(); } /** * Test updating of data category as a user without privileges. */ public function test_update_category_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setAdminUser(); $category = api::create_category((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1 ]); // Back to a regular user. $this->setUser($pleb); $this->expectException(required_capability_exception::class); $category->set('name', 'yeah'); api::update_category($category->to_record()); } /** * Test deletion of data category as a user without privileges. */ public function test_delete_category_non_dpo_user() { $pleb = $this->getDataGenerator()->create_user(); $this->setAdminUser(); $category = api::create_category((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1 ]); // Back to a regular user. $this->setUser($pleb); $this->expectException(required_capability_exception::class); api::delete_category($category->get('id')); $this->fail('Users shouldn\'t be allowed to manage categories by default'); } /** * Test data categories CRUD actions. * * @return null */ public function test_category_crud() { $this->setAdminUser(); // Add. $category = api::create_category((object)[ 'name' => 'bbb', 'description' => 'yeah', 'descriptionformat' => 1 ]); $this->assertInstanceOf('\tool_dataprivacy\category', $category); $this->assertEquals('bbb', $category->get('name')); // Update. $category->set('name', 'bcd'); $category = api::update_category($category->to_record()); $this->assertEquals('bcd', $category->get('name')); // Retrieve. $category = api::create_category((object)['name' => 'aaa']); $categories = api::get_categories(); $this->assertCount(2, $categories); $this->assertEquals('aaa', $categories[0]->get('name')); $this->assertEquals('bcd', $categories[1]->get('name')); // Delete. api::delete_category($categories[0]->get('id')); $this->assertCount(1, api::get_categories()); api::delete_category($categories[1]->get('id')); $this->assertCount(0, api::get_categories()); } /** * Test context instances. * * @return null */ public function test_context_instances() { global $DB; $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); $coursecontext1 = \context_course::instance($courses[0]->id); $coursecontext2 = \context_course::instance($courses[1]->id); $record1 = (object)['contextid' => $coursecontext1->id, 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')]; $contextinstance1 = api::set_context_instance($record1); $record2 = (object)['contextid' => $coursecontext2->id, 'purposeid' => $purposes[1]->get('id'), 'categoryid' => $categories[1]->get('id')]; $contextinstance2 = api::set_context_instance($record2); $this->assertCount(2, $DB->get_records('tool_dataprivacy_ctxinstance')); api::unset_context_instance($contextinstance1); $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance')); $update = (object)['id' => $contextinstance2->get('id'), 'contextid' => $coursecontext2->id, 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')]; $contextinstance2 = api::set_context_instance($update); $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance')); } /** * Test contextlevel. * * @return null */ public function test_contextlevel() { global $DB; $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); $record = (object)[ 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id'), 'contextlevel' => CONTEXT_SYSTEM, ]; $contextlevel = api::set_contextlevel($record); $this->assertInstanceOf('\tool_dataprivacy\contextlevel', $contextlevel); $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel')); $this->assertEquals($record->purposeid, $contextlevel->get('purposeid')); $this->assertEquals($record->categoryid, $contextlevel->get('categoryid')); // Now update it. $record->purposeid = $purposes[1]->get('id'); $contextlevel = api::set_contextlevel($record); $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel')); $this->assertEquals($record->purposeid, $contextlevel->get('purposeid')); $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxlevel')); $record->contextlevel = CONTEXT_USER; $contextlevel = api::set_contextlevel($record); $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxlevel')); } /** * Test effective context levels purpose and category defaults. * * @return null */ public function test_effective_contextlevel_defaults() { $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM); $this->assertEquals(false, $purposeid); $this->assertEquals(false, $categoryid); list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_SYSTEM) ); set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy'); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM); $this->assertEquals($purposes[0]->get('id'), $purposeid); $this->assertEquals(false, $categoryid); // Course inherits from system if not defined. list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE); $this->assertEquals($purposes[0]->get('id'), $purposeid); $this->assertEquals(false, $categoryid); // Course defined values should have preference. list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_COURSE) ); set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy'); set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy'); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE); $this->assertEquals($purposes[1]->get('id'), $purposeid); $this->assertEquals($categories[0]->get('id'), $categoryid); // Context level defaults are also allowed to be set to 'inherit'. set_config($purposevar, context_instance::INHERIT, 'tool_dataprivacy'); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE); $this->assertEquals($purposes[0]->get('id'), $purposeid); $this->assertEquals($categories[0]->get('id'), $categoryid); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_MODULE); $this->assertEquals($purposes[0]->get('id'), $purposeid); $this->assertEquals($categories[0]->get('id'), $categoryid); } /** * Test effective contextlevel return. * * @return null */ public function test_effective_contextlevel() { $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); // Set the system context level to purpose 1. $record = (object)[ 'contextlevel' => CONTEXT_SYSTEM, 'purposeid' => $purposes[1]->get('id'), 'categoryid' => $categories[1]->get('id'), ]; api::set_contextlevel($record); $purpose = api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM); $this->assertEquals($purposes[1]->get('id'), $purpose->get('id')); // Value 'not set' will get the default value for the context level. For context level defaults // both 'not set' and 'inherit' result in inherit, so the parent context (system) default // will be retrieved. $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER); $this->assertEquals($purposes[1]->get('id'), $purpose->get('id')); // The behaviour forcing an inherit from context system should result in the same effective // purpose. $record->purposeid = context_instance::INHERIT; $record->contextlevel = CONTEXT_USER; api::set_contextlevel($record); $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER); $this->assertEquals($purposes[1]->get('id'), $purpose->get('id')); $record->purposeid = $purposes[2]->get('id'); $record->contextlevel = CONTEXT_USER; api::set_contextlevel($record); $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER); $this->assertEquals($purposes[2]->get('id'), $purpose->get('id')); // Only system and user allowed. $this->expectException(coding_exception::class); $record->contextlevel = CONTEXT_COURSE; $record->purposeid = $purposes[1]->get('id'); api::set_contextlevel($record); } /** * Test effective context purposes and categories. * * @return null */ public function test_effective_context() { $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); // Define system defaults (all context levels below will inherit). list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_SYSTEM) ); set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy'); set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy'); // Define course defaults. list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_COURSE) ); set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy'); set_config($categoryvar, $categories[1]->get('id'), 'tool_dataprivacy'); $course0context = \context_course::instance($courses[0]->id); $course1context = \context_course::instance($courses[1]->id); $mod0context = \context_module::instance($modules[0]->cmid); $mod1context = \context_module::instance($modules[1]->cmid); // Set course instance values. $record = (object)[ 'contextid' => $course0context->id, 'purposeid' => $purposes[1]->get('id'), 'categoryid' => $categories[2]->get('id'), ]; api::set_context_instance($record); $category = api::get_effective_context_category($course0context); $this->assertEquals($record->categoryid, $category->get('id')); // Module instances get the context level default if nothing specified. $category = api::get_effective_context_category($mod0context); $this->assertEquals($categories[1]->get('id'), $category->get('id')); // Module instances get the parent context category if they inherit. $record->contextid = $mod0context->id; $record->categoryid = context_instance::INHERIT; api::set_context_instance($record); $category = api::get_effective_context_category($mod0context); $this->assertEquals($categories[2]->get('id'), $category->get('id')); // The $forcedvalue param allows us to override the actual value (method php-docs for more info). $category = api::get_effective_context_category($mod0context, $categories[1]->get('id')); $this->assertEquals($categories[1]->get('id'), $category->get('id')); $category = api::get_effective_context_category($mod0context, $categories[0]->get('id')); $this->assertEquals($categories[0]->get('id'), $category->get('id')); // Module instances get the parent context category if they inherit; in // this case the parent context category is not set so it should use the // context level default (see 'Define course defaults' above). $record->contextid = $mod1context->id; $record->categoryid = context_instance::INHERIT; api::set_context_instance($record); $category = api::get_effective_context_category($mod1context); $this->assertEquals($categories[1]->get('id'), $category->get('id')); // User instances use the value set at user context level instead of the user default. // User defaults to cat 0 and user context level to 1. list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_USER) ); set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy'); set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy'); $usercontextlevel = (object)[ 'contextlevel' => CONTEXT_USER, 'purposeid' => $purposes[1]->get('id'), 'categoryid' => $categories[1]->get('id'), ]; api::set_contextlevel($usercontextlevel); $newuser = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($newuser->id); $category = api::get_effective_context_category($usercontext); $this->assertEquals($categories[1]->get('id'), $category->get('id')); } /** * Tests the deletion of expired contexts. * * @return null */ public function test_expired_context_deletion() { global $DB; $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); $course0context = \context_course::instance($courses[0]->id); $course1context = \context_course::instance($courses[1]->id); $expiredcontext0 = api::create_expired_context($course0context->id); $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired')); $expiredcontext1 = api::create_expired_context($course1context->id); $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired')); api::delete_expired_context($expiredcontext0->get('id')); $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired')); } /** * Tests the status of expired contexts. * * @return null */ public function test_expired_context_status() { global $DB; $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); $course0context = \context_course::instance($courses[0]->id); $expiredcontext = api::create_expired_context($course0context->id); // Default status. $this->assertEquals(expired_context::STATUS_EXPIRED, $expiredcontext->get('status')); api::set_expired_context_status($expiredcontext, expired_context::STATUS_APPROVED); $this->assertEquals(expired_context::STATUS_APPROVED, $expiredcontext->get('status')); } /** * Creates test purposes and categories. * * @return null */ protected function add_purposes_and_categories() { $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']); $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_b']); $purpose3 = api::create_purpose((object)['name' => 'p3', 'retentionperiod' => 'PT3H', 'lawfulbases' => 'gdpr_art_6_1_c']); $cat1 = api::create_category((object)['name' => 'a']); $cat2 = api::create_category((object)['name' => 'b']); $cat3 = api::create_category((object)['name' => 'c']); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1)); $module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2)); return [ [$purpose1, $purpose2, $purpose3], [$cat1, $cat2, $cat3], [$course1, $course2], [$module1, $module2] ]; } /** * Test that delete requests filter out protected purpose contexts. */ public function test_add_request_contexts_with_status_delete() { $data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_DELETE); $contextids = $data->list->get_contextids(); $this->assertCount(1, $contextids); $this->assertEquals($data->contexts->unprotected, $contextids); } /** * Test that export requests don't filter out protected purpose contexts. */ public function test_add_request_contexts_with_status_export() { $data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_EXPORT); $contextids = $data->list->get_contextids(); $this->assertCount(2, $contextids); $this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true); } /** * Perform setup for the test_add_request_contexts_with_status_xxxxx tests. * * @param int $type The type of request to create * @return \stdClass */ protected function setup_test_add_request_contexts_with_status($type) { $this->setAdminUser(); // User under test. $s1 = $this->getDataGenerator()->create_user(); // Create three sample contexts. // 1 which should not be returned; and // 1 which will be returned and is not protected; and // 1 which will be returned and is protected. $c1 = $this->getDataGenerator()->create_course(); $c2 = $this->getDataGenerator()->create_course(); $c3 = $this->getDataGenerator()->create_course(); $ctx1 = \context_course::instance($c1->id); $ctx2 = \context_course::instance($c2->id); $ctx3 = \context_course::instance($c3->id); $unprotected = api::create_purpose((object)[ 'name' => 'Unprotected', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a']); $protected = api::create_purpose((object) [ 'name' => 'Protected', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a', 'protected' => true]); $cat1 = api::create_category((object)['name' => 'a']); // Set the defaults. list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_SYSTEM) ); set_config($purposevar, $unprotected->get('id'), 'tool_dataprivacy'); set_config($categoryvar, $cat1->get('id'), 'tool_dataprivacy'); $contextinstance1 = api::set_context_instance((object) [ 'contextid' => $ctx1->id, 'purposeid' => $unprotected->get('id'), 'categoryid' => $cat1->get('id'), ]); $contextinstance2 = api::set_context_instance((object) [ 'contextid' => $ctx2->id, 'purposeid' => $unprotected->get('id'), 'categoryid' => $cat1->get('id'), ]); $contextinstance3 = api::set_context_instance((object) [ 'contextid' => $ctx3->id, 'purposeid' => $protected->get('id'), 'categoryid' => $cat1->get('id'), ]); $collection = new \core_privacy\local\request\contextlist_collection($s1->id); $contextlist = new \core_privacy\local\request\contextlist(); $contextlist->set_component('tool_dataprivacy'); $contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx2, :ctx3)', [ 'ctx2' => $ctx2->id, 'ctx3' => $ctx3->id, ]); $collection->add_contextlist($contextlist); // Create the sample data request. $datarequest = api::create_data_request($s1->id, $type); $requestid = $datarequest->get('id'); // Add the full collection with contexts 2, and 3. api::add_request_contexts_with_status($collection, $requestid, \tool_dataprivacy\contextlist_context::STATUS_PENDING); // Mark it as approved. api::update_request_contexts_with_status($requestid, \tool_dataprivacy\contextlist_context::STATUS_APPROVED); // Fetch the list. $approvedcollection = api::get_approved_contextlist_collection_for_request($datarequest); return (object) [ 'contexts' => (object) [ 'unused' => [ $ctx1->id, ], 'used' => [ $ctx2->id, $ctx3->id, ], 'unprotected' => [ $ctx2->id, ], 'protected' => [ $ctx3->id, ], ], 'list' => $approvedcollection->get_contextlist_for_component('tool_dataprivacy'), ]; } }