first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,334 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Step definitions to generate database fixtures for the data privacy tool.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
use Behat\Gherkin\Node\TableNode as TableNode;
use Behat\Behat\Tester\Exception\PendingException as PendingException;
use tool_dataprivacy\api;
/**
* Step definitions to generate database fixtures for the data privacy tool.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_tool_dataprivacy extends behat_base {
/**
* Each element specifies:
* - The data generator suffix used.
* - The required fields.
* - The mapping between other elements references and database field names.
* @var array
*/
protected static $elements = array(
'categories' => array(
'datagenerator' => 'category',
'required' => array()
),
'purposes' => array(
'datagenerator' => 'purpose',
'required' => array()
),
);
/**
* Creates the specified element. More info about available elements in https://moodledev.io/general/development/tools/behat.
*
* @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/
*
* @param string $elementname The name of the entity to add
* @param TableNode $data
*/
public function the_following_data_categories_exist($elementname, TableNode $data) {
// Now that we need them require the data generators.
require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php');
if (empty(self::$elements[$elementname])) {
throw new PendingException($elementname . ' data generator is not implemented');
}
$datagenerator = testing_util::get_data_generator();
$dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy');
$elementdatagenerator = self::$elements[$elementname]['datagenerator'];
$requiredfields = self::$elements[$elementname]['required'];
if (!empty(self::$elements[$elementname]['switchids'])) {
$switchids = self::$elements[$elementname]['switchids'];
}
foreach ($data->getHash() as $elementdata) {
// Check if all the required fields are there.
foreach ($requiredfields as $requiredfield) {
if (!isset($elementdata[$requiredfield])) {
throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
}
}
// Switch from human-friendly references to ids.
if (isset($switchids)) {
foreach ($switchids as $element => $field) {
$methodname = 'get_' . $element . '_id';
// Not all the switch fields are required, default vars will be assigned by data generators.
if (isset($elementdata[$element])) {
// Temp $id var to avoid problems when $element == $field.
$id = $this->{$methodname}($elementdata[$element]);
unset($elementdata[$element]);
$elementdata[$field] = $id;
}
}
}
// Preprocess the entities that requires a special treatment.
if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
$elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
}
// Creates element.
$methodname = 'create_' . $elementdatagenerator;
if (method_exists($dataprivacygenerator, $methodname)) {
// Using data generators directly.
$dataprivacygenerator->{$methodname}($elementdata);
} else if (method_exists($this, 'process_' . $elementdatagenerator)) {
// Using an alternative to the direct data generator call.
$this->{'process_' . $elementdatagenerator}($elementdata);
} else {
throw new PendingException($elementname . ' data generator is not implemented');
}
}
}
/**
* Sets the data category and data storage purpose for the site.
*
* @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_site_category_and_purpose($category, $purpose) {
$category = \tool_dataprivacy\category::get_record(['name' => $category]);
$purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]);
$data = (object)[
'contextlevel' => CONTEXT_SYSTEM,
'categoryid' => $category->get('id'),
'purposeid' => $purpose->get('id'),
];
api::set_contextlevel($data);
}
/**
* Sets the data category and data storage purpose for a course category instance.
*
* @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the name or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) {
global $DB;
$params = [
'name' => $name,
'idnumber' => $name,
];
$select = 'name = :name OR idnumber = :idnumber';
$coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST);
$context = context_coursecat::instance($coursecatid);
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the data category and data storage purpose for a course instance.
*
* @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the fullname or the shortname, or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) {
global $DB;
$params = [
'shortname' => $name,
'fullname' => $name,
'idnumber' => $name,
];
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
$context = context_course::instance($courseid);
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the data category and data storage purpose for a course instance.
*
* @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the name of the activity.
* @param string $type The activity type. E.g. assign, quiz, forum, etc.
* @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) {
global $DB;
$params = [
'shortname' => $coursename,
'fullname' => $coursename,
'idnumber' => $coursename,
];
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
$cmid = null;
$cms = get_coursemodules_in_course($type, $courseid);
foreach ($cms as $cm) {
if ($cm->name === $name || $cm->idnumber === $name) {
$cmid = $cm->id;
break;
}
}
if ($cmid === null) {
throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!");
}
$context = context_module::instance($cmid);
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the data category and data storage purpose for a course instance.
*
* @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
*
* @param string $name The instance name. It should match the name of the block. (e.g. online_users)
* @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
* @param string $category The ID of the category to be set for the instance.
* @param string $purpose The ID of the purpose to be set for the instance.
*/
public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) {
global $DB;
$params = [
'shortname' => $coursename,
'fullname' => $coursename,
'idnumber' => $coursename,
];
$select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
$courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
// Fetch the course context.
$coursecontext = context_course::instance($courseid);
// Fetch the block record and context.
$blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]);
$context = context_block::instance($blockid);
// Set the category and purpose.
$this->set_category_and_purpose($context->id, $category, $purpose);
}
/**
* Sets the category and purpose for a context instance.
*
* @param int $contextid The context ID.
* @param int $categoryname The category name.
* @param int $purposename The purpose name.
* @throws coding_exception
*/
protected function set_category_and_purpose($contextid, $categoryname, $purposename) {
$category = \tool_dataprivacy\category::get_record(['name' => $categoryname]);
$purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]);
api::set_context_instance((object) [
'contextid' => $contextid,
'purposeid' => $purpose->get('id'),
'categoryid' => $category->get('id'),
]);
}
/**
* Create a dataprivacy request.
*
* @Given /^I create a dataprivacy "(?P<type_string>(?:[^"]|\\")*)" request for "(?P<user_string>(?:[^"]|\\")*)"$/
*
* @param string $type The type of request to create (delete, or export)
* @param string $username The username to create for
*/
public function i_create_a_dataprivacy_request_for($type, $username) {
if ($type === 'delete') {
$type = \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE;
} else if ($type === 'export') {
$type = \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT;
} else {
throw new \Behat\Behat\Tester\Exception\ExpectationException("Unknown request type '{$type}'");
}
$user = \core_user::get_user_by_username($username);
\tool_dataprivacy\api::create_data_request($user->id, $type);
}
/**
* Approve a dataprivacy request.
*
* @Given /^I approve a dataprivacy "(?P<type_string>(?:[^"]|\\")*)" request for "(?P<user_string>(?:[^"]|\\")*)"$/
*
* @param string $type The type of request to create (delete, or export)
* @param string $username The username to create for
*/
public function i_approve_a_dataprivacy_request_for($type, $username) {
if ($type === 'delete') {
$type = \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE;
} else if ($type === 'export') {
$type = \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT;
} else {
throw new \Behat\Behat\Tester\Exception\ExpectationException("Unknown request type '{$type}'");
}
$user = \core_user::get_user_by_username($username);
$request = \tool_dataprivacy\data_request::get_record([
'userid' => $user->id,
'type' => $type,
'status' => \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL,
]);
\tool_dataprivacy\api::approve_data_request($request->get('id'));
}
}
@@ -0,0 +1,28 @@
@tool @tool_dataprivacy
Feature: Contact the privacy officer
As a user
In order to reach out to the site's privacy officer
I need to be able to contact the site's privacy officer in Moodle
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | s1@example.com |
@javascript
Scenario: Contacting the privacy officer
Given the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
When I log in as "student1"
And I follow "Profile" in the user menu
And I click on "Contact the privacy officer" "link"
And I set the field "Message" to "Hello DPO!"
And I click on "Send" "button" in the "Contact the privacy officer" "dialogue"
Then I should see "Your request has been submitted to the privacy officer"
And I click on "Data requests" "link"
And I should see "Hello DPO!" in the "General enquiry" "table_row"
Scenario: Contacting the privacy officer when not enabled
When I log in as "student1"
And I follow "Profile" in the user menu
Then "Contact the privacy officer" "link" should not exist
@@ -0,0 +1,283 @@
@tool @tool_dataprivacy
Feature: Data delete from the privacy API
In order to delete data for users and meet legal requirements
As an admin, user, or parent
I need to be able to request a user and their data data be deleted
Background:
Given the following "users" exist:
| username | firstname | lastname |
| victim | Victim User | 1 |
| parent | Long-suffering | Parent |
| privacyofficer | Privacy Officer | One |
And the following "roles" exist:
| shortname | name | archetype |
| tired | Tired | |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | |
| tool/dataprivacy:makedatadeletionrequestsforchildren | Allow | tired | System | |
| tool/dataprivacy:managedatarequests | Allow | manager | System | |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| parent | tired | User | victim |
And the following "system role assigns" exist:
| user | role | contextlevel |
| privacyofficer | manager | User |
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
And the following data privacy "categories" exist:
| name |
| Site category |
And the following data privacy "purposes" exist:
| name | retentionperiod |
| Site purpose | P10Y |
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
| privacyrequestexpiry | 55 | tool_dataprivacy |
| dporoles | 1 | tool_dataprivacy |
And I set the site category and purpose to "Site category" and "Site purpose"
@javascript
Scenario: As admin, delete a user and their data
Given I log in as "victim"
And I should see "Victim User 1"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I wait until "Approve request" "button" exists
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Deleted" in the "Victim User 1" "table_row"
And I log out
And I log in as "victim"
And I should see "Invalid login"
@javascript
Scenario: As a student, request deletion of account and data
Given I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
Then I should see "Delete all of my personal data"
And I should see "Awaiting approval" in the "Delete all of my personal data" "table_row"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I wait until "Approve request" "button" exists
And I press "Approve request"
And I log out
And I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I should see "Approved" in the "Delete all of my personal data" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Your session has timed out"
And I log in as "victim"
And I should see "Invalid login"
And I log in as "admin"
And I am on site homepage
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Deleted"
@javascript
Scenario: As a parent, request account and data deletion for my child
Given I log in as "parent"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I wait until "Approve request" "button" exists
And I press "Approve request"
And I log out
And I log in as "parent"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "You don't have any personal data requests"
@javascript
Scenario: As a Privacy Officer, I cannot create data deletion request unless I have permission.
Given I log in as "privacyofficer"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I open the autocomplete suggestions list
And I click on "Victim User 1" item in the autocomplete list
Then I should see "Export all of my personal data"
And "Type" "select" should not be visible
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:requestdeleteforotheruser | Allow | manager | System | |
And I reload the page
And I open the autocomplete suggestions list
And I click on "Victim User 1" item in the autocomplete list
And "Type" "select" should be visible
@javascript
Scenario: As a student, I cannot create data deletion request unless I have permission.
Given I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
Then "Type" "select" should exist
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:requestdelete | Prevent | user | System | |
And I reload the page
And I should see "Export all of my personal data"
And "Type" "select" should not exist
@javascript
Scenario: As a parent, I cannot create data deletion request unless I have permission.
Given I log in as "parent"
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:makedatadeletionrequestsforchildren | Prevent | tired | System | victim |
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I open the autocomplete suggestions list
And I click on "Victim User 1" item in the autocomplete list
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
And I should see "You don't have permission to create deletion request for this user."
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:makedatadeletionrequestsforchildren | Allow | tired | System | victim |
| tool/dataprivacy:requestdelete | Prevent | user | System | |
And I open the autocomplete suggestions list
And I click on "Long-suffering Parent" item in the autocomplete list
And I press "Save changes"
And I should see "You don't have permission to create deletion request for yourself."
@javascript
Scenario: As a student, link to create data deletion should not be shown if I don't have permission.
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:requestdelete | Prohibit | user | System | |
When I log in as "victim"
And I follow "Profile" in the user menu
Then I should not see "Delete my account"
@javascript
Scenario: As a primary admin, the link to create a data deletion request should not be shown.
Given I log in as "admin"
When I follow "Profile" in the user menu
Then I should not see "Delete my account"
@javascript
Scenario: As a Privacy Officer, I cannot Approve to Deny deletion data request without permission.
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:requestdeleteforotheruser | Allow | manager | System | |
When I log in as "privacyofficer"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I open the autocomplete suggestions list
And I click on "Victim User 1" item in the autocomplete list
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:requestdeleteforotheruser | Prohibit | manager | System | |
And I reload the page
Then ".selectrequests" "css_element" should not exist
And I open the action menu in "region-main" "region"
And I should not see "Approve request"
And I should not see "Deny request"
And I choose "View the request" in the open action menu
And "Approve" "button" should not exist
And "Deny" "button" should not exist
@javascript
Scenario: As a Privacy Officer, I cannot re-submit deletion data request without permission.
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:requestdeleteforotheruser | Allow | manager | System | |
When I log in as "privacyofficer"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I open the autocomplete suggestions list
And I click on "Victim User 1" item in the autocomplete list
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
And I open the action menu in "region-main" "region"
And I follow "Deny request"
And I press "Deny request"
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:requestdeleteforotheruser | Prohibit | manager | System | |
And I reload the page
And I open the action menu in "region-main" "region"
Then I should not see "Resubmit as new request"
Scenario: Request data deletion as student with automatic approval turned on
Given the following config values are set as admin:
| automaticdatadeletionapproval | 1 | tool_dataprivacy |
And I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Delete my account"
When I press "Save changes"
Then I should see "Your request has been submitted and will be processed soon."
And I should see "Approved" in the "Delete all of my personal data" "table_row"
@javascript
Scenario: Delete flow stay the same even allow filtering of exports by course setting is enabled.
Given the following config values are set as admin:
| allowfiltering | 1 | tool_dataprivacy |
And I log in as "victim"
And I should see "Victim User 1"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I set the field "Type" to "Delete all of my personal data"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Deleted" in the "Victim User 1" "table_row"
And I log out
And I log in as "victim"
And I should see "Invalid login"
@@ -0,0 +1,273 @@
@tool @tool_dataprivacy
Feature: Data export from the privacy API
In order to export data for users and meet legal requirements
As an admin, user, or parent
I need to be able to export data for a user
Background:
Given the following "users" exist:
| username | firstname | lastname | institution |
| victim | Victim User | 1 | University1 |
| victim2 | Victim User | 2 | University2 |
| requester | The | Requester | University3 |
| parent | Long-suffering | Parent | |
And the following "roles" exist:
| shortname | name | archetype |
| tired | Tired | |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | |
| tool/dataprivacy:managedatarequests | Allow | manager | System | |
| moodle/site:viewuseridentity | Prevent | manager | System | |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| parent | tired | User | victim |
And the following "system role assigns" exist:
| user | role | contextlevel |
| requester | manager | User |
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
| privacyrequestexpiry | 55 | tool_dataprivacy |
| dporoles | 1 | tool_dataprivacy |
And the following data privacy "categories" exist:
| name |
| Site category |
And the following data privacy "purposes" exist:
| name | retentionperiod |
| Site purpose | P10Y |
And I set the site category and purpose to "Site category" and "Site purpose"
@javascript
Scenario: As admin, export data for a user and download it, unless it has expired
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I wait until "Approve request" "button" exists
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download a file that:
| Contains file in zip | index.html |
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Expired" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And I should not see "Download"
@javascript
Scenario: As a student, request data export and then download it when approved, unless it has expired
Given I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I press "Save changes"
Then I should see "Export all of my personal data"
And I should see "Awaiting approval" in the "Export all of my personal data" "table_row"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I press "Approve request"
And I log out
And I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I should see "Approved" in the "Export all of my personal data" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Export all of my personal data" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download a file that:
| Contains file in zip | index.html |
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
And I reload the page
And I should see "Expired" in the "Export all of my personal data" "table_row"
And I should not see "Actions"
@javascript
Scenario: As a parent, request data export for my child because I don't trust the little blighter
Given I log in as "parent"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request"
And I wait until "Approve request" "button" exists
And I press "Approve request"
And I log out
And I log in as "parent"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download a file that:
| Contains file in zip | index.html |
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
And I reload the page
And I should see "Expired" in the "Victim User 1" "table_row"
And I should not see "Actions"
@javascript
Scenario: Test search for user using extra field.
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/site:viewuseridentity | Allow | manager | System | |
And the following config values are set as admin:
| showuseridentity | institution |
And I log in as "requester"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "Search" to "University1"
Then I should see "Victim User 1"
When I reload the page
And I set the field "Search" to "University2"
Then I should see "Victim User 2"
Scenario: Request data export as student with automatic approval turned on
Given the following config values are set as admin:
| automaticdataexportapproval | 1 | tool_dataprivacy |
And I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Export all of my personal data"
When I press "Save changes"
Then I should see "Your request has been submitted and will be processed soon."
And I should see "Approved" in the "Export all of my personal data" "table_row"
@javascript
Scenario: As admin, enable allow filtering of exports by course setting, export data for a user and download it
Given the following config values are set as admin:
| allowfiltering | 1 | tool_dataprivacy |
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I should see "Export my personal data"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Pending" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request (all data)"
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download a file that:
| Contains file in zip | index.html |
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Expired" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And I should not see "Download"
@javascript
Scenario: As admin, enable allow filtering of exports by course setting, filter before export data for a user and download it
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
| Course 2 | C2 |
| Coruse 3 | C3 |
And the following "course enrolments" exist:
| user | course | role |
| victim | C1 | student |
| victim | C2 | student |
And the following config values are set as admin:
| allowfiltering | 1 | tool_dataprivacy |
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I run all adhoc tasks
And I reload the page
And I open the action menu in "Victim User 1" "table_row"
And I follow "Approve request (data from selected courses)"
And I should see "Course 1"
And I should see "Course 2"
And I should not see "Course 3"
And I press "Approve request"
And I should see "You must select at least one course"
And I set the field "Select courses to export" to "Course 1"
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download a file that:
| Contains file in zip | index.html |
@javascript
Scenario: Filter before export data for a user and download it in the view request action
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
| Course 2 | C2 |
| Coruse 3 | C3 |
And the following "course enrolments" exist:
| user | course | role |
| victim | C1 | student |
| victim | C2 | student |
And the following config values are set as admin:
| allowfiltering | 1 | tool_dataprivacy |
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "User" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I run all adhoc tasks
And I reload the page
And I open the action menu in "Victim User 1" "table_row"
And I follow "View the request"
And I press "Approve selected courses"
And I set the field "Select courses to export" to "Course 1"
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download a file that:
| Contains file in zip | index.html |
@@ -0,0 +1,34 @@
@tool @tool_dataprivacy @javascript
Feature: Manage data categories
As the privacy officer
In order to manage the data registry
I need to be able to manage the data categories for the data registry
Background:
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I open the action menu in "region-main" "region"
And I choose "Categories" in the open action menu
And I press "Add category"
And I set the field "Name" to "Category 1"
And I set the field "Description" to "Category 1 description"
When I click on "Save" "button" in the "Add category" "dialogue"
Then I should see "Category 1" in the "List of data categories" "table"
And I should see "Category 1 description" in the "Category 1" "table_row"
Scenario: Update a data category
Given I open the action menu in "Category 1" "table_row"
And I choose "Edit" in the open action menu
And I set the field "Name" to "Category 1 edited"
And I set the field "Description" to "Category 1 description edited"
When I press "Save changes"
Then I should see "Category 1 edited" in the "List of data categories" "table"
And I should see "Category 1 description edited" in the "List of data categories" "table"
Scenario: Delete a data category
Given I open the action menu in "Category 1" "table_row"
And I choose "Delete" in the open action menu
And I should see "Delete category"
And I should see "Are you sure you want to delete the category 'Category 1'?"
When I click on "Delete" "button" in the "Delete category" "dialogue"
Then I should not see "Category 1" in the "List of data categories" "table"
@@ -0,0 +1,146 @@
@tool @tool_dataprivacy
Feature: Manage data requests
As the privacy officer
In order to address the privacy-related requests
I need to be able to manage the data requests of the site's users
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | John | Doe | s1@example.com |
| student2 | Jane | Doe | s2@example.com |
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
@javascript
Scenario: Marking general enquiries as complete
Given I log in as "student1"
And I follow "Profile" in the user menu
And I should see "Contact the privacy officer"
And I click on "Contact the privacy officer" "link"
And I set the field "Message" to "Hi PO! Can others access my information on your site?"
And I click on "Send" "button" in the "Contact the privacy officer" "dialogue"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I log in as "student2"
And I follow "Profile" in the user menu
And I click on "Contact the privacy officer" "link"
And I set the field "Message" to "Dear Mr. Privacy Officer, I'd like to know more about GDPR. Thanks!"
And I click on "Send" "button" in the "Contact the privacy officer" "dialogue"
And I should see "Your request has been submitted to the privacy officer"
And I log out
When I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
Then I should see "Hi PO!" in the "John Doe" "table_row"
And I should see "Dear Mr. Privacy Officer" in the "Jane Doe" "table_row"
And I open the action menu in "John Doe" "table_row"
And I should see "View the request"
And I should see "Mark as complete"
And I choose "View the request" in the open action menu
And I should see "Hi PO! Can others access my information on your site?"
And I press "Mark as complete"
And I wait until the page is ready
And I should see "Complete" in the "John Doe" "table_row"
And I open the action menu in "John Doe" "table_row"
And I should see "View the request"
But I should not see "Mark as complete"
And I press the escape key
And I open the action menu in "Jane Doe" "table_row"
And I choose "Mark as complete" in the open action menu
And I should see "Do you really want to mark this user enquiry as complete?"
And I press "Mark as complete"
And I wait until the page is ready
And I should see "Complete" in the "Jane Doe" "table_row"
And I open the action menu in "Jane Doe" "table_row"
And I should see "View the request"
But I should not see "Mark as complete"
@javascript
Scenario: Bulk accepting requests
Given I log in as "student1"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment1"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I log in as "student2"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment2"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I trigger cron
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Comment1" in the "John Doe" "table_row"
And I should see "Awaiting approval" in the "John Doe" "table_row"
And I should see "Comment2" in the "Jane Doe" "table_row"
And I should see "Awaiting approval" in the "Jane Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
And I set the field with xpath "//select[@id='bulk-action']" to "Approve"
And I press "Confirm"
And I should see "Approve requests"
And I should see "Do you really want to bulk approve the selected data requests?"
When I press "Approve requests"
Then I should see "Approved" in the "John Doe" "table_row"
And I should see "Approved" in the "Jane Doe" "table_row"
@javascript
Scenario: Bulk denying requests
Given I log in as "student1"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment1"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I log in as "student2"
And I follow "Profile" in the user menu
And I should see "Data requests"
And I click on "Data requests" "link"
And I should see "New request"
And I click on "New request" "link"
And I should see "Type"
And I should see "Comments"
And I set the field "Type" to "Export all of my personal data"
And I set the field "Comments" to "Comment2"
And I press "Save changes"
And I should see "Your request has been submitted to the privacy officer"
And I log out
And I trigger cron
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Comment1" in the "John Doe" "table_row"
And I should see "Awaiting approval" in the "John Doe" "table_row"
And I should see "Comment2" in the "Jane Doe" "table_row"
And I should see "Awaiting approval" in the "Jane Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "John Doe" "table_row"
And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row"
And I set the field with xpath "//select[@id='bulk-action']" to "Deny"
And I press "Confirm"
And I should see "Deny requests"
And I should see "Do you really want to bulk deny the selected data requests?"
When I press "Deny requests"
Then I should see "Rejected" in the "John Doe" "table_row"
And I should see "Rejected" in the "Jane Doe" "table_row"
@@ -0,0 +1,373 @@
@tool @tool_dataprivacy @javascript
Feature: Manage data registry defaults
As the privacy officer
In order to manage the data registry
I need to be able to manage the default data categories and data storage purposes for various context levels.
Background:
Given I log in as "admin"
And the following "categories" exist:
| name | idnumber | category |
| Science and technology | scitech | |
| Physics | st-phys | scitech |
And the following "courses" exist:
| fullname | shortname | category |
| Fundamentals of physics 1 | Physics 101 | st-phys |
And the following "activities" exist:
| activity | name | idnumber | course |
| assign | Assignment 1 | assign1 | Physics 101 |
| forum | Forum 1 | forum1 | Physics 101 |
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| online_users | Course | Physics 101 | course-view-* | site-post |
And the following data privacy "categories" exist:
| name |
| Site category |
| Category 1 |
| Category 2 |
And the following data privacy "purposes" exist:
| name | retentionperiod |
| Site purpose | P10Y |
| Purpose 1 | P3Y |
| Purpose 2 | P5Y |
And I set the site category and purpose to "Site category" and "Site purpose"
# Setting a default for course categories should apply to everything beneath that category.
Scenario: Set course category data registry defaults
Given I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I should see "Inherit"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years"
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "3 years"
# When Setting a default for course categories, and overriding a specific category, only that category and its
# children will be overridden.
# If any child is a course category, it will get the default.
Scenario: Set course category data registry defaults with override
Given I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I press "Save changes"
And I should see "Category 1"
And I should see "Purpose 1"
And I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
When I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
Then the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years"
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
# Physics 101 is also a category, so it will get the category default.
And I click on "Physics 101" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "3 years"
# When overriding a specific category, only that category and its children will be overridden.
Scenario: Set course category data registry defaults with override
Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
When I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
Then the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years"
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
# Physics 101 is also a category, so it will get the category default.
And I click on "Physics 101" "link"
And I wait until the page is ready
And I should see "5 years"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "5 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "5 years"
# Resetting instances removes custom values.
Scenario: Set course category data registry defaults with override
Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I click on "Reset instances with custom values" "checkbox"
And I press "Save changes"
And I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
Then the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years"
Scenario: Set course data registry defaults
Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Courses" "link" in the "#region-main" "css_element"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "5 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "5 years"
Scenario: Set course data registry defaults with override
Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Courses" "link" in the "#region-main" "css_element"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I should see "3 years"
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And I should see "3 years"
Scenario: Set module level data registry defaults
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
Scenario: Set module level data registry defaults with override
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I click on "Forum 1 (Forum)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
Scenario: Set data registry defaults for an activity module
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Add a new module default"
And I set the field "Activity module" to "Assignment"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1" in the "Assignment" "table_row"
And I should see "Purpose 1" in the "Assignment" "table_row"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
Scenario: Set data registry defaults for an activity module with override
Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Activity modules" "link"
And I should see "Inherit"
And I should see "Add a new module default"
And I press "Add a new module default"
And I set the field "Activity module" to "Assignment"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1" in the "Assignment" "table_row"
And I should see "Purpose 1" in the "Assignment" "table_row"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Activities and resources" "link"
And I wait until the page is ready
And I click on "Assignment 1 (Assignment)" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
Scenario: Set block category data registry defaults
Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Blocks" "link"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Blocks" "link"
And I wait until the page is ready
And I click on "Online users" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Category 2"
And the field "purposeid" matches value "Purpose 2"
And I should see "5 years (after the course end date)"
Scenario: Set course category data registry defaults with override
Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Set defaults" "link"
And I click on "Blocks" "link"
And I should see "Inherit"
And I should not see "Add a new module default"
And I press "Edit"
And I set the field "Category" to "Category 1"
And I set the field "Purpose" to "Purpose 1"
And I click on "Reset instances with custom values" "checkbox"
When I press "Save changes"
Then I should see "Category 1"
And I should see "Purpose 1"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Science and technology" "link"
And I wait until the page is ready
And I click on "Courses" "link" in the ".data-registry" "css_element"
And I wait until the page is ready
And I click on "Physics 101" "link"
And I wait until the page is ready
And I click on "Blocks" "link"
And I wait until the page is ready
And I click on "Online users" "link"
And I wait until the page is ready
And the field "categoryid" matches value "Not set (use the default value)"
And the field "purposeid" matches value "Not set (use the default value)"
And I should see "3 years (after the course end date)"
@@ -0,0 +1,51 @@
@tool @tool_dataprivacy @javascript
Feature: Manage data storage purposes
As the privacy officer
In order to manage the data registry
I need to be able to manage the data storage purposes for the data registry
Background:
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I open the action menu in "region-main" "region"
And I choose "Purposes" in the open action menu
And I press "Add purpose"
And I set the following fields to these values:
| Name | Purpose 1 |
| Description | Purpose 1 description |
| Lawful bases | Contract (GDPR Art. 6.1(b)),Legal obligation (GDPR Art 6.1(c)) |
| Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a)) |
| retentionperiodnumber | 2 |
When I press "Save"
Then I should see "Purpose 1" in the "List of data purposes" "table"
And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row"
And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row"
And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row"
And I should see "2 years" in the "Purpose 1" "table_row"
And "Purpose 1 Purpose 1 description" row "5" column of "List of data purposes" table should contain "No"
Scenario: Update a data storage purpose
Given I open the action menu in "Purpose 1" "table_row"
And I choose "Edit" in the open action menu
And I set the following fields to these values:
| Name | Purpose 1 edited |
| Description | Purpose 1 description edited |
| Lawful bases | Contract (GDPR Art. 6.1(b)), Vital interests (GDPR Art. 6.1(d)) |
| Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a)) |
| retentionperiodnumber | 3 |
| protected | 1 |
When I press "Save changes"
Then I should see "Purpose 1 edited" in the "List of data purposes" "table"
And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row"
And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row"
And I should see "3 years" in the "Purpose 1 edited" "table_row"
But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row"
And "Purpose 1 edited Purpose 1 description edited" row "5" column of "List of data purposes" table should not contain "No"
Scenario: Delete a data storage purpose
Given I open the action menu in "Purpose 1" "table_row"
And I choose "Delete" in the open action menu
And I should see "Delete purpose"
And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
When I click on "Delete" "button" in the "Delete purpose" "dialogue"
Then I should not see "Purpose 1" in the "List of data purposes" "table"
@@ -0,0 +1,26 @@
@tool @tool_dataprivacy
Feature: Manage my own data requests
In order to manage my own data requests
As a user
I need to be able to view and cancel all my data requests
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | s1@example.com |
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
@javascript
Scenario: Cancel my own data request
Given I log in as "student1"
And I follow "Profile" in the user menu
And I click on "Contact the privacy officer" "link"
And I set the field "Message" to "Hello DPO!"
And I click on "Send" "button" in the "Contact the privacy officer" "dialogue"
And I should see "Your request has been submitted to the privacy officer"
When I click on "Data requests" "link"
And I open the action menu in "Hello DPO!" "table_row"
And I choose "Cancel" in the open action menu
And I click on "Cancel request" "button" in the "Cancel request" "dialogue"
Then I should see "Cancelled" in the "Hello DPO!" "table_row"
@@ -0,0 +1,68 @@
@tool @tool_dataprivacy
Feature: Protected data should not be deleted
In order to delete data for users and meet legal requirements
As an privacy office
I need to be ensure that only expired or unprotected data is removed
Background:
Given the following "users" exist:
| username | firstname | lastname |
| u1 | u1 | u1 |
And the following "courses" exist:
| fullname | shortname | startdate | enddate |
| C1 | C1 | ##1 year ago## | ##1 month ago## |
| C2 | C2 | ##1 year ago## | ##last day of next month## |
And the following "course enrolments" exist:
| user | course | role |
| u1 | C1 | student |
| u1 | C2 | student |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| forum | forump1 | Test forum description | C1 | forump1 |
| forum | forumu1 | Test forum description | C1 | forumu1 |
| forum | forump2 | Test forum description | C2 | forump2 |
| forum | forumu2 | Test forum description | C2 | forumu2 |
And the following data privacy "categories" exist:
| name |
| CAT |
And the following data privacy "purposes" exist:
| name | retentionperiod | protected |
| Site purpose | PT1H | 0 |
| prot | P1D | 1 |
| unprot | P1D | 0 |
And the following "mod_forum > discussions" exist:
| user | forum | name | message |
| u1 | forump1 | Discussion subject | Test post in forump1 |
| u1 | forumu1 | Discussion subject | Test post in forumu1 |
| u1 | forump2 | Discussion subject | Test post in forump2 |
| u1 | forumu2 | Discussion subject | Test post in forumu2 |
And I set the category and purpose for the "forump1" "forum" in course "C1" to "CAT" and "prot"
And I set the category and purpose for the "forump2" "forum" in course "C2" to "CAT" and "prot"
And I set the category and purpose for the "forumu1" "forum" in course "C1" to "CAT" and "unprot"
And I set the category and purpose for the "forumu2" "forum" in course "C2" to "CAT" and "unprot"
And I set the site category and purpose to "CAT" and "Site purpose"
@javascripta
Scenario: Unexpired and protected data is not removed
Given I log in as "admin"
And I create a dataprivacy "delete" request for "u1"
And I approve a dataprivacy "delete" request for "u1"
And I run all adhoc tasks
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I should see "Deleted" in the "u1" "table_row"
And I am on the "forump1" "forum activity" page
And I follow "Discussion subject"
Then I should not see "Test post in forump1"
When I am on the "forumu1" "forum activity" page
And I follow "Discussion subject"
Then I should not see "Test post in forumu1"
And I am on the "forump2" "forum activity" page
And I follow "Discussion subject"
Then I should see "Test post in forump2"
When I am on the "forumu2" "forum activity" page
And I follow "Discussion subject"
Then I should not see "Test post in forumu2"
@@ -0,0 +1,29 @@
@core @tool @tool_dataprivacy @javascript
Feature: Verify the breadcrumbs in different privacy site administration pages
Whenever I navigate to data registry page in site administration
As an admin
The breadcrumbs should be visible
Background:
Given I log in as "admin"
Scenario: Verify the breadcrumbs in data registry page as an admin
Given I navigate to "Users > Privacy and policies > Data registry" in site administration
And "Data registry" "text" should exist in the ".breadcrumb" "css_element"
And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element"
When I click on "Set defaults" "link"
Then "Set defaults" "text" should exist in the ".breadcrumb" "css_element"
And "Data registry" "link" should exist in the ".breadcrumb" "css_element"
And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element"
And I navigate to "Users > Privacy and policies > Data registry" in site administration
And I click on "Edit" "link"
And I choose "Categories" in the open action menu
And "Edit categories" "text" should exist in the ".breadcrumb" "css_element"
And "Data registry" "link" should exist in the ".breadcrumb" "css_element"
And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element"
And I click on "Back" "link"
And I click on "Edit" "link"
And I choose "Purposes" in the open action menu
And "Edit purposes" "text" should exist in the ".breadcrumb" "css_element"
And "Data registry" "link" should exist in the ".breadcrumb" "css_element"
And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element"
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Parent class for tests which need data privacy functionality.
*
* @package tool_dataprivacy
* @copyright 2018 Michael Hawkins
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Parent class for tests which need data privacy functionality.
*
* @package tool_dataprivacy
* @copyright 2018 Michael Hawkins
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class data_privacy_testcase extends advanced_testcase {
/**
* Assign one or more user IDs as site DPO
*
* @param stdClass|array $users User ID or array of user IDs to be assigned as site DPO
* @return void
*/
protected function assign_site_dpo($users) {
global $DB;
$this->resetAfterTest();
if (!is_array($users)) {
$users = array($users);
}
$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 user(s) as manager.
foreach ($users as $user) {
role_assign($managerroleid, $user->id, $context->id);
}
// Only map the manager role to the DPO role.
set_config('dporoles', $managerroleid, 'tool_dataprivacy');
}
}
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy;
/**
* Unit tests for the data_registry class.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_registry_test extends \advanced_testcase {
/**
* Ensure that the get_effective_context_value only errors if provided an inappropriate element.
*
* This test is not great because we only test a limited set of values. This is a fault of the underlying API.
*/
public function test_get_effective_context_value_invalid_element(): void {
$this->expectException(\coding_exception::class);
data_registry::get_effective_context_value(\context_system::instance(), 'invalid');
}
/**
* Ensure that the get_effective_contextlevel_value only errors if provided an inappropriate element.
*
* This test is not great because we only test a limited set of values. This is a fault of the underlying API.
*/
public function test_get_effective_contextlevel_value_invalid_element(): void {
$this->expectException(\coding_exception::class);
data_registry::get_effective_contextlevel_value(\context_system::instance(), 'invalid');
}
}
@@ -0,0 +1,235 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy;
use data_privacy_testcase;
defined('MOODLE_INTERNAL') || die();
require_once('data_privacy_testcase.php');
/**
* Tests for the data_request persistent.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class data_request_test extends data_privacy_testcase {
/**
* Data provider for testing is_resettable, and is_active.
*
* @return array
*/
public function status_state_provider(): array {
return [
[
'state' => api::DATAREQUEST_STATUS_PENDING,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_AWAITING_APPROVAL,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_APPROVED,
'resettable' => true,
'active' => true,
],
[
'state' => api::DATAREQUEST_STATUS_PROCESSING,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_COMPLETE,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_CANCELLED,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_REJECTED,
'resettable' => true,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_DOWNLOAD_READY,
'resettable' => false,
'active' => false,
],
[
'state' => api::DATAREQUEST_STATUS_EXPIRED,
'resettable' => false,
'active' => false,
],
];
}
/**
* Test the pseudo states of a data request with an export request.
*
* @dataProvider status_state_provider
* @param int $status
* @param bool $resettable
* @param bool $active
*/
public function test_pseudo_states_export(int $status, bool $resettable, bool $active): void {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$uut->set('type', api::DATAREQUEST_TYPE_EXPORT);
$this->assertEquals($resettable, $uut->is_resettable());
$this->assertEquals($active, $uut->is_active());
}
/**
* Test the pseudo states of a data request with a delete request.
*
* @dataProvider status_state_provider
* @param int $status
* @param bool $resettable
* @param bool $active
*/
public function test_pseudo_states_delete(int $status, bool $resettable, bool $active): void {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
$this->assertEquals($resettable, $uut->is_resettable());
$this->assertEquals($active, $uut->is_active());
}
/**
* Test the pseudo states of a data request.
*
* @dataProvider status_state_provider
* @param int $status
*/
public function test_can_reset_others($status): void {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$uut->set('type', api::DATAREQUEST_TYPE_OTHERS);
$this->assertFalse($uut->is_resettable());
}
/**
* Data provider for states which are not resettable.
*
* @return array
*/
public function non_resettable_provider(): array {
$states = [];
foreach ($this->status_state_provider() as $thisstatus) {
if (!$thisstatus['resettable']) {
$states[] = $thisstatus;
}
}
return $states;
}
/**
* Ensure that requests which are not resettable cause an exception to be thrown.
*
* @dataProvider non_resettable_provider
* @param int $status
*/
public function test_non_resubmit_request($status): void {
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', $status);
$this->expectException(\moodle_exception::class);
$this->expectExceptionMessage(get_string('cannotreset', 'tool_dataprivacy'));
$uut->resubmit_request();
}
/**
* Ensure that a rejected request can be reset.
*/
public function test_resubmit_request(): void {
$this->resetAfterTest();
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', api::DATAREQUEST_STATUS_REJECTED);
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
$uut->set('comments', 'Foo');
$uut->set('requestedby', 42);
$uut->set('dpo', 98);
$newrequest = $uut->resubmit_request();
$this->assertEquals('Foo', $newrequest->get('comments'));
$this->assertEquals(42, $newrequest->get('requestedby'));
$this->assertEquals(98, $newrequest->get('dpo'));
$this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $newrequest->get('status'));
$this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
$this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
}
/**
* Ensure that an active request can be reset.
*/
public function test_resubmit_active_request(): void {
$this->resetAfterTest();
$uut = new \tool_dataprivacy\data_request();
$uut->set('status', api::DATAREQUEST_STATUS_APPROVED);
$uut->set('type', api::DATAREQUEST_TYPE_DELETE);
$uut->set('comments', 'Foo');
$uut->set('requestedby', 42);
$uut->set('dpo', 98);
$newrequest = $uut->resubmit_request();
$this->assertEquals('Foo', $newrequest->get('comments'));
$this->assertEquals(42, $newrequest->get('requestedby'));
$this->assertEquals(98, $newrequest->get('dpo'));
$this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $newrequest->get('status'));
$this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type'));
$this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status'));
}
/**
* Create a data request for the user.
*
* @param int $userid
* @param int $type
* @param int $status
* @return data_request
*/
public function create_request_for_user_with_status(int $userid, int $type, int $status): data_request {
$request = new data_request(0, (object) [
'userid' => $userid,
'type' => $type,
'status' => $status,
]);
$request->save();
return $request;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,238 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy;
use data_privacy_testcase;
defined('MOODLE_INTERNAL') || die();
require_once('data_privacy_testcase.php');
/**
* Expired data requests tests.
*
* @package tool_dataprivacy
* @covers \tool_dataprivacy\data_request
* @copyright 2018 Michael Hawkins
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class expired_data_requests_test extends data_privacy_testcase {
/**
* Test tearDown.
*/
public function tearDown(): void {
\core_privacy\local\request\writer::reset();
}
/**
* Test finding and deleting expired data requests
*/
public function test_data_request_expiry(): void {
global $DB;
$this->resetAfterTest();
\core_privacy\local\request\writer::setup_real_writer_instance();
// Set up test users.
$this->setAdminUser();
$studentuser = $this->getDataGenerator()->create_user();
$studentusercontext = \context_user::instance($studentuser->id);
$dpouser = $this->getDataGenerator()->create_user();
$this->assign_site_dpo($dpouser);
// Set site purpose.
$this->create_system_purpose();
// Set request expiry to 5 minutes.
set_config('privacyrequestexpiry', 300, 'tool_dataprivacy');
// Create and approve data request.
$this->setUser($studentuser->id);
$datarequest = api::create_data_request($studentuser->id, api::DATAREQUEST_TYPE_EXPORT);
$requestid = $datarequest->get('id');
$this->setAdminUser();
api::approve_data_request($requestid);
$this->setUser();
ob_start();
$this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
ob_end_clean();
// Confirm approved and exported.
$request = new data_request($requestid);
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
$fileconditions = array(
'userid' => $studentuser->id,
'component' => 'tool_dataprivacy',
'filearea' => 'export',
'itemid' => $requestid,
'contextid' => $studentusercontext->id,
);
$this->assertEquals(2, $DB->count_records('files', $fileconditions));
// Run expiry deletion - should not affect test export.
$expiredrequests = data_request::get_expired_requests();
$this->assertEquals(0, count($expiredrequests));
data_request::expire($expiredrequests);
// Confirm test export was not deleted.
$request = new data_request($requestid);
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
$this->assertEquals(2, $DB->count_records('files', $fileconditions));
// Change request expiry to 1 second and allow it to elapse.
set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
$this->waitForSecond();
// Re-run expiry deletion, confirm the request expires and export is deleted.
$expiredrequests = data_request::get_expired_requests();
$this->assertEquals(1, count($expiredrequests));
data_request::expire($expiredrequests);
$request = new data_request($requestid);
$this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
$this->assertEquals(0, $DB->count_records('files', $fileconditions));
}
/**
* Test that data requests are not expired when expiration is disabled (set to zero)
*/
public function test_data_request_expiry_never(): void {
global $DB;
$this->resetAfterTest();
\core_privacy\local\request\writer::setup_real_writer_instance();
// Disable request expiry.
set_config('privacyrequestexpiry', 0, 'tool_dataprivacy');
// Create and approve data request.
$user = $this->getDataGenerator()->create_user();
$usercontext = \context_user::instance($user->id);
$this->setUser($user->id);
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
$requestid = $datarequest->get('id');
$this->setAdminUser();
api::approve_data_request($requestid);
ob_start();
$this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
ob_end_clean();
// Run expiry deletion - should not affect test export.
$expiredrequests = data_request::get_expired_requests();
$this->assertEmpty($expiredrequests);
data_request::expire($expiredrequests);
// Confirm approved and exported.
$request = new data_request($requestid);
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
$fileconditions = [
'userid' => $user->id,
'component' => 'tool_dataprivacy',
'filearea' => 'export',
'itemid' => $requestid,
'contextid' => $usercontext->id,
];
$this->assertEquals(2, $DB->count_records('files', $fileconditions));
}
/**
* Test for \tool_dataprivacy\data_request::is_expired()
* Tests for the expected request status to protect from false positive/negative,
* then tests is_expired() is returning the expected response.
*/
public function test_is_expired(): void {
$this->resetAfterTest();
\core_privacy\local\request\writer::setup_real_writer_instance();
// Set request expiry beyond this test.
set_config('privacyrequestexpiry', 20, 'tool_dataprivacy');
$admin = get_admin();
$this->setAdminUser();
// Set site purpose.
$this->create_system_purpose();
// Create export request.
$datarequest = api::create_data_request($admin->id, api::DATAREQUEST_TYPE_EXPORT);
$requestid = $datarequest->get('id');
// Approve the request.
ob_start();
$this->setAdminUser();
api::approve_data_request($requestid);
$this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
ob_end_clean();
// Test Download ready (not expired) response.
$request = new data_request($requestid);
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
$result = data_request::is_expired($request);
$this->assertFalse($result);
// Let request expiry time lapse.
set_config('privacyrequestexpiry', 1, 'tool_dataprivacy');
$this->waitForSecond();
// Test Download ready (time expired) response.
$request = new data_request($requestid);
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
$result = data_request::is_expired($request);
$this->assertTrue($result);
// Run the expiry task to properly expire the request.
ob_start();
$task = \core\task\manager::get_scheduled_task('\tool_dataprivacy\task\delete_expired_requests');
$task->execute();
ob_end_clean();
// Test Expired response status response.
$request = new data_request($requestid);
$this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status'));
$result = data_request::is_expired($request);
$this->assertTrue($result);
}
/**
* Create a site (system context) purpose and category.
*
* @return void
*/
protected function create_system_purpose() {
$purpose = new purpose(0, (object) [
'name' => 'Test purpose ' . rand(1, 1000),
'retentionperiod' => 'P1D',
'lawfulbases' => 'gdpr_art_6_1_a',
]);
$purpose->create();
$cat = new category(0, (object) ['name' => 'Test category']);
$cat->create();
$record = (object) [
'purposeid' => $purpose->get('id'),
'categoryid' => $cat->get('id'),
'contextlevel' => CONTEXT_SYSTEM,
];
api::set_contextlevel($record);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,88 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the external method for submit selected courses.
*
* @package tool_dataprivacy
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use tool_dataprivacy\api;
/**
* External function submit_selected_courses_form_test.
*
* @package tool_dataprivacy
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \tool_dataprivacy\api
*/
class submit_selected_courses_form_test extends \externallib_advanced_testcase {
/**
* Test for submit_selected_courses_form().
*/
public function test_submit_selected_courses_form(): void {
global $DB;
$this->resetAfterTest();
set_config('allowfiltering', 1, 'tool_dataprivacy');
$generator = new \testing_data_generator();
$s1 = $generator->create_user();
$s1->ignoresesskey = true;
$u1 = $generator->create_user();
$u1->ignoresesskey = true;
$context = \context_system::instance();
$course = $this->getDataGenerator()->create_course([]);
$coursecontext1 = \context_course::instance($course->id);
$this->getDataGenerator()->enrol_user($s1->id, $course->id, 'student');
// 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);
$jsonstring = "requestid=" . $requestid . "&sesskey=" . sesskey() .
"&_qf__tool_dataprivacy_form_exportfilter_form=1&coursecontextids%5B%5D=" . $coursecontext1->id;
$results = submit_selected_courses_form::execute($requestid, json_encode($jsonstring));
$this->assertTrue($results["result"]);
}
}
@@ -0,0 +1,92 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy;
/**
* Unit tests for the filtered_userlist.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filtered_userlist_test extends \advanced_testcase {
/**
* Test the apply_expired_contexts_filters function with arange of options.
*
* @dataProvider apply_expired_contexts_filters_provider
* @param array $initial The set of userids in the initial filterlist.
* @param array $expired The set of userids considered as expired.
* @param array $unexpired The set of userids considered as unexpired.
* @param array $expected The expected values.
*/
public function test_apply_expired_contexts_filters(array $initial, array $expired, array $unexpired, array $expected): void {
$userlist = $this->getMockBuilder(\tool_dataprivacy\filtered_userlist::class)
->disableOriginalConstructor()
->onlyMethods([])
->getMock();
$rc = new \ReflectionClass(\tool_dataprivacy\filtered_userlist::class);
$rcm = $rc->getMethod('set_userids');
$rcm->invoke($userlist, $initial);
$userlist->apply_expired_context_filters($expired, $unexpired);
$filtered = $userlist->get_userids();
sort($expected);
sort($filtered);
$this->assertEquals($expected, $filtered);
}
/**
* Data provider for the apply_expired_contexts_filters function.
*
* @return array
*/
public function apply_expired_contexts_filters_provider(): array {
return [
// Entire list should be preserved.
'No overrides' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [],
'unexpired' => [],
[1, 2, 3, 4, 5],
],
// The list should be filtered to only keep the expired users.
'Expired only' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [2, 3, 4],
'unexpired' => [],
'expected' => [2, 3, 4],
],
// The list should be filtered to remove any unexpired users.
'Unexpired only' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [],
'unexpired' => [1, 5],
'expected' => [2, 3, 4],
],
// The list should be filtered to only keep expired users who are not on the unexpired list.
'Combination of expired and unexpired' => [
'users' => [1, 2, 3, 4, 5],
'expired' => [1, 2, 3],
'unexpired' => [1, 5],
'expected' => [2, 3],
],
];
}
}
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data privacy tool data generator.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use tool_dataprivacy\api;
use tool_dataprivacy\category;
use tool_dataprivacy\purpose;
defined('MOODLE_INTERNAL') || die();
/**
* Data privacy tool data generator class.
*
* @package tool_dataprivacy
* @category test
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_dataprivacy_generator extends component_generator_base {
/** @var int Number of created categories. */
protected $categorycount = 0;
/** @var int Number of created purposes. */
protected $purposecount = 0;
/**
* Reset process.
*
* Do not call directly.
*
* @return void
*/
public function reset() {
$this->categorycount = 0;
$this->purposecount = 0;
}
/**
* Create a new category.
*
* @param array|stdClass $record
* @return category
*/
public function create_category($record = null) {
$this->categorycount++;
$i = $this->categorycount;
$record = (object)$record;
if (!isset($record->name)) {
$record->name = "Test purpose $i";
}
if (!isset($record->description)) {
$record->description = "{$record->name} description";
}
$category = api::create_category($record);
return $category;
}
/**
* Create a new purpose.
*
* @param array|stdClass $record
* @return purpose
*/
public function create_purpose($record = null) {
$this->purposecount++;
$i = $this->purposecount;
$record = (object)$record;
if (!isset($record->name)) {
$record->name = "Test purpose $i";
}
if (!isset($record->description)) {
$record->description = "{$record->name} $i description";
}
if (!isset($record->retentionperiod)) {
$record->retentionperiod = 'PT1M';
}
if (!isset($record->lawfulbases)) {
$record->lawfulbases = 'gdpr_art_6_1_a';
}
$purpose = api::create_purpose($record);
return $purpose;
}
}
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy;
use data_privacy_testcase;
defined('MOODLE_INTERNAL') || die();
require_once('data_privacy_testcase.php');
/**
* Tests for the manager observer.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager_observer_test extends data_privacy_testcase {
/**
* Ensure that when users are configured as DPO, they are sent an message upon failure.
*/
public function test_handle_component_failure(): void {
$this->resetAfterTest();
// Create another user who is not a DPO.
$this->getDataGenerator()->create_user();
// Create two DPOs.
$dpo1 = $this->getDataGenerator()->create_user();
$dpo2 = $this->getDataGenerator()->create_user();
$this->assign_site_dpo(array($dpo1, $dpo2));
$dpos = \tool_dataprivacy\api::get_site_dpos();
$observer = new \tool_dataprivacy\manager_observer();
// Handle the failure, catching messages.
$mailsink = $this->redirectMessages();
$mailsink->clear();
$observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']);
// Messages should be sent to both DPOs only.
$this->assertEquals(2, $mailsink->count());
$messages = $mailsink->get_messages();
$messageusers = array_map(function($message) {
return $message->useridto;
}, $messages);
$this->assertEqualsCanonicalizing(array_keys($dpos), $messageusers);
}
/**
* Ensure that when no user is configured as DPO, the message is sent to admin instead.
*/
public function test_handle_component_failure_no_dpo(): void {
$this->resetAfterTest();
// Create another user who is not a DPO or admin.
$this->getDataGenerator()->create_user();
$observer = new \tool_dataprivacy\manager_observer();
$mailsink = $this->redirectMessages();
$mailsink->clear();
$observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']);
// Messages should have been sent only to the admin.
$this->assertEquals(1, $mailsink->count());
$messages = $mailsink->get_messages();
$message = reset($messages);
$admin = \core_user::get_user_by_username('admin');
$this->assertEquals($admin->id, $message->useridto);
}
}
@@ -0,0 +1,96 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy;
/**
* Metadata registry tests.
*
* @package tool_dataprivacy
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class metadata_registry_test extends \advanced_testcase {
/**
* Fetch the meta data and return it in a form that we can easily unit test.
*
* @return array the meta data.
*/
protected function get_meta_data() {
$metadataregistry = new \tool_dataprivacy\metadata_registry();
$data = $metadataregistry->get_registry_metadata();
$newdata = [];
foreach ($data as $value) {
$additional = [];
foreach ($value['plugins'] as $moredata) {
$additional[$moredata['raw_component']] = $moredata;
}
$newdata[$value['plugin_type_raw']] = $additional;
}
return $newdata;
}
/**
* Test that we can fetch metadata about users for the whole system and that it matches the system count.
*/
public function test_get_registry_metadata_count(): void {
$data = $this->get_meta_data();
$plugintypes = \core_component::get_plugin_types();
// Check that we have the correct number of plugin types.
$plugincount = count($plugintypes) + 1; // Plus one for core.
$this->assertEquals($plugincount, count($data));
// Check that each plugin count matches.
foreach ($plugintypes as $plugintype => $notused) {
$plugins = \core_component::get_plugin_list($plugintype);
$this->assertEquals(count($plugins), count($data[$plugintype]));
}
// Let's check core subsystems.
// The Privacy API adds an extra component in the form of 'core'.
$coresubsystems = \core_component::get_core_subsystems();
$this->assertEquals(count($coresubsystems) + 1, count($data['core']));
}
/**
* Check that the expected null provider information is returned.
*/
public function test_get_registry_metadata_null_provider_details(): void {
$data = $this->get_meta_data();
// Check details of core privacy (a null privder) are correct.
$coreprivacy = $data['core']['core_privacy'];
$this->assertEquals(1, $coreprivacy['compliant']);
$this->assertNotEmpty($coreprivacy['nullprovider']);
}
/**
* Check that the expected privacy provider information is returned.
*/
public function test_get_registry_metadata_provider_details(): void {
$data = $this->get_meta_data();
// Check details of core rating (a normal provider) are correct.
$corerating = $data['core']['core_rating'];
$this->assertEquals(1, $corerating['compliant']);
$this->assertNotEmpty($corerating['metadata']);
$this->assertEquals('database_table', $corerating['metadata'][0]['type']);
$this->assertNotEmpty($corerating['metadata'][0]['fields']);
}
}
@@ -0,0 +1,188 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tests for the plugin privacy provider
*
* @package tool_dataprivacy
* @copyright 2020 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use core_privacy\tests\provider_testcase;
use tool_dataprivacy\api;
use tool_dataprivacy\local\helper;
use tool_dataprivacy\privacy\provider;
/**
* Privacy provider tests
*
* @package tool_dataprivacy
* @copyright 2020 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/**
* Test provider get_contexts_for_userid method
*
* @return void
*/
public function test_get_contexts_for_userid(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$context = \context_user::instance($user->id);
// Returned context list should contain a single item.
$contextlist = $this->get_contexts_for_userid($user->id, 'tool_dataprivacy');
$this->assertCount(1, $contextlist);
// We should have the user context of our test user.
$this->assertSame($context, $contextlist->current());
}
/**
* Test provider get_users_in_context method
*
* @return void
*/
public function test_get_users_in_context(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$context = \context_user::instance($user->id);
$userlist = new userlist($context, 'tool_dataprivacy');
provider::get_users_in_context($userlist);
$this->assertEquals([$user->id], $userlist->get_userids());
}
/**
* Test provider get_users_in_context method for a non-user context
*
* @return void
*/
public function test_get_users_in_context_non_user_context(): void {
$context = \context_system::instance();
$userlist = new userlist($context, 'tool_dataprivacy');
provider::get_users_in_context($userlist);
$this->assertEmpty($userlist);
}
/**
* Test provider export_user_data method
*
* @return void
*/
public function test_export_user_data(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$context = \context_user::instance($user->id);
$this->setUser($user);
// Create an export request, approve it.
$requestexport = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT,
'Please export my stuff');
api::update_request_status($requestexport->get('id'), api::DATAREQUEST_STATUS_APPROVED);
// Create a deletion request, reject it.
$requestdelete = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
api::update_request_status($requestdelete->get('id'), api::DATAREQUEST_STATUS_REJECTED, 0, 'Nope');
$this->export_context_data_for_user($user->id, $context, 'tool_dataprivacy');
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
$this->assertTrue($writer->has_any_data());
/** @var stdClass[] $data */
$data = (array) $writer->get_data([
get_string('privacyandpolicies', 'admin'),
get_string('datarequests', 'tool_dataprivacy'),
]);
$this->assertCount(2, $data);
$strs = get_strings(['requesttypeexportshort', 'requesttypedeleteshort',
'statusapproved', 'statusrejected', 'creationmanual'], 'tool_dataprivacy');
// First item is the approved export request.
$this->assertEquals($strs->requesttypeexportshort, $data[0]->type);
$this->assertEquals($strs->statusapproved, $data[0]->status);
$this->assertEquals($strs->creationmanual, $data[0]->creationmethod);
$this->assertEquals($requestexport->get('comments'), $data[0]->comments);
$this->assertEmpty($data[0]->dpocomment);
$this->assertNotEmpty($data[0]->timecreated);
// Next is the rejected deletion request.
$this->assertEquals($strs->requesttypedeleteshort, $data[1]->type);
$this->assertEquals($strs->statusrejected, $data[1]->status);
$this->assertEquals($strs->creationmanual, $data[1]->creationmethod);
$this->assertEmpty($data[1]->comments);
$this->assertStringContainsString('Nope', $data[1]->dpocomment);
$this->assertNotEmpty($data[1]->timecreated);
}
/**
* Test class export_user_preferences method
*
* @return void
*/
public function test_export_user_preferences(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Set filters preference.
$filters = [
helper::FILTER_TYPE . ':' . api::DATAREQUEST_TYPE_EXPORT,
helper::FILTER_STATUS . ':' . api::DATAREQUEST_STATUS_PENDING,
];
set_user_preference(helper::PREF_REQUEST_FILTERS, json_encode($filters), $user);
// Set paging preference.
set_user_preference(helper::PREF_REQUEST_PERPAGE, 6, $user);
provider::export_user_preferences($user->id);
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
/** @var stdClass[] $preferences */
$preferences = (array) $writer->get_user_preferences('tool_dataprivacy');
$this->assertCount(2, $preferences);
$this->assertEquals((object) [
'value' => '1:1, 2:0',
'description' => 'Type: Export, Status: Pending',
], $preferences[helper::PREF_REQUEST_FILTERS]);
$this->assertEquals(6, $preferences[helper::PREF_REQUEST_PERPAGE]->value);
}
}
@@ -0,0 +1,221 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy\task;
use core\task\task_trait;
use tool_dataprivacy\api;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../data_privacy_testcase.php');
/**
* Tests for scheduled tasks.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class task_test extends \data_privacy_testcase {
use task_trait;
/**
* Test tearDown.
*/
public function tearDown(): void {
\core_privacy\local\request\writer::reset();
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is created when there are not any existing data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_no_previous_requests(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the deleted user should have a delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is not being created when automatic creation of delete data requests is disabled.
*/
public function test_delete_existing_deleted_users_task_automatic_creation_disabled(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Disable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 0, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the deleted user should still not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is created when there are existing non-delete data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_existing_export_data_requests(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
// Create export data request for the user.
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should have a export data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_EXPORT]));
// The user should not have a delete data request.
$this->assertCount(0, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the deleted user should have a delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is not created when there are existing ongoing delete data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_existing_ongoing_delete_data_requests(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create delete data request for the user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// The user should have an ongoing delete data request.
$this->assertCount(1, api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should still have the existing ongoing delete data request.
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the user should have only one delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request for pre-existing deleted users
* is not created when there are existing finished delete data requests
* for that particular user.
*/
public function test_delete_existing_deleted_users_task_existing_finished_delete_data_requests(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create a user.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create delete data request for the user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
// The user should have a delete data request.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
// The user should not have an ongoing data requests.
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
// Mark the user as deleted.
$user->deleted = 1;
$DB->update_record('user', $user);
// The user should still have the existing cancelled delete data request.
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
$this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users');
// After running the scheduled task, the user should still have one delete data requests.
$this->assertCount(1, api::get_data_requests($user->id, [],
[api::DATAREQUEST_TYPE_DELETE]));
// The user should only have the existing cancelled delete data request.
$this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id,
[api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE]));
}
}
@@ -0,0 +1,198 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_dataprivacy;
use tool_dataprivacy\event\user_deleted_observer;
/**
* Event observer test.
*
* @package tool_dataprivacy
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_deleted_observer_test extends \advanced_testcase {
/**
* Ensure that a delete data request is created upon user deletion.
*/
public function test_create_delete_data_request(): void {
$this->resetAfterTest();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that delete data request has been created.
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Ensure that a delete data request is not created upon user deletion if automatic creation of
* delete data requests is disabled.
*/
public function test_create_delete_data_request_automatic_creation_disabled(): void {
$this->resetAfterTest();
// Disable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 0, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that delete data request has been created.
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Ensure that a delete data request is being created upon user deletion
* if an ongoing export data request (or any other except delete data request) for that user already exists.
*/
public function test_create_delete_data_request_export_data_request_preexists(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
// Validate that delete data request has been created.
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_EXPORT));
$this->assertEquals(0, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that delete data request has been created.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request is not being created upon user deletion
* if an ongoing delete data request for that user already exists.
*/
public function test_create_delete_data_request_ongoing_delete_data_request_preexists(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
// Validate that delete data request has been created.
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that additional delete data request has not been created.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
}
/**
* Ensure that a delete data request is being created upon user deletion
* if a finished delete data request (excluding complete) for that user already exists.
*/
public function test_create_delete_data_request_canceled_delete_data_request_preexists(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
// Validate that delete data request has been created and the status has been updated to 'Canceled'.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that additional delete data request has been created.
$this->assertEquals(2, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Ensure that a delete data request is being created upon user deletion
* if a completed delete data request for that user already exists.
*/
public function test_create_delete_data_request_completed_delete_data_request_preexists(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Enable automatic creation of delete data requests.
set_config('automaticdeletionrequests', 1, 'tool_dataprivacy');
// Create another user who is not a DPO.
$user = $this->getDataGenerator()->create_user();
// Create a delete data request for $user.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$requestid = $datarequest->get('id');
api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE);
// Validate that delete data request has been created and the status has been updated to 'Completed'.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
$event = $this->trigger_delete_user_event($user);
user_deleted_observer::create_delete_data_request($event);
// Validate that additional delete data request has not been created.
$this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE]));
$this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE));
}
/**
* Helper to trigger and capture the delete user event.
*
* @param object $user The user object.
* @return \core\event\user_deleted $event The returned event.
*/
private function trigger_delete_user_event($user) {
$sink = $this->redirectEvents();
delete_user($user);
$events = $sink->get_events();
$sink->close();
$event = reset($events);
// Validate event data.
$this->assertInstanceOf('\core\event\user_deleted', $event);
return $event;
}
}