From 8ba35e317c8a5e5303c613157a1d5953f098e751 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 31 Aug 2018 10:20:57 +0800 Subject: [PATCH] MDL-63495 privacy: Add support for removal of multiple users in a context This issue is a part of the MDL-62560 Epic. --- lang/en/privacy.php | 7 +- .../local/request/approved_userlist.php | 61 +++++ .../local/request/core_userlist_provider.php | 51 ++++ privacy/classes/local/request/helper.php | 12 + privacy/classes/local/request/userlist.php | 103 ++++++++ .../classes/local/request/userlist_base.php | 220 ++++++++++++++++ .../local/request/userlist_collection.php | 177 +++++++++++++ privacy/classes/manager.php | 83 ++++++ privacy/tests/approved_userlist_test.php | 100 ++++++++ privacy/tests/userlist_base_test.php | 241 ++++++++++++++++++ privacy/tests/userlist_collection.php | 161 ++++++++++++ privacy/tests/userlist_test.php | 93 +++++++ 12 files changed, 1307 insertions(+), 2 deletions(-) create mode 100644 privacy/classes/local/request/approved_userlist.php create mode 100644 privacy/classes/local/request/core_userlist_provider.php create mode 100644 privacy/classes/local/request/userlist.php create mode 100644 privacy/classes/local/request/userlist_base.php create mode 100644 privacy/classes/local/request/userlist_collection.php create mode 100644 privacy/tests/approved_userlist_test.php create mode 100644 privacy/tests/userlist_base_test.php create mode 100644 privacy/tests/userlist_collection.php create mode 100644 privacy/tests/userlist_test.php diff --git a/lang/en/privacy.php b/lang/en/privacy.php index 0045cb6462db5..354e9925cbf6f 100644 --- a/lang/en/privacy.php +++ b/lang/en/privacy.php @@ -33,9 +33,12 @@ $string['trace:exportingrelated'] = 'Performing related export for {$a->total} components ({$a->datetime})'; $string['trace:finalisingexport'] = 'Finalising export'; $string['trace:processingcomponent'] = 'Processing {$a->component} ({$a->progress}/{$a->total}) ({$a->datetime})'; -$string['trace:fetchcomponents'] = 'Fetching {$a->total} components ({$a->datetime})'; -$string['trace:deletingapproved'] = 'Performing removal of approved {$a->total} contexts ({$a->datetime})'; +$string['trace:preprocessingcomponent'] = 'Pre-processing {$a->component} ({$a->progress}/{$a->total}) ({$a->datetime})'; +$string['trace:fetchcomponents'] = 'Fetching data from {$a->total} components ({$a->datetime})'; +$string['trace:deletingapproved'] = 'Performing removal of {$a->total} approved contexts ({$a->datetime})'; +$string['trace:deletingapprovedusers'] = 'Performing removal of users in {$a->total} approved component for context {$a->contextid} ({$a->datetime})'; $string['trace:deletingcontext'] = 'Performing removal of context from {$a->total} components ({$a->datetime})'; $string['navigation'] = 'Navigation'; +$string['trace:deletinguser'] = 'Performing removal of user from {$a->total} components ({$a->datetime})'; $string['privacy:subsystem:empty'] = 'This subsystem does not store any data.'; $string['viewdata'] = 'Click on a link in the navigation to view data.'; diff --git a/privacy/classes/local/request/approved_userlist.php b/privacy/classes/local/request/approved_userlist.php new file mode 100644 index 0000000000000..47126ccb592ec --- /dev/null +++ b/privacy/classes/local/request/approved_userlist.php @@ -0,0 +1,61 @@ +. + +/** + * An implementation of a userlist which has been filtered and approved. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * An implementation of a userlist which has been filtered and approved. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class approved_userlist extends userlist_base { + + /** + * Create a new approved userlist. + * + * @param \context $context The context. + * @param string $component the frankenstyle component name. + * @param \int[] $userids The list of userids present in this list. + */ + public function __construct(\context $context, string $component, array $userids) { + parent::__construct($context, $component); + + $this->set_userids($userids); + } + + /** + * Create an approved userlist from a userlist. + * + * @param userlist $userlist The source list + * @return approved_userlist The newly created approved userlist. + */ + public static function create_from_userlist(userlist $userlist) : approved_userlist { + $newlist = new static($userlist->get_context(), $userlist->get_component(), $userlist->get_userids()); + + return $newlist; + } +} diff --git a/privacy/classes/local/request/core_userlist_provider.php b/privacy/classes/local/request/core_userlist_provider.php new file mode 100644 index 0000000000000..e1b8e8fc24272 --- /dev/null +++ b/privacy/classes/local/request/core_userlist_provider.php @@ -0,0 +1,51 @@ +. + +/** + * This file contains an interface to describe classes which provide user data in some form to core. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The interface is used to describe a provider which is capable of identifying the users who have data within it. + * + * It describes data how these requests are serviced in a specific format. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + */ +interface core_userlist_provider { + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist); + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist); +} diff --git a/privacy/classes/local/request/helper.php b/privacy/classes/local/request/helper.php index 4485670976a59..9836e68b059df 100644 --- a/privacy/classes/local/request/helper.php +++ b/privacy/classes/local/request/helper.php @@ -60,6 +60,18 @@ public static function add_shared_contexts_to_contextlist_for(int $userid, conte return $contextlist; } + /** + * Add core-controlled contexts which are related to a component but that component may know about. + * + * For example, most activities are not aware of activity completion, but the course implements it for them. + * These should be included. + * + * @param \core_privacy\local\request\userlist $userlist + * @return contextlist The final contextlist + */ + public static function add_shared_users_to_userlist(\core_privacy\local\request\userlist $userlist) { + } + /** * Handle export of standard data for a plugin which implements the null provider and does not normally store data * of its own. diff --git a/privacy/classes/local/request/userlist.php b/privacy/classes/local/request/userlist.php new file mode 100644 index 0000000000000..0664f14eda6ac --- /dev/null +++ b/privacy/classes/local/request/userlist.php @@ -0,0 +1,103 @@ +. + +/** + * List of users from the Privacy API Search functions. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * List of users from the Privacy API Search functions. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class userlist extends userlist_base { + + /** + * Add a set of users from SQL. + * + * The SQL should only return a list of user IDs. + * + * @param string $fieldname The name of the field which holds the user id + * @param string $sql The SQL which will fetch the list of * user IDs + * @param array $params The set of SQL parameters + * @return $this + */ + public function add_from_sql(string $fieldname, string $sql, array $params) : userlist { + global $DB; + + // Able to guess a field name. + $wrapper = " + SELECT DISTINCT u.id + FROM {user} u + JOIN ({$sql}) target ON u.id = target.{$fieldname}"; + + $users = $DB->get_records_sql($wrapper, $params); + $this->add_userids(array_keys($users)); + + return $this; + } + + /** + * Adds the user user for a given user. + * + * @param int $userid + * @return $this + */ + public function add_user(int $userid) : userlist { + $this->add_users([$userid]); + + return $this; + } + + /** + * Adds the user users for given users. + * + * @param int[] $userids + * @return $this + */ + public function add_users(array $userids) : userlist { + global $DB; + + list($useridsql, $useridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $sql = "SELECT DISTINCT u.id + FROM {user} u + WHERE u.id {$useridsql}"; + $this->add_from_sql('id', $sql, $useridparams); + + return $this; + } + + /** + * Sets the component for this userlist. + * + * @param string $component the frankenstyle component name. + * @return $this + */ + public function set_component($component) : userlist_base { + parent::set_component($component); + + return $this; + } +} diff --git a/privacy/classes/local/request/userlist_base.php b/privacy/classes/local/request/userlist_base.php new file mode 100644 index 0000000000000..3a49a2222760d --- /dev/null +++ b/privacy/classes/local/request/userlist_base.php @@ -0,0 +1,220 @@ +. + +/** + * Base implementation of a userlist. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base implementation of a userlist used to store a set of users. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class userlist_base implements + // Implement an Iterator to fetch the Context objects. + \Iterator, + + // Implement the Countable interface to allow the number of returned results to be queried easily. + \Countable { + + /** + * @var array List of user IDs. + * + * Note: this must not be updated using set_userids only as this + * ensures uniqueness. + */ + private $userids = []; + + /** + * @var string component the frankenstyle component name. + */ + protected $component = ''; + + /** + * @var int Current position of the iterator. + */ + protected $iteratorposition = 0; + + /** @var \context The context that this userlist belongs to */ + protected $context; + + /** + * Constructor to create a new userlist. + * + * @param \context $context + * @param string $component + */ + public function __construct(\context $context, string $component) { + $this->context = $context; + $this->set_component($component); + } + + /** + * Set the userids. + * + * @param array $userids The list of users. + * @return $this + */ + protected function set_userids(array $userids) : userlist_base { + $this->userids = array_values(array_unique($userids)); + + return $this; + } + + /** + * Add a set of additional userids. + * + * @param array $userids The list of users. + * @return $this + */ + protected function add_userids(array $userids) : userlist_base { + $this->set_userids(array_merge($this->get_userids(), $userids)); + + return $this; + } + + /** + * Get the list of user IDs that relate to this request. + * + * @return int[] + */ + public function get_userids() : array { + return $this->userids; + } + + /** + * Get the complete list of user objects that relate to this request. + * + * @return \stdClass[] + */ + public function get_users() : array { + $users = []; + foreach ($this->userids as $userid) { + if ($user = \core_user::get_user($userid)) { + $users[] = $user; + } + } + + return $users; + } + + /** + * Sets the component for this userlist. + * + * @param string $component the frankenstyle component name. + * @return $this + */ + protected function set_component($component) : userlist_base { + $this->component = $component; + + return $this; + } + + /** + * Get the name of the component to which this userlist belongs. + * + * @return string the component name associated with this userlist. + */ + public function get_component() : string { + return $this->component; + } + + /** + * Return the current user. + * + * @return \user + */ + public function current() { + $user = \core_user::get_user($this->userids[$this->iteratorposition]); + + if (false === $user) { + // This user was not found. + unset($this->userids[$this->iteratorposition]); + + // Check to see if there are any more users left. + if ($this->count()) { + // Move the pointer to the next record and try again. + $this->next(); + $user = $this->current(); + } else { + // There are no more context ids left. + return; + } + } + + return $user; + } + + /** + * Return the key of the current element. + * + * @return mixed + */ + public function key() { + return $this->iteratorposition; + } + + /** + * Move to the next user in the list. + */ + public function next() { + ++$this->iteratorposition; + } + + /** + * Check if the current position is valid. + * + * @return bool + */ + public function valid() { + return isset($this->userids[$this->iteratorposition]) && $this->current(); + } + + /** + * Rewind to the first found user. + * + * The list of users is uniqued during the rewind. + * The rewind is called at the start of most iterations. + */ + public function rewind() { + $this->iteratorposition = 0; + } + + /** + * Return the number of users. + */ + public function count() { + return count($this->userids); + } + + /** + * Get the context for this userlist + * + * @return \context + */ + public function get_context() : \context { + return $this->context; + } +} diff --git a/privacy/classes/local/request/userlist_collection.php b/privacy/classes/local/request/userlist_collection.php new file mode 100644 index 0000000000000..72eff15ea57cf --- /dev/null +++ b/privacy/classes/local/request/userlist_collection.php @@ -0,0 +1,177 @@ +. + +/** + * This file defines the userlist_collection class object. + * + * The userlist_collection is used to organize a collection of userlists. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A collection of userlist items. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class userlist_collection implements \Iterator, \Countable { + + /** + * @var \context $context The context that the userlist collection belongs to. + */ + protected $context = null; + + /** + * @var array $userlists the internal array of userlist objects. + */ + protected $userlists = []; + + /** + * @var int Current position of the iterator. + */ + protected $iteratorposition = 0; + + /** + * Constructor to create a new userlist_collection. + * + * @param \context $context The context to which this collection belongs. + */ + public function __construct(\context $context) { + $this->context = $context; + } + + /** + * Return the context that this collection relates to. + * + * @return int + */ + public function get_context() : \context { + return $this->context; + } + + /** + * Add a userlist to this collection. + * + * @param userlist_base $userlist the userlist to export. + * @return $this + */ + public function add_userlist(userlist_base $userlist) : userlist_collection { + $component = $userlist->get_component(); + if (isset($this->userlists[$component])) { + throw new \moodle_exception("A userlist has already been added for the '{$component}' component"); + } + + $this->userlists[$component] = $userlist; + + return $this; + } + + /** + * Get the userlists in this collection. + * + * @return array the associative array of userlists in this collection, indexed by component name. + * E.g. mod_assign => userlist, core_comment => userlist. + */ + public function get_userlists() : array { + return $this->userlists; + } + + /** + * Get the userlist for the specified component. + * + * @param string $component the frankenstyle name of the component to fetch for. + * @return userlist_base|null + */ + public function get_userlist_for_component(string $component) { + if (isset($this->userlists[$component])) { + return $this->userlists[$component]; + } + + return null; + } + + /** + * Return the current contexlist. + * + * @return \user + */ + public function current() { + $key = $this->get_key_from_position(); + return $this->userlists[$key]; + } + + /** + * Return the key of the current element. + * + * @return mixed + */ + public function key() { + return $this->get_key_from_position(); + } + + /** + * Move to the next user in the list. + */ + public function next() { + ++$this->iteratorposition; + } + + /** + * Check if the current position is valid. + * + * @return bool + */ + public function valid() { + return ($this->iteratorposition < count($this->userlists)); + } + + /** + * Rewind to the first found user. + * + * The list of users is uniqued during the rewind. + * The rewind is called at the start of most iterations. + */ + public function rewind() { + $this->iteratorposition = 0; + } + + /** + * Get the key for the current iterator position. + * + * @return string + */ + protected function get_key_from_position() { + $keylist = array_keys($this->userlists); + if (isset($keylist[$this->iteratorposition])) { + return $keylist[$this->iteratorposition]; + } + + return null; + } + + /** + * Return the number of users. + */ + public function count() { + return count($this->userlists); + } +} diff --git a/privacy/classes/manager.php b/privacy/classes/manager.php index 47e73d291f995..6ec6abdedc597 100644 --- a/privacy/classes/manager.php +++ b/privacy/classes/manager.php @@ -27,6 +27,7 @@ use core_privacy\local\request\context_aware_provider; use core_privacy\local\request\contextlist_collection; use core_privacy\local\request\core_user_data_provider; +use core_privacy\local\request\core_userlist_provider; use core_privacy\local\request\data_provider; use core_privacy\local\request\user_preference_provider; use \core_privacy\local\metadata\provider as metadata_provider; @@ -255,6 +256,46 @@ public function get_contexts_for_userid(int $userid) : contextlist_collection { return $clcollection; } + /** + * Gets a collection of users for all components in the specified context. + * + * @param \context $context The context to search + * @return userlist_collection the collection of userlist items for the respective components. + */ + public function get_users_in_context(\context $context) : \core_privacy\local\request\userlist_collection { + $progress = static::get_log_tracer(); + + $components = $this->get_component_list(); + $a = (object) [ + 'total' => count($components), + 'progress' => 0, + 'component' => '', + 'datetime' => userdate(time()), + ]; + $collection = new \core_privacy\local\request\userlist_collection($context); + + $progress->output(get_string('trace:fetchcomponents', 'core_privacy', $a), 1); + foreach ($components as $component) { + $a->component = $component; + $a->progress++; + $a->datetime = userdate(time()); + $progress->output(get_string('trace:preprocessingcomponent', 'core_privacy', $a), 2); + $userlist = new local\request\userlist($context, $component); + + $this->handled_component_class_callback($component, core_userlist_provider::class, 'get_users_in_context', [$userlist]); + + // Add contexts that the component may not know about. + \core_privacy\local\request\helper::add_shared_users_to_userlist($userlist); + + if (count($userlist)) { + $collection->add_userlist($userlist); + } + } + $progress->output(get_string('trace:done', 'core_privacy'), 1); + + return $collection; + } + /** * Export all user data for the specified approved_contextlist items. * @@ -380,6 +421,48 @@ public function delete_data_for_user(contextlist_collection $contextlistcollecti $progress->output(get_string('trace:done', 'core_privacy'), 1); } + /** + * Delete all user data for all specified users in a context. + * + * @param \core_privacy\local\request\userlist_collection $collection + */ + public function delete_data_for_users_in_context(\core_privacy\local\request\userlist_collection $collection) { + $progress = static::get_log_tracer(); + + $a = (object) [ + 'contextid' => $collection->get_context()->id, + 'total' => count($collection), + 'progress' => 0, + 'component' => '', + 'datetime' => userdate(time()), + ]; + + // Delete the data. + $progress->output(get_string('trace:deletingapprovedusers', 'core_privacy', $a), 1); + foreach ($collection as $userlist) { + if (!$userlist instanceof \core_privacy\local\request\approved_userlist) { + throw new \moodle_exception('The supplied userlist must be an approved_userlist'); + } + + $component = $userlist->get_component(); + $a->component = $component; + $a->progress++; + $a->datetime = userdate(time()); + + if (empty($userlist)) { + // This really shouldn't happen! + continue; + } + + $progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2); + + $this->handled_component_class_callback($component, core_userlist_provider::class, + 'delete_data_for_users', [$userlist]); + } + + $progress->output(get_string('trace:done', 'core_privacy'), 1); + } + /** * Delete all use data which matches the specified deletion criteria. * diff --git a/privacy/tests/approved_userlist_test.php b/privacy/tests/approved_userlist_test.php new file mode 100644 index 0000000000000..edb79fd1864ef --- /dev/null +++ b/privacy/tests/approved_userlist_test.php @@ -0,0 +1,100 @@ +. + +/** + * Unit Tests for the approved userlist Class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\approved_userlist; +use \core_privacy\local\request\userlist; + +/** + * Tests for the \core_privacy API's approved userlist functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class approved_userlist_test extends advanced_testcase { + /** + * The approved userlist should not be modifiable once set. + */ + public function test_default_values_set() { + $this->resetAfterTest(); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + $u4 = $this->getDataGenerator()->create_user(); + + $context = \context_system::instance(); + $component = 'core_privacy'; + + $uut = new approved_userlist($context, $component, [$u1->id, $u2->id]); + + $this->assertEquals($context, $uut->get_context()); + $this->assertEquals($component, $uut->get_component()); + + $expected = [ + $u1->id, + $u2->id, + ]; + sort($expected); + + $result = $uut->get_userids(); + sort($result); + + $this->assertEquals($expected, $result); + } + + public function test_create_from_userlist() { + $this->resetAfterTest(); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + $u4 = $this->getDataGenerator()->create_user(); + + $context = \context_system::instance(); + $component = 'core_privacy'; + + $sourcelist = new userlist($context, $component); + $sourcelist->add_users([$u1->id, $u3->id]); + + $expected = [ + $u1->id, + $u3->id, + ]; + sort($expected); + + $approvedlist = approved_userlist::create_from_userlist($sourcelist); + + $this->assertEquals($component, $approvedlist->get_component()); + $this->assertEquals($context, $approvedlist->get_context()); + + $result = $approvedlist->get_userids(); + sort($result); + $this->assertEquals($expected, $result); + } +} diff --git a/privacy/tests/userlist_base_test.php b/privacy/tests/userlist_base_test.php new file mode 100644 index 0000000000000..fc18b83132042 --- /dev/null +++ b/privacy/tests/userlist_base_test.php @@ -0,0 +1,241 @@ +. + +/** + * Unit Tests for the abstract userlist Class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\userlist_base; + +/** + * Tests for the \core_privacy API's userlist base functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class userlist_base_test extends advanced_testcase { + /** + * Ensure that get_userids returns the list of unique userids. + * + * @dataProvider get_userids_provider + * @param array $input List of user IDs + * @param array $expected list of userids + * @param int $count Expected count + */ + public function test_get_userids($input, $expected, $count) { + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $uut->set_userids($input); + + $result = $uut->get_userids(); + $this->assertCount($count, $result); + + // Note: Array order is not guaranteed and should not matter. + foreach ($expected as $userid) { + $this->assertNotFalse(array_search($userid, $result)); + } + } + + /** + * Provider for the list of userids. + * + * @return array + */ + public function get_userids_provider() { + return [ + 'basic' => [ + [1, 2, 3, 4, 5], + [1, 2, 3, 4, 5], + 5, + ], + 'duplicates' => [ + [1, 1, 2, 2, 3, 4, 5], + [1, 2, 3, 4, 5], + 5, + ], + 'Mixed order with duplicates' => [ + [5, 4, 2, 5, 4, 1, 3, 4, 1, 5, 5, 5, 2, 4, 1, 2], + [1, 2, 3, 4, 5], + 5, + ], + ]; + } + + /** + * Ensure that get_users returns the correct list of users. + */ + public function test_get_users() { + $this->resetAfterTest(); + + $users = []; + $user = $this->getDataGenerator()->create_user(); + $users[$user->id] = $user; + + $user = $this->getDataGenerator()->create_user(); + $users[$user->id] = $user; + + $user = $this->getDataGenerator()->create_user(); + $users[$user->id] = $user; + + $otheruser = $this->getDataGenerator()->create_user(); + + $ids = array_keys($users); + + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $uut->set_userids($ids); + + $result = $uut->get_users(); + + sort($users); + sort($result); + + $this->assertCount(3, $result); + $this->assertEquals($users, $result); + } + + /** + * Ensure that the userlist_base is countable. + * + * @dataProvider get_userids_provider + * @param array $input List of user IDs + * @param array $expected list of userids + * @param int $count Expected count + */ + public function test_countable($input, $expected, $count) { + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $uut->set_userids($input); + + $this->assertCount($count, $uut); + } + + /** + * Ensure that the userlist_base iterates over the set of users. + */ + public function test_user_iteration() { + $this->resetAfterTest(); + + $users = []; + $user = $this->getDataGenerator()->create_user(); + $users[$user->id] = $user; + + $user = $this->getDataGenerator()->create_user(); + $users[$user->id] = $user; + + $user = $this->getDataGenerator()->create_user(); + $users[$user->id] = $user; + + $otheruser = $this->getDataGenerator()->create_user(); + + $ids = array_keys($users); + + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $uut->set_userids($ids); + + foreach ($uut as $key => $user) { + $this->assertTrue(isset($users[$user->id])); + $this->assertEquals($users[$user->id], $user); + } + } + + /** + * Test that a deleted user is still returned. + * If a user has data then it still must be deleted, even if they are deleted. + */ + public function test_current_user_one_user() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $uut->set_userids([$user->id]); + + $this->assertCount(1, $uut); + $this->assertEquals($user, $uut->current()); + + delete_user($user); + $u = $uut->current(); + $this->assertEquals($user->id, $u->id); + } + + /** + * Test that an invalid user returns no entry. + */ + public function test_current_user_invalid() { + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $uut->set_userids([-100]); + + $this->assertCount(1, $uut); + $this->assertNull($uut->current()); + } + + /** + * Test that where an invalid user is listed, the next user in the list is returned instead. + */ + public function test_current_user_two_users() { + $this->resetAfterTest(); + + $u1 = $this->getDataGenerator()->create_user(); + + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $uut->set_userids([-100, $u1->id]); + + $this->assertCount(2, $uut); + $this->assertEquals($u1, $uut->current()); + } + + /** + * Ensure that the component specified in the constructor is used and available. + */ + public function test_set_component_in_constructor() { + $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); + $this->assertEquals('core_tests', $uut->get_component()); + } + + /** + * Ensure that the context specified in the constructor is available. + */ + public function test_set_context_in_constructor() { + $context = \context_user::instance(\core_user::get_user_by_username('admin')->id); + + $uut = new test_userlist_base($context, 'core_tests'); + $this->assertEquals($context, $uut->get_context()); + } +} + +/** + * A test class extending the userlist_base allowing setting of the userids. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_userlist_base extends userlist_base { + /** + * Set the contextids for the test class. + * + * @param int[] $contexids The list of contextids to use. + */ + public function set_userids(array $userids) : userlist_base { + return parent::set_userids($userids); + } +} diff --git a/privacy/tests/userlist_collection.php b/privacy/tests/userlist_collection.php new file mode 100644 index 0000000000000..000ac3d73966c --- /dev/null +++ b/privacy/tests/userlist_collection.php @@ -0,0 +1,161 @@ +. + +/** + * Unit Tests for a the collection of userlists class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\userlist_collection; +use \core_privacy\local\request\userlist; +use \core_privacy\local\request\approved_userlist; + +/** + * Tests for the \core_privacy API's userlist collection functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class userlist_collection_test extends advanced_testcase { + + /** + * A userlist_collection should support the userlist type. + */ + public function test_supports_userlist() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $userlist = new userlist($cut, 'core_privacy'); + $uut->add_userlist($userlist); + + $this->assertCount(1, $uut->get_userlists()); + } + + /** + * A userlist_collection should support the approved_userlist type. + */ + public function test_supports_approved_userlist() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $userlist = new approved_userlist($cut, 'core_privacy', [1, 2, 3]); + $uut->add_userlist($userlist); + + $this->assertCount(1, $uut->get_userlists()); + } + + /** + * Ensure that get_userlist_for_component returns the correct userlist. + */ + public function test_get_userlist_for_component() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $privacy = new userlist($cut, 'core_privacy'); + $uut->add_userlist($privacy); + + $test = new userlist($cut, 'core_tests'); + $uut->add_userlist($test); + + // Note: This uses assertSame rather than assertEquals. + // The former checks the actual object, whilst assertEquals only checks that they look the same. + $this->assertSame($privacy, $uut->get_userlist_for_component('core_privacy')); + $this->assertSame($test, $uut->get_userlist_for_component('core_tests')); + } + + /** + * Ensure that get_userlist_for_component does not die horribly when querying a non-existent component. + */ + public function test_get_userlist_for_component_not_found() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $this->assertNull($uut->get_userlist_for_component('core_tests')); + } + + /** + * Ensure that a duplicate userlist in the collection throws an Exception. + */ + public function test_duplicate_addition_throws() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $userlist = new userlist($cut, 'core_privacy'); + $uut->add_userlist($userlist); + + $this->expectException('moodle_exception'); + $uut->add_userlist($userlist); + } + + /** + * Ensure that the userlist_collection is countable. + */ + public function test_countable() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $uut->add_userlist(new userlist($cut, 'core_privacy')); + $uut->add_userlist(new userlist($cut, 'core_tests')); + + $this->assertCount(2, $uut); + } + + /** + * Ensure that the userlist_collection iterates over the set of userlists. + */ + public function test_iteration() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $testdata = []; + + $privacy = new userlist($cut, 'core_privacy'); + $uut->add_userlist($privacy); + $testdata['core_privacy'] = $privacy; + + $test = new userlist($cut, 'core_tests'); + $uut->add_userlist($test); + $testdata['core_tests'] = $test; + + $another = new userlist($cut, 'privacy_another'); + $uut->add_userlist($another); + $testdata['privacy_another'] = $another; + + foreach ($uut as $component => $list) { + $this->assertEquals($testdata[$component], $list); + } + + $this->assertCount(3, $uut); + } + + /** + * Test that the context is correctly returned. + */ + public function test_get_context() { + $cut = \context_system::instance(); + $uut = new userlist_collection($cut); + + $this->assertSame($cut, $uut->get_context()); + } +} diff --git a/privacy/tests/userlist_test.php b/privacy/tests/userlist_test.php new file mode 100644 index 0000000000000..9afc7ea1d7a33 --- /dev/null +++ b/privacy/tests/userlist_test.php @@ -0,0 +1,93 @@ +. + +/** + * Unit Tests for the approved userlist Class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\userlist; + +/** + * Tests for the \core_privacy API's approved userlist functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class userlist_test extends advanced_testcase { + + /** + * Ensure that valid SQL results in the relevant users being added. + */ + public function test_add_from_sql() { + global $DB; + + $sql = "SELECT c.id FROM {user} c"; + $params = []; + $allusers = $DB->get_records_sql($sql, $params); + + $uut = new userlist(\context_system::instance(), 'core_privacy'); + $uut->add_from_sql('id', $sql, $params); + + $this->assertCount(count($allusers), $uut); + } + + /** + * Ensure that adding a single user adds that user. + */ + public function test_add_user() { + $this->resetAfterTest(); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + + $uut = new userlist(\context_system::instance(), 'core_privacy'); + $uut->add_user($u1->id); + + $this->assertCount(1, $uut); + $this->assertEquals($uut->current(), $u1); + } + + + /** + * Ensure that adding multiple users by ID adds those users. + */ + public function test_add_users() { + $this->resetAfterTest(); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + $expected = [$u1->id, $u3->id]; + + $uut = new userlist(\context_system::instance(), 'core_privacy'); + $uut->add_users([$u1->id, $u3->id]); + + $this->assertCount(2, $uut); + + foreach ($uut as $user) { + $this->assertNotFalse(array_search($user->id, $expected)); + } + } +}