6261528a069969f3c8cbfd89d0f82632ff374dff
[moodle.git] / privacy / tests / provider_test.php
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/>.
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  */
25 defined('MOODLE_INTERNAL') || die();
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;
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                 ];
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     }
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->assertInternalType('string', $reason);
90         $this->assertInternalType('string', get_string($reason, $component));
91         $this->assertDebuggingNotCalled();
92     }
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     }
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;
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());
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->assertInternalType('string', $item->get_name());
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             }
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             }
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             }
146             if ($summary = $item->get_summary()) {
147                 // Summary is optional, but when provided must be a valid string identifier.
148                 $this->assertInternalType('string', $summary);
150                 // Check that the string is also correctly defined.
151                 $this->assertInternalType('string', get_string($summary, $component));
152                 $this->assertDebuggingNotCalled();
153             }
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->assertInternalType('string', $field);
159                     $this->assertInternalType('string', $identifier);
161                     // Check that the string is also correctly defined.
162                     $this->assertInternalType('string', get_string($identifier, $component));
163                     $this->assertDebuggingNotCalled();
164                 }
165             }
166         }
167     }
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     }
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();
190         // Create a user.
191         $user = $this->getDataGenerator()->create_user();
193         // Delete the user and their context.
194         delete_user($user);
195         $usercontext = \context_user::instance($user->id);
196         $usercontext->delete();
198         $contextlist = manager::component_class_callback($component, \core_privacy\local\request\core_user_data_provider::class,
199                 'get_contexts_for_userid', [$user->id]);
201         $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, $contextlist);
202     }
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     }
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     }
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     }
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         }
255         return false;
256     }
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 = [];
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         }
286         return $userfields;
287     }
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         $schema = $dbman->get_install_xml_schema();
296         $tables = [];
297         foreach ($schema->getTables() as $table) {
298             if ($table->getName() === 'role_sortorder') {
299                 // TODO MDL-62459 this table is not used anywhere. Remove the table and this statement.
300                 continue;
301             }
302             if ($fields = $this->get_userid_fields($table)) {
303                 $tables[$table->getName()] = '  - ' . $table->getName() . ' (' . join(', ', $fields) . ')';
304             }
305         }
307         $componentlist = $this->metadata_provider_provider();
308         foreach ($componentlist as $componentarray) {
309             $component = $componentarray['component'];
310             $classname = $componentarray['classname'];
311             $collection = new collection($component);
312             $metadata = $classname::get_metadata($collection);
313             foreach ($metadata->get_collection() as $item) {
314                 if ($item instanceof database_table) {
315                     unset($tables[$item->get_name()]);
316                 }
317             }
318         }
320         if ($tables) {
321             $this->fail("The following tables with user fields must be covered with metadata providers: \n".
322                 join("\n", $tables));
323         }
325     }