MDL-62601 core_privacy: Ensure providers can handle deleted users
[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      * Data provider for the metadata\provider tests.
206      *
207      * @return array
208      */
209     public function metadata_provider_provider() {
210         return array_filter($this->get_component_list(), function($component) {
211                 return static::component_implements(
212                     $component['classname'],
213                     \core_privacy\local\metadata\provider::class
214                 );
215         });
216     }
218     /**
219      * List of providers which implement the core_user_data_provider.
220      *
221      * @return array
222      */
223     public function is_user_data_provider() {
224         return array_filter($this->get_component_list(), function($component) {
225                 return static::component_implements(
226                     $component['classname'],
227                     \core_privacy\local\request\core_user_data_provider::class
228                 );
229         });
230     }
232     /**
233      * Checks whether the component's provider class implements the specified interface, either directly or as a grandchild.
234      *
235      * @param   string  $providerclass The name of the class to test.
236      * @param   string  $interface the name of the interface we want to check.
237      * @return  bool    Whether the class implements the interface.
238      */
239     protected static function component_implements($providerclass, $interface) {
240         if (class_exists($providerclass) && interface_exists($interface)) {
241             return is_subclass_of($providerclass, $interface);
242         }
244         return false;
245     }
247     /**
248      * Finds user fields in a table
249      *
250      * Returns fields that have foreign key to user table and fields that are named 'userid'.
251      *
252      * @param xmldb_table $table
253      * @return array
254      */
255     protected function get_userid_fields(xmldb_table $table) {
256         $userfields = [];
258         // Find all fields that have a foreign key to 'id' field in 'user' table.
259         $keys = $table->getKeys();
260         foreach ($keys as $key) {
261             $reffields = $key->getRefFields();
262             $fields = $key->getFields();
263             if ($key->getRefTable() === 'user' && count($reffields) == 1 && $reffields[0] == 'id' && count($fields) == 1) {
264                 $userfields[$fields[0]] = $fields[0];
265             }
266         }
267         // Find fields with the name 'userid' even if they don't have a foreign key.
268         $fields = $table->getFields();
269         foreach ($fields as $field) {
270             if ($field->getName() == 'userid') {
271                 $userfields['userid'] = 'userid';
272             }
273         }
275         return $userfields;
276     }
278     /**
279      * Test that all tables with user fields are covered by metadata providers
280      */
281     public function test_table_coverage() {
282         global $DB;
283         $dbman = $DB->get_manager();
284         $schema = $dbman->get_install_xml_schema();
285         $tables = [];
286         foreach ($schema->getTables() as $table) {
287             if ($table->getName() === 'role_sortorder') {
288                 // TODO MDL-62459 this table is not used anywhere. Remove the table and this statement.
289                 continue;
290             }
291             if ($fields = $this->get_userid_fields($table)) {
292                 $tables[$table->getName()] = '  - ' . $table->getName() . ' (' . join(', ', $fields) . ')';
293             }
294         }
296         $componentlist = $this->metadata_provider_provider();
297         foreach ($componentlist as $componentarray) {
298             $component = $componentarray['component'];
299             $classname = $componentarray['classname'];
300             $collection = new collection($component);
301             $metadata = $classname::get_metadata($collection);
302             foreach ($metadata->get_collection() as $item) {
303                 if ($item instanceof database_table) {
304                     unset($tables[$item->get_name()]);
305                 }
306             }
307         }
309         if ($tables) {
310             $this->fail("The following tables with user fields must be covered with metadata providers: \n".
311                 join("\n", $tables));
312         }
314     }