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
25defined('MOODLE_INTERNAL') || die();
26
27use \core_privacy\manager;
28use \core_privacy\local\metadata\collection;
29use \core_privacy\local\metadata\types\type;
30use \core_privacy\local\metadata\types\database_table;
31use \core_privacy\local\metadata\types\external_location;
32use \core_privacy\local\metadata\types\plugin_type_link;
33use \core_privacy\local\metadata\types\subsystem_link;
34use \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 */
42class 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}
332