Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core_privacy;
  18  
  19  use core_privacy\local\request\writer;
  20  use core_privacy\local\request\approved_contextlist;
  21  
  22  defined('MOODLE_INTERNAL') || die();
  23  global $CFG;
  24  require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_null_provider.php');
  25  require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_provider.php');
  26  require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_plugin_subplugin_provider.php');
  27  require_once($CFG->dirroot . '/privacy/tests/fixtures/mock_mod_with_user_data_provider.php');
  28  require_once($CFG->dirroot . '/privacy/tests/fixtures/provider_a.php');
  29  require_once($CFG->dirroot . '/privacy/tests/fixtures/provider_throwing_exception.php');
  30  
  31  /**
  32   * Privacy manager unit tests.
  33   *
  34   * @copyright   2018 Jake Dallimore <jrhdallimore@gmail.com>
  35   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   * @coversDefaultClass \core_privacy\manager
  37   */
  38  class manager_test extends \advanced_testcase {
  39      /**
  40       * Test tearDown.
  41       */
  42      public function tearDown(): void {
  43          \core_privacy\local\request\writer::reset();
  44      }
  45  
  46      /**
  47       * Helper to spoof the results of the internal function get_components_list, allowing mock components to be tested.
  48       *
  49       * @param array $componentnames and array of component names to include as valid core components.
  50       * @return PHPUnit_Framework_MockObject_MockObject
  51       */
  52      protected function get_mock_manager_with_core_components($componentnames) {
  53          $mock = $this->getMockBuilder(\core_privacy\manager::class)
  54              ->onlyMethods(['get_component_list'])
  55              ->getMock();
  56          $mock->expects($this->any())
  57              ->method('get_component_list')
  58              ->will($this->returnValue($componentnames));
  59          return $mock;
  60      }
  61  
  62      /**
  63       * Test collection of metadata for components implementing a metadata provider.
  64       *
  65       * @covers ::get_metadata_for_components
  66       */
  67      public function test_get_metadata_for_components() {
  68          // Get a mock manager, in which the core components list is mocked to include all mock plugins.
  69          // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core).
  70          $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']);
  71  
  72          // Core providers and shared providers both implement the metadata provider.
  73          $collectionarray = $mockman->get_metadata_for_components();
  74          $this->assertArrayHasKey('mod_testcomponent', $collectionarray);
  75          $collection = $collectionarray['mod_testcomponent'];
  76          $this->assertInstanceOf(\core_privacy\local\metadata\collection::class, $collection);
  77          $this->assertArrayHasKey('mod_testcomponent3', $collectionarray);
  78          $collection = $collectionarray['mod_testcomponent3'];
  79          $this->assertInstanceOf(\core_privacy\local\metadata\collection::class, $collection);
  80  
  81          // Component which implements just the local\metadata\null_provider. Metadata is not provided.
  82          $this->assertArrayNotHasKey('mod_testcomponent2', $collectionarray);
  83      }
  84  
  85      /**
  86       * Test that get_contexts_for_userid() only returns contextlist collections for core providers.
  87       *
  88       * @covers ::get_contexts_for_userid
  89       */
  90      public function test_get_contexts_for_userid() {
  91          // Get a mock manager, in which the core components list is mocked to include all mock plugins.
  92          // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core).
  93          $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']);
  94  
  95          // Get the contextlist_collection.
  96          $contextlistcollection = $mockman->get_contexts_for_userid(10);
  97          $this->assertInstanceOf(\core_privacy\local\request\contextlist_collection::class, $contextlistcollection);
  98  
  99          ob_flush();
 100  
 101          // Verify we have a contextlist for the component in the collection.
 102          $this->assertInstanceOf(\core_privacy\local\request\contextlist::class,
 103                                  $contextlistcollection->get_contextlist_for_component('mod_testcomponent'));
 104  
 105          // Verify we don't have a contextlist for the shared provider in the collection.
 106          $this->assertNull($contextlistcollection->get_contextlist_for_component('mod_testcomponent3'));
 107  
 108          // Verify we don't have a contextlist for the component which does not store user data.
 109          $this->assertEmpty($contextlistcollection->get_contextlist_for_component('mod_testcomponent2'));
 110      }
 111  
 112      /**
 113       * Test verifying the output of component_is_compliant.
 114       *
 115       * @covers ::component_is_compliant
 116       */
 117      public function test_component_is_compliant() {
 118          // Get a mock manager, in which the core components list is mocked to include all mock plugins.
 119          // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core).
 120          $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']);
 121  
 122          // A core_provider plugin implementing all required interfaces (local\metadata\provider, local\request\plugin_provider).
 123          $this->assertTrue($mockman->component_is_compliant('mod_testcomponent'));
 124  
 125          // A component implementing just the \core_privacy\local\metadata\null_provider is compliant.
 126          $this->assertTrue($mockman->component_is_compliant('mod_testcomponent2'));
 127  
 128          // A shared provider plugin implementing all required interfaces (local\metadata\provider, local\request\plugin\subplugin_provider)
 129          // is compliant.
 130          $this->assertTrue($mockman->component_is_compliant('mod_testcomponent3'));
 131  
 132          // A component implementing none of the providers.
 133          $this->assertFalse($mockman->component_is_compliant('tool_thisisnotarealtool123'));
 134      }
 135  
 136      /**
 137       * Provider for component_is_compliant tests.
 138       *
 139       * @return  array
 140       */
 141      public function component_is_compliant_provider() {
 142          return [
 143              'An empty subsystem' => [
 144                  'core_countries',
 145                  true,
 146              ],
 147              'A real subsystem' => [
 148                  'core_privacy',
 149                  true,
 150              ],
 151          ];
 152      }
 153  
 154      /**
 155       * Test verifying the output of component_is_compliant with specified
 156       * components.
 157       *
 158       * @dataProvider    component_is_compliant_provider
 159       * @param   string  $component
 160       * @param   boolean $expected
 161       * @covers ::component_is_compliant
 162       */
 163      public function test_component_is_compliant_examples($component, $expected) {
 164          $manager = new \core_privacy\manager();
 165  
 166          $this->assertEquals($expected, $manager->component_is_compliant($component));
 167      }
 168  
 169      /**
 170       *  Test verifying only approved contextlists can be used with the export_user_data method.
 171       *
 172       * @covers ::export_user_data
 173       */
 174      public function test_export_user_data() {
 175          // Get a mock manager, in which the core components list is mocked to include all mock plugins.
 176          // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core).
 177          $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3', 'mod_testcomponent4']);
 178  
 179          // Get the non-approved contextlists.
 180          $contextlistcollection = $mockman->get_contexts_for_userid(10);
 181  
 182          // Create an approved contextlist.
 183          $approvedcontextlistcollection = new \core_privacy\local\request\contextlist_collection(10);
 184          foreach ($contextlistcollection->get_contextlists() as $contextlist) {
 185              $approvedcontextlist = new approved_contextlist(new \stdClass(), $contextlist->get_component(),
 186                  $contextlist->get_contextids());
 187              $approvedcontextlistcollection->add_contextlist($approvedcontextlist);
 188          }
 189          // Verify the mocked return from the writer, meaning the manager method exited normally.
 190          $this->assertEquals('mock_path', $mockman->export_user_data($approvedcontextlistcollection));
 191  
 192          // Verify that a user preference was exported for 'mod_testcomponent4'.
 193          $prefs = writer::with_context(\context_system::instance())->get_user_preferences('mod_testcomponent4');
 194          $this->assertNotEmpty($prefs);
 195          $this->assertNotEmpty($prefs->mykey);
 196          $this->assertEquals('myvalue', $prefs->mykey->value);
 197          $this->assertEquals('mydescription', $prefs->mykey->description);
 198  
 199          // Verify an exception is thrown if trying to pass in a collection of non-approved_contextlist items.
 200          $this->expectException(\moodle_exception::class);
 201          $mockman->export_user_data($contextlistcollection);
 202      }
 203  
 204      /**
 205       *  Test verifying only approved contextlists can be used with the delete_data_for_user method.
 206       *
 207       * @covers ::delete_data_for_user
 208       */
 209      public function test_delete_data_for_user() {
 210          $this->resetAfterTest();
 211          // Get a mock manager, in which the core components list is mocked to include all mock plugins.
 212          // testcomponent is a core provider, testcomponent2 is a null provider, testcomponent3 is subplugin provider (non core).
 213          $mockman = $this->get_mock_manager_with_core_components(['mod_testcomponent', 'mod_testcomponent2', 'mod_testcomponent3']);
 214  
 215          // Get the non-approved contextlists.
 216          $user = \core_user::get_user_by_username('admin');
 217          $contextlistcollection = $mockman->get_contexts_for_userid($user->id);
 218  
 219          // Create an approved contextlist.
 220          $approvedcontextlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
 221          foreach ($contextlistcollection->get_contextlists() as $contextlist) {
 222              $approvedcontextlist = new approved_contextlist($user, $contextlist->get_component(),
 223                  $contextlist->get_contextids());
 224              $approvedcontextlistcollection->add_contextlist($approvedcontextlist);
 225          }
 226  
 227          // Verify null, as the method has no return type and exits normally. Mainly checking we don't see any exception.
 228          $this->assertNull($mockman->delete_data_for_user($approvedcontextlistcollection));
 229  
 230          // Verify an exception is thrown if trying to pass in a collection of non-approved_contextlist items.
 231          $this->expectException(\moodle_exception::class);
 232          $mockman->delete_data_for_user($contextlistcollection);
 233      }
 234  
 235      /**
 236       * Ensure that all installed plugins can provide metadata.
 237       *
 238       * This really just checks that all providers can be safely autoloaded.
 239       *
 240       * @covers ::get_metadata_for_components
 241       */
 242      public function test_installed_plugins() {
 243          $manager = new \core_privacy\manager();
 244          $metadata = $manager->get_metadata_for_components();
 245          $this->assertNotEmpty($metadata);
 246      }
 247  
 248      /**
 249       * Test that the reason for the null provider is returned.
 250       *
 251       * @covers ::get_null_provider_reason
 252       */
 253      public function test_get_null_provider_reason() {
 254          $manager = new \core_privacy\manager();
 255          // Null providers return the reason string.
 256          $this->assertEquals('testcomponent2 null provider reason', $manager->get_null_provider_reason('mod_testcomponent2'));
 257          // Throw an exception if the wrong type of provider is given.
 258          $this->expectException(\coding_exception::class);
 259          $string = $manager->get_null_provider_reason('mod_testcomponent');
 260      }
 261  
 262      /**
 263       * Test that manager::plugintype_class_callback() can be executed.
 264       *
 265       * @covers ::plugintype_class_callback
 266       */
 267      public function test_plugintype_class_callback() {
 268          \core_privacy\manager::plugintype_class_callback('doesnotexist', 'unusable', 'foo', ['bar']);
 269      }
 270  
 271      /**
 272       * Test that manager::component_class_callback() can be executed.
 273       *
 274       * @covers ::component_class_callback
 275       */
 276      public function test_component_class_callback() {
 277          \core_privacy\manager::component_class_callback('foo_bar', 'unusable', 'foo', ['bar']);
 278      }
 279  
 280      /**
 281       * Test the manager::is_empty_subsystem function.
 282       *
 283       * @dataProvider is_empty_subsystem_provider
 284       * @param   string  $component
 285       * @param   bool    $expected
 286       * @covers ::is_empty_subsystem
 287       */
 288      public function test_is_empty_subsystem($component, $expected) {
 289          $this->assertEquals($expected, \core_privacy\manager::is_empty_subsystem($component));
 290      }
 291  
 292      /**
 293       * Test cases for the is_empty_subsystem function.
 294       *
 295       * @return array
 296       */
 297      public function is_empty_subsystem_provider() {
 298          return [
 299              'A subsystem which has no directory' => [
 300                  'core_langconfig',
 301                  true,
 302              ],
 303              'A subsystem with a directory' => [
 304                  'core_portfolio',
 305                  false,
 306              ],
 307              'A plugin' => [
 308                  'mod_forum',
 309                  false,
 310              ],
 311              'A plugintype' => [
 312                  'mod',
 313                  false,
 314              ],
 315              'An unprefixed subsystem with no directory' => [
 316                  'langconfig',
 317                  false,
 318              ],
 319          ];
 320      }
 321  
 322      /**
 323       * Test that get_contexts_for_userid() with a failing item.
 324       *
 325       * @covers ::get_contexts_for_userid
 326       */
 327      public function test_get_contexts_for_userid_with_failing() {
 328          // Get a mock manager, in which the core components list is mocked to include all mock plugins.
 329          // testcomponent is a core provider, testcomponent2 isa null provider, testcomponent3 is subplugin provider (non core).
 330          $mockman = $this->get_mock_manager_with_core_components(['mod_component_broken', 'mod_component_a']);
 331  
 332          $observer = $this->getMockBuilder(\core_privacy\manager_observer::class)
 333              ->onlyMethods(['handle_component_failure'])
 334              ->getMock();
 335          $mockman->set_observer($observer);
 336  
 337          $observer->expects($this->once())
 338              ->method('handle_component_failure')
 339              ->with(
 340                  $this->isInstanceOf(\coding_exception::class),
 341                  $this->identicalTo('mod_component_broken'),
 342                  $this->identicalTo(\core_privacy\local\request\core_user_data_provider::class),
 343                  $this->identicalTo('get_contexts_for_userid'),
 344                  $this->anything()
 345              );
 346  
 347          // Get the contextlist_collection.
 348          $contextlistcollection = $mockman->get_contexts_for_userid(10);
 349          $this->assertDebuggingCalled();
 350          $this->assertInstanceOf(\core_privacy\local\request\contextlist_collection::class, $contextlistcollection);
 351          $this->assertCount(1, $contextlistcollection);
 352  
 353          // The component which completed shoudl have returned a contextlist.
 354          $this->assertInstanceOf(\core_privacy\local\request\contextlist::class,
 355                                  $contextlistcollection->get_contextlist_for_component('mod_component_a'));
 356          $this->assertEmpty($contextlistcollection->get_contextlist_for_component('mod_component_broken'));
 357      }
 358  
 359      /**
 360       * Test that export_user_data() with a failing item.
 361       *
 362       * @covers ::export_user_data
 363       */
 364      public function test_export_user_data_with_failing() {
 365          $user = \core_user::get_user_by_username('admin');
 366          $mockman = $this->get_mock_manager_with_core_components(['mod_component_broken', 'mod_component_a']);
 367          $context = \context_system::instance();
 368          $contextid = $context->id;
 369  
 370          $observer = $this->getMockBuilder(\core_privacy\manager_observer::class)
 371              ->onlyMethods(['handle_component_failure'])
 372              ->getMock();
 373          $mockman->set_observer($observer);
 374  
 375          $observer->expects($this->once())
 376              ->method('handle_component_failure')
 377              ->with(
 378                  $this->isInstanceOf(\coding_exception::class),
 379                  $this->identicalTo('mod_component_broken'),
 380                  $this->identicalTo(\core_privacy\local\request\core_user_data_provider::class),
 381                  $this->identicalTo('export_user_data'),
 382                  $this->anything()
 383              );
 384  
 385          $collection = new \core_privacy\local\request\contextlist_collection(10);
 386          $collection->add_contextlist(new approved_contextlist($user, 'mod_component_broken', [$contextid]));
 387          $collection->add_contextlist(new approved_contextlist($user, 'mod_component_a', [$contextid]));
 388  
 389          // Get the contextlist_collection.
 390          $mockman->export_user_data($collection);
 391          $this->assertDebuggingCalled();
 392      }
 393  
 394      /**
 395       * Test that delete_data_for_user() with a failing item.
 396       *
 397       * @covers ::delete_data_for_user
 398       */
 399      public function test_delete_data_for_user_with_failing() {
 400          $user = \core_user::get_user_by_username('admin');
 401          $mockman = $this->get_mock_manager_with_core_components(['mod_component_broken', 'mod_component_a']);
 402          $context = \context_system::instance();
 403          $contextid = $context->id;
 404  
 405          $observer = $this->getMockBuilder(\core_privacy\manager_observer::class)
 406              ->onlyMethods(['handle_component_failure'])
 407              ->getMock();
 408          $mockman->set_observer($observer);
 409  
 410          $observer->expects($this->once())
 411              ->method('handle_component_failure')
 412              ->with(
 413                  $this->isInstanceOf(\coding_exception::class),
 414                  $this->identicalTo('mod_component_broken'),
 415                  $this->identicalTo(\core_privacy\local\request\core_user_data_provider::class),
 416                  $this->identicalTo('delete_data_for_user'),
 417                  $this->anything()
 418              );
 419  
 420          $collection = new \core_privacy\local\request\contextlist_collection(10);
 421          $collection->add_contextlist(new approved_contextlist($user, 'mod_component_broken', [$contextid]));
 422          $collection->add_contextlist(new approved_contextlist($user, 'mod_component_a', [$contextid]));
 423  
 424          // Get the contextlist_collection.
 425          $mockman->delete_data_for_user($collection);
 426          $this->assertDebuggingCalled();
 427      }
 428  
 429      /**
 430       * Test that delete_data_for_all_users_in_context() with a failing item.
 431       *
 432       * @covers ::delete_data_for_all_users_in_context
 433       */
 434      public function test_delete_data_for_all_users_in_context_with_failing() {
 435          $user = \core_user::get_user_by_username('admin');
 436          $mockman = $this->get_mock_manager_with_core_components(['mod_component_broken', 'mod_component_a']);
 437          $context = \context_system::instance();
 438  
 439          $observer = $this->getMockBuilder(\core_privacy\manager_observer::class)
 440              ->onlyMethods(['handle_component_failure'])
 441              ->getMock();
 442          $mockman->set_observer($observer);
 443  
 444          $observer->expects($this->once())
 445              ->method('handle_component_failure')
 446              ->with(
 447                  $this->isInstanceOf(\coding_exception::class),
 448                  $this->identicalTo('mod_component_broken'),
 449                  $this->identicalTo(\core_privacy\local\request\core_user_data_provider::class),
 450                  $this->identicalTo('delete_data_for_all_users_in_context'),
 451                  $this->anything()
 452              );
 453  
 454          // Get the contextlist_collection.
 455          $mockman->delete_data_for_all_users_in_context($context);
 456          $this->assertDebuggingCalled();
 457      }
 458  
 459      /**
 460       * Test that get_metadata_for_components() with a failing item.
 461       *
 462       * @covers ::get_metadata_for_components
 463       */
 464      public function test_get_metadata_for_components_with_failing() {
 465          $user = \core_user::get_user_by_username('admin');
 466          $mockman = $this->get_mock_manager_with_core_components(['mod_component_broken', 'mod_component_a']);
 467          $context = \context_system::instance();
 468  
 469          $observer = $this->getMockBuilder(\core_privacy\manager_observer::class)
 470              ->onlyMethods(['handle_component_failure'])
 471              ->getMock();
 472          $mockman->set_observer($observer);
 473  
 474          $observer->expects($this->once())
 475              ->method('handle_component_failure')
 476              ->with(
 477                  $this->isInstanceOf(\coding_exception::class),
 478                  $this->identicalTo('mod_component_broken'),
 479                  $this->identicalTo(\core_privacy\local\metadata\provider::class),
 480                  $this->identicalTo('get_metadata'),
 481                  $this->anything()
 482              );
 483  
 484          // Get the contextlist_collection.
 485          $metadata = $mockman->get_metadata_for_components();
 486          $this->assertDebuggingCalled();
 487  
 488          $this->assertIsArray($metadata);
 489          $this->assertCount(1, $metadata);
 490      }
 491  }