Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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