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 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   * Unit tests for all Privacy Providers.
  19   *
  20   * @package     core_privacy
  21   * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  use \core_privacy\manager;
  28  use \core_privacy\local\metadata\collection;
  29  use \core_privacy\local\metadata\types\type;
  30  use \core_privacy\local\metadata\types\database_table;
  31  use \core_privacy\local\metadata\types\external_location;
  32  use \core_privacy\local\metadata\types\plugin_type_link;
  33  use \core_privacy\local\metadata\types\subsystem_link;
  34  use \core_privacy\local\metadata\types\user_preference;
  35  
  36  /**
  37   * Unit tests for all Privacy Providers.
  38   *
  39   * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  40   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class provider_testcase extends advanced_testcase {
  43      /**
  44       * Returns a list of frankenstyle names of core components (plugins and subsystems).
  45       *
  46       * @return array the array of frankenstyle component names with the relevant class name.
  47       */
  48      public function get_component_list() {
  49          $components = ['core' => [
  50              'component' => 'core',
  51              'classname' => manager::get_provider_classname_for_component('core')
  52          ]];
  53          // Get all plugins.
  54          $plugintypes = \core_component::get_plugin_types();
  55          foreach ($plugintypes as $plugintype => $typedir) {
  56              $plugins = \core_component::get_plugin_list($plugintype);
  57              foreach ($plugins as $pluginname => $plugindir) {
  58                  $frankenstyle = $plugintype . '_' . $pluginname;
  59                  $components[$frankenstyle] = [
  60                      'component' => $frankenstyle,
  61                      'classname' => manager::get_provider_classname_for_component($frankenstyle),
  62                  ];
  63  
  64              }
  65          }
  66          // Get all subsystems.
  67          foreach (\core_component::get_core_subsystems() as $name => $path) {
  68              if (isset($path)) {
  69                  $frankenstyle = 'core_' . $name;
  70                  $components[$frankenstyle] = [
  71                      'component' => $frankenstyle,
  72                      'classname' => manager::get_provider_classname_for_component($frankenstyle),
  73                  ];
  74              }
  75          }
  76          return $components;
  77      }
  78  
  79      /**
  80       * Test that the specified null_provider works as expected.
  81       *
  82       * @dataProvider null_provider_provider
  83       * @param   string  $component The name of the component.
  84       * @param   string  $classname The name of the class for privacy
  85       */
  86      public function test_null_provider($component, $classname) {
  87          $reason = $classname::get_reason();
  88          $this->assertIsString($reason);
  89  
  90          $this->assertIsString(get_string($reason, $component));
  91          $this->assertDebuggingNotCalled();
  92      }
  93  
  94      /**
  95       * Data provider for the null_provider tests.
  96       *
  97       * @return array
  98       */
  99      public function null_provider_provider() {
 100          return array_filter($this->get_component_list(), function($component) {
 101                  return static::component_implements(
 102                      $component['classname'],
 103                      \core_privacy\local\metadata\null_provider::class
 104                  );
 105          });
 106      }
 107  
 108      /**
 109       * Test that the specified metadata_provider works as expected.
 110       *
 111       * @dataProvider metadata_provider_provider
 112       * @param   string  $component The name of the component.
 113       * @param   string  $classname The name of the class for privacy
 114       */
 115      public function test_metadata_provider($component, $classname) {
 116          global $DB;
 117  
 118          $collection = new collection($component);
 119          $metadata = $classname::get_metadata($collection);
 120          $this->assertInstanceOf(collection::class, $metadata);
 121          $this->assertSame($collection, $metadata);
 122          $this->assertContainsOnlyInstancesOf(type::class, $metadata->get_collection());
 123  
 124          foreach ($metadata->get_collection() as $item) {
 125              // All items must have a valid string name.
 126              // Note: This is not a string identifier.
 127              $this->assertIsString($item->get_name());
 128  
 129              if ($item instanceof database_table) {
 130                  // Check that the table is valid.
 131                  $this->assertTrue($DB->get_manager()->table_exists($item->get_name()));
 132              }
 133  
 134              if ($item instanceof \core_privacy\local\metadata\types\plugintype_link) {
 135                  // Check that plugin type is valid.
 136                  $this->assertTrue(array_key_exists($item->get_name(), \core_component::get_plugin_types()));
 137              }
 138  
 139              if ($item instanceof subsystem_link) {
 140                  // Check that core subsystem exists.
 141                  list($plugintype, $pluginname) = \core_component::normalize_component($item->get_name());
 142                  $this->assertEquals('core', $plugintype);
 143                  $this->assertTrue(\core_component::is_core_subsystem($pluginname));
 144              }
 145  
 146              if ($summary = $item->get_summary()) {
 147                  // Summary is optional, but when provided must be a valid string identifier.
 148                  $this->assertIsString($summary);
 149  
 150                  // Check that the string is also correctly defined.
 151                  $this->assertIsString(get_string($summary, $component));
 152                  $this->assertDebuggingNotCalled();
 153              }
 154  
 155              if ($fields = $item->get_privacy_fields()) {
 156                  // Privacy fields are optional, but when provided must be a valid string identifier.
 157                  foreach ($fields as $field => $identifier) {
 158                      $this->assertIsString($field);
 159                      $this->assertIsString($identifier);
 160  
 161                      // Check that the string is also correctly defined.
 162                      $this->assertIsString(get_string($identifier, $component));
 163                      $this->assertDebuggingNotCalled();
 164                  }
 165              }
 166          }
 167      }
 168  
 169      /**
 170       * Test that all providers implement some form of compliant provider.
 171       *
 172       * @dataProvider get_component_list
 173       * @param string $component frankenstyle component name, e.g. 'mod_assign'
 174       * @param string $classname the fully qualified provider classname
 175       */
 176      public function test_all_providers_compliant($component, $classname) {
 177          $manager = new manager();
 178          $this->assertTrue($manager->component_is_compliant($component));
 179      }
 180  
 181      /**
 182       * Ensure that providers do not throw an error when processing a deleted user.
 183       *
 184       * @dataProvider    is_user_data_provider
 185       * @param   string  $component
 186       */
 187      public function test_component_understands_deleted_users($component) {
 188          $this->resetAfterTest();
 189  
 190          // Create a user.
 191          $user = $this->getDataGenerator()->create_user();
 192  
 193          // Delete the user and their context.
 194          delete_user($user);
 195          $usercontext = \context_user::instance($user->id);
 196          $usercontext->delete();
 197  
 198          $contextlist = manager::component_class_callback($component, \core_privacy\local\request\core_user_data_provider::class,
 199                  'get_contexts_for_userid', [$user->id]);
 200  
 201          $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, $contextlist);
 202      }
 203  
 204      /**
 205       * Ensure that providers do not throw an error when processing a deleted user.
 206       *
 207       * @dataProvider    is_user_data_provider
 208       * @param   string  $component
 209       */
 210      public function test_userdata_provider_implements_userlist($component) {
 211          $classname = manager::get_provider_classname_for_component($component);
 212          $this->assertTrue(is_subclass_of($classname, \core_privacy\local\request\core_userlist_provider::class));
 213      }
 214  
 215      /**
 216       * Data provider for the metadata\provider tests.
 217       *
 218       * @return array
 219       */
 220      public function metadata_provider_provider() {
 221          return array_filter($this->get_component_list(), function($component) {
 222                  return static::component_implements(
 223                      $component['classname'],
 224                      \core_privacy\local\metadata\provider::class
 225                  );
 226          });
 227      }
 228  
 229      /**
 230       * List of providers which implement the core_user_data_provider.
 231       *
 232       * @return array
 233       */
 234      public function is_user_data_provider() {
 235          return array_filter($this->get_component_list(), function($component) {
 236                  return static::component_implements(
 237                      $component['classname'],
 238                      \core_privacy\local\request\core_user_data_provider::class
 239                  );
 240          });
 241      }
 242  
 243      /**
 244       * Checks whether the component's provider class implements the specified interface, either directly or as a grandchild.
 245       *
 246       * @param   string  $providerclass The name of the class to test.
 247       * @param   string  $interface the name of the interface we want to check.
 248       * @return  bool    Whether the class implements the interface.
 249       */
 250      protected static function component_implements($providerclass, $interface) {
 251          if (class_exists($providerclass) && interface_exists($interface)) {
 252              return is_subclass_of($providerclass, $interface);
 253          }
 254  
 255          return false;
 256      }
 257  
 258      /**
 259       * Finds user fields in a table
 260       *
 261       * Returns fields that have foreign key to user table and fields that are named 'userid'.
 262       *
 263       * @param xmldb_table $table
 264       * @return array
 265       */
 266      protected function get_userid_fields(xmldb_table $table) {
 267          $userfields = [];
 268  
 269          // Find all fields that have a foreign key to 'id' field in 'user' table.
 270          $keys = $table->getKeys();
 271          foreach ($keys as $key) {
 272              $reffields = $key->getRefFields();
 273              $fields = $key->getFields();
 274              if ($key->getRefTable() === 'user' && count($reffields) == 1 && $reffields[0] == 'id' && count($fields) == 1) {
 275                  $userfields[$fields[0]] = $fields[0];
 276              }
 277          }
 278          // Find fields with the name 'userid' even if they don't have a foreign key.
 279          $fields = $table->getFields();
 280          foreach ($fields as $field) {
 281              if ($field->getName() == 'userid') {
 282                  $userfields['userid'] = 'userid';
 283              }
 284          }
 285  
 286          return $userfields;
 287      }
 288  
 289      /**
 290       * Test that all tables with user fields are covered by metadata providers
 291       */
 292      public function test_table_coverage() {
 293          global $DB;
 294          $dbman = $DB->get_manager();
 295          $tables = [];
 296  
 297          foreach ($dbman->get_install_xml_files() as $filename) {
 298              $xmldbfile = new xmldb_file($filename);
 299              if (!$xmldbfile->loadXMLStructure()) {
 300                  continue;
 301              }
 302              $structure = $xmldbfile->getStructure();
 303              $tablelist = $structure->getTables();
 304  
 305              foreach ($tablelist as $table) {
 306                  if ($fields = $this->get_userid_fields($table)) {
 307                      $tables[$table->getName()] = '  - ' . $table->getName() . ' (' . join(', ', $fields) . ')';
 308                  }
 309              }
 310          }
 311  
 312          $componentlist = $this->metadata_provider_provider();
 313          foreach ($componentlist as $componentarray) {
 314              $component = $componentarray['component'];
 315              $classname = $componentarray['classname'];
 316              $collection = new collection($component);
 317              $metadata = $classname::get_metadata($collection);
 318              foreach ($metadata->get_collection() as $item) {
 319                  if ($item instanceof database_table) {
 320                      unset($tables[$item->get_name()]);
 321                  }
 322              }
 323          }
 324  
 325          if ($tables) {
 326              $this->fail("The following tables with user fields must be covered with metadata providers: \n".
 327                  join("\n", $tables));
 328          }
 329  
 330      }
 331  }