MDL-66804 user: consistent user access times in privacy export.
[moodle.git] / user / tests / privacy_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/>.
16 /**
17  * Privacy tests for core_user.
18  *
19  * @package    core_user
20  * @category   test
21  * @copyright  2018 Adrian Greeve <adrian@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
26 global $CFG;
28 use \core_privacy\tests\provider_testcase;
29 use \core_user\privacy\provider;
30 use \core_privacy\local\request\approved_userlist;
31 use \core_privacy\local\request\transform;
33 require_once($CFG->dirroot . "/user/lib.php");
35 /**
36  * Unit tests for core_user.
37  *
38  * @copyright  2018 Adrian Greeve <adrian@moodle.com>
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class core_user_privacy_testcase extends provider_testcase {
43     /**
44      * Check that context information is returned correctly.
45      */
46     public function test_get_contexts_for_userid() {
47         $this->resetAfterTest();
48         $user = $this->getDataGenerator()->create_user();
49         // Create some other users as well.
50         $user2 = $this->getDataGenerator()->create_user();
51         $user3 = $this->getDataGenerator()->create_user();
53         $context = context_user::instance($user->id);
54         $contextlist = \core_user\privacy\provider::get_contexts_for_userid($user->id);
55         $this->assertSame($context, $contextlist->current());
56     }
58     /**
59      * Test that data is exported as expected for a user.
60      */
61     public function test_export_user_data() {
62         $this->resetAfterTest();
63         $user = $this->getDataGenerator()->create_user([
64             'firstaccess' => 1535760000,
65             'lastaccess' => 1541030400,
66             'currentlogin' => 1541030400,
67         ]);
68         $course = $this->getDataGenerator()->create_course();
69         $context = \context_user::instance($user->id);
71         $this->create_data_for_user($user, $course);
73         $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'core_user', [$context->id]);
75         $writer = \core_privacy\local\request\writer::with_context($context);
76         \core_user\privacy\provider::export_user_data($approvedlist);
78         // Make sure that the password history only returns a count.
79         $history = $writer->get_data([get_string('privacy:passwordhistorypath', 'user')]);
80         $objectcount = new ArrayObject($history);
81         // This object should only have one property.
82         $this->assertCount(1, $objectcount);
83         $this->assertEquals(1, $history->password_history_count);
85         // Password resets should have two fields - timerequested and timererequested.
86         $resetarray = (array) $writer->get_data([get_string('privacy:passwordresetpath', 'user')]);
87         $detail = array_shift($resetarray);
88         $this->assertTrue(array_key_exists('timerequested', $detail));
89         $this->assertTrue(array_key_exists('timererequested', $detail));
91         // Last access to course.
92         $lastcourseaccess = (array) $writer->get_data([get_string('privacy:lastaccesspath', 'user')]);
93         $entry = array_shift($lastcourseaccess);
94         $this->assertEquals($course->fullname, $entry['course_name']);
95         $this->assertTrue(array_key_exists('timeaccess', $entry));
97         // User devices.
98         $userdevices = (array) $writer->get_data([get_string('privacy:devicespath', 'user')]);
99         $entry = array_shift($userdevices);
100         $this->assertEquals('com.moodle.moodlemobile', $entry['appid']);
101         // Make sure these fields are not exported.
102         $this->assertFalse(array_key_exists('pushid', $entry));
103         $this->assertFalse(array_key_exists('uuid', $entry));
105         // Session data.
106         $sessiondata = (array) $writer->get_data([get_string('privacy:sessionpath', 'user')]);
107         $entry = array_shift($sessiondata);
108         // Make sure that the sid is not exported.
109         $this->assertFalse(array_key_exists('sid', $entry));
110         // Check that some of the other fields are present.
111         $this->assertTrue(array_key_exists('state', $entry));
112         $this->assertTrue(array_key_exists('sessdata', $entry));
113         $this->assertTrue(array_key_exists('timecreated', $entry));
115         // Course requests
116         $courserequestdata = (array) $writer->get_data([get_string('privacy:courserequestpath', 'user')]);
117         $entry = array_shift($courserequestdata);
118         // Make sure that the password is not exported.
119         $this->assertFalse(array_key_exists('password', $entry));
120         // Check that some of the other fields are present.
121         $this->assertTrue(array_key_exists('fullname', $entry));
122         $this->assertTrue(array_key_exists('shortname', $entry));
123         $this->assertTrue(array_key_exists('summary', $entry));
125          // User details.
126         $userdata = (array) $writer->get_data([]);
127         // Check that the password is not exported.
128         $this->assertFalse(array_key_exists('password', $userdata));
129         // Check that some critical fields exist.
130         $this->assertTrue(array_key_exists('firstname', $userdata));
131         $this->assertTrue(array_key_exists('lastname', $userdata));
132         $this->assertTrue(array_key_exists('email', $userdata));
133         // Check access times.
134         $this->assertEquals(transform::datetime($user->firstaccess), $userdata['firstaccess']);
135         $this->assertEquals(transform::datetime($user->lastaccess), $userdata['lastaccess']);
136         $this->assertNull($userdata['lastlogin']);
137         $this->assertEquals(transform::datetime($user->currentlogin), $userdata['currentlogin']);
138     }
140     /**
141      * Test that user data is deleted for one user.
142      */
143     public function test_delete_data_for_all_users_in_context() {
144         global $DB;
145         $this->resetAfterTest();
146         $user = $this->getDataGenerator()->create_user([
147             'idnumber' => 'A0023',
148             'emailstop' => 1,
149             'icq' => 'aksdjf98',
150             'phone1' => '555 3257',
151             'institution' => 'test',
152             'department' => 'Science',
153             'city' => 'Perth',
154             'country' => 'au'
155         ]);
156         $user2 = $this->getDataGenerator()->create_user();
157         $course = $this->getDataGenerator()->create_course();
159         $this->create_data_for_user($user, $course);
160         $this->create_data_for_user($user2, $course);
162         \core_user\privacy\provider::delete_data_for_all_users_in_context(context_user::instance($user->id));
164         // These tables should not have any user data for $user. Only for $user2.
165         $records = $DB->get_records('user_password_history');
166         $this->assertCount(1, $records);
167         $data = array_shift($records);
168         $this->assertNotEquals($user->id, $data->userid);
169         $this->assertEquals($user2->id, $data->userid);
170         $records = $DB->get_records('user_password_resets');
171         $this->assertCount(1, $records);
172         $data = array_shift($records);
173         $this->assertNotEquals($user->id, $data->userid);
174         $this->assertEquals($user2->id, $data->userid);
175         $records = $DB->get_records('user_lastaccess');
176         $this->assertCount(1, $records);
177         $data = array_shift($records);
178         $this->assertNotEquals($user->id, $data->userid);
179         $this->assertEquals($user2->id, $data->userid);
180         $records = $DB->get_records('user_devices');
181         $this->assertCount(1, $records);
182         $data = array_shift($records);
183         $this->assertNotEquals($user->id, $data->userid);
184         $this->assertEquals($user2->id, $data->userid);
186         // Now check that there is still a record for the deleted user, but that non-critical information is removed.
187         $record = $DB->get_record('user', ['id' => $user->id]);
188         $this->assertEmpty($record->idnumber);
189         $this->assertEmpty($record->emailstop);
190         $this->assertEmpty($record->icq);
191         $this->assertEmpty($record->phone1);
192         $this->assertEmpty($record->institution);
193         $this->assertEmpty($record->department);
194         $this->assertEmpty($record->city);
195         $this->assertEmpty($record->country);
196         $this->assertEmpty($record->timezone);
197         $this->assertEmpty($record->timecreated);
198         $this->assertEmpty($record->timemodified);
199         $this->assertEmpty($record->firstnamephonetic);
200         // Check for critical fields.
201         // Deleted should now be 1.
202         $this->assertEquals(1, $record->deleted);
203         $this->assertEquals($user->id, $record->id);
204         $this->assertEquals($user->username, $record->username);
205         $this->assertEquals($user->password, $record->password);
206         $this->assertEquals($user->firstname, $record->firstname);
207         $this->assertEquals($user->lastname, $record->lastname);
208         $this->assertEquals($user->email, $record->email);
209     }
211     /**
212      * Test that user data is deleted for one user.
213      */
214     public function test_delete_data_for_user() {
215         global $DB;
216         $this->resetAfterTest();
217         $user = $this->getDataGenerator()->create_user([
218             'idnumber' => 'A0023',
219             'emailstop' => 1,
220             'icq' => 'aksdjf98',
221             'phone1' => '555 3257',
222             'institution' => 'test',
223             'department' => 'Science',
224             'city' => 'Perth',
225             'country' => 'au'
226         ]);
227         $user2 = $this->getDataGenerator()->create_user();
228         $course = $this->getDataGenerator()->create_course();
230         $this->create_data_for_user($user, $course);
231         $this->create_data_for_user($user2, $course);
233         // Provide multiple different context to check that only the correct user is deleted.
234         $contexts = [context_user::instance($user->id)->id, context_user::instance($user2->id)->id, context_system::instance()->id];
235         $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'core_user', $contexts);
237         \core_user\privacy\provider::delete_data_for_user($approvedlist);
239         // These tables should not have any user data for $user. Only for $user2.
240         $records = $DB->get_records('user_password_history');
241         $this->assertCount(1, $records);
242         $data = array_shift($records);
243         $this->assertNotEquals($user->id, $data->userid);
244         $this->assertEquals($user2->id, $data->userid);
245         $records = $DB->get_records('user_password_resets');
246         $this->assertCount(1, $records);
247         $data = array_shift($records);
248         $this->assertNotEquals($user->id, $data->userid);
249         $this->assertEquals($user2->id, $data->userid);
250         $records = $DB->get_records('user_lastaccess');
251         $this->assertCount(1, $records);
252         $data = array_shift($records);
253         $this->assertNotEquals($user->id, $data->userid);
254         $this->assertEquals($user2->id, $data->userid);
255         $records = $DB->get_records('user_devices');
256         $this->assertCount(1, $records);
257         $data = array_shift($records);
258         $this->assertNotEquals($user->id, $data->userid);
259         $this->assertEquals($user2->id, $data->userid);
261         // Now check that there is still a record for the deleted user, but that non-critical information is removed.
262         $record = $DB->get_record('user', ['id' => $user->id]);
263         $this->assertEmpty($record->idnumber);
264         $this->assertEmpty($record->emailstop);
265         $this->assertEmpty($record->icq);
266         $this->assertEmpty($record->phone1);
267         $this->assertEmpty($record->institution);
268         $this->assertEmpty($record->department);
269         $this->assertEmpty($record->city);
270         $this->assertEmpty($record->country);
271         $this->assertEmpty($record->timezone);
272         $this->assertEmpty($record->timecreated);
273         $this->assertEmpty($record->timemodified);
274         $this->assertEmpty($record->firstnamephonetic);
275         // Check for critical fields.
276         // Deleted should now be 1.
277         $this->assertEquals(1, $record->deleted);
278         $this->assertEquals($user->id, $record->id);
279         $this->assertEquals($user->username, $record->username);
280         $this->assertEquals($user->password, $record->password);
281         $this->assertEquals($user->firstname, $record->firstname);
282         $this->assertEquals($user->lastname, $record->lastname);
283         $this->assertEquals($user->email, $record->email);
284     }
286     /**
287      * Test that only users with a user context are fetched.
288      */
289     public function test_get_users_in_context() {
290         $this->resetAfterTest();
292         $component = 'core_user';
293         // Create a user.
294         $user = $this->getDataGenerator()->create_user();
295         $usercontext = \context_user::instance($user->id);
296         $userlist = new \core_privacy\local\request\userlist($usercontext, $component);
298         // The list of users for user context should return the user.
299         provider::get_users_in_context($userlist);
300         $this->assertCount(1, $userlist);
301         $expected = [$user->id];
302         $actual = $userlist->get_userids();
303         $this->assertEquals($expected, $actual);
305         // The list of users for system context should not return any users.
306         $systemcontext = context_system::instance();
307         $userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
308         provider::get_users_in_context($userlist);
309         $this->assertCount(0, $userlist);
310     }
312     /**
313      * Test that data for users in approved userlist is deleted.
314      */
315     public function test_delete_data_for_users() {
316         global $DB;
318         $this->resetAfterTest();
320         $component = 'core_user';
322         // Create user1.
323         $user1 = $this->getDataGenerator()->create_user([
324             'idnumber' => 'A0023',
325             'emailstop' => 1,
326             'icq' => 'aksdjf98',
327             'phone1' => '555 3257',
328             'institution' => 'test',
329             'department' => 'Science',
330             'city' => 'Perth',
331             'country' => 'au'
332         ]);
333         $usercontext1 = \context_user::instance($user1->id);
334         $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
336         // Create user2.
337         $user2 = $this->getDataGenerator()->create_user([
338             'idnumber' => 'A0024',
339             'emailstop' => 1,
340             'icq' => 'aksdjf981',
341             'phone1' => '555 3258',
342             'institution' => 'test',
343             'department' => 'Science',
344             'city' => 'Perth',
345             'country' => 'au'
346         ]);
347         $usercontext2 = \context_user::instance($user2->id);
348         $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
350         // The list of users for usercontext1 should return user1.
351         provider::get_users_in_context($userlist1);
352         $this->assertCount(1, $userlist1);
353         // The list of users for usercontext2 should return user2.
354         provider::get_users_in_context($userlist2);
355         $this->assertCount(1, $userlist2);
357         // Add userlist1 to the approved user list.
358         $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids());
359         // Delete using delete_data_for_users().
360         provider::delete_data_for_users($approvedlist);
362         // Now check that there is still a record for user1 (deleted user), but non-critical information is removed.
363         $record = $DB->get_record('user', ['id' => $user1->id]);
364         $this->assertEmpty($record->idnumber);
365         $this->assertEmpty($record->emailstop);
366         $this->assertEmpty($record->icq);
367         $this->assertEmpty($record->phone1);
368         $this->assertEmpty($record->institution);
369         $this->assertEmpty($record->department);
370         $this->assertEmpty($record->city);
371         $this->assertEmpty($record->country);
372         $this->assertEmpty($record->timezone);
373         $this->assertEmpty($record->timecreated);
374         $this->assertEmpty($record->timemodified);
375         $this->assertEmpty($record->firstnamephonetic);
376         // Check for critical fields.
377         // Deleted should now be 1.
378         $this->assertEquals(1, $record->deleted);
379         $this->assertEquals($user1->id, $record->id);
380         $this->assertEquals($user1->username, $record->username);
381         $this->assertEquals($user1->password, $record->password);
382         $this->assertEquals($user1->firstname, $record->firstname);
383         $this->assertEquals($user1->lastname, $record->lastname);
384         $this->assertEquals($user1->email, $record->email);
386         // Now check that the record and information for user2 is still present.
387         $record = $DB->get_record('user', ['id' => $user2->id]);
388         $this->assertNotEmpty($record->idnumber);
389         $this->assertNotEmpty($record->emailstop);
390         $this->assertNotEmpty($record->icq);
391         $this->assertNotEmpty($record->phone1);
392         $this->assertNotEmpty($record->institution);
393         $this->assertNotEmpty($record->department);
394         $this->assertNotEmpty($record->city);
395         $this->assertNotEmpty($record->country);
396         $this->assertNotEmpty($record->timezone);
397         $this->assertNotEmpty($record->timecreated);
398         $this->assertNotEmpty($record->timemodified);
399         $this->assertNotEmpty($record->firstnamephonetic);
400         $this->assertEquals(0, $record->deleted);
401         $this->assertEquals($user2->id, $record->id);
402         $this->assertEquals($user2->username, $record->username);
403         $this->assertEquals($user2->password, $record->password);
404         $this->assertEquals($user2->firstname, $record->firstname);
405         $this->assertEquals($user2->lastname, $record->lastname);
406         $this->assertEquals($user2->email, $record->email);
407     }
409     /**
410      * Create user data for a user.
411      *
412      * @param  stdClass $user A user object.
413      * @param  stdClass $course A course.
414      */
415     protected function create_data_for_user($user, $course) {
416         global $DB;
417         $this->resetAfterTest();
418         // Last course access.
419         $lastaccess = (object) [
420             'userid' => $user->id,
421             'courseid' => $course->id,
422             'timeaccess' => time() - DAYSECS
423         ];
424         $DB->insert_record('user_lastaccess', $lastaccess);
426         // Password history.
427         $history = (object) [
428             'userid' => $user->id,
429             'hash' => 'HID098djJUU',
430             'timecreated' => time()
431         ];
432         $DB->insert_record('user_password_history', $history);
434         // Password resets.
435         $passwordreset = (object) [
436             'userid' => $user->id,
437             'timerequested' => time(),
438             'timererequested' => time(),
439             'token' => $this->generate_random_string()
440         ];
441         $DB->insert_record('user_password_resets', $passwordreset);
443         // User mobile devices.
444         $userdevices = (object) [
445             'userid' => $user->id,
446             'appid' => 'com.moodle.moodlemobile',
447             'name' => 'occam',
448             'model' => 'Nexus 4',
449             'platform' => 'Android',
450             'version' => '4.2.2',
451             'pushid' => 'kishUhd',
452             'uuid' => 'KIhud7s',
453             'timecreated' => time(),
454             'timemodified' => time()
455         ];
456         $DB->insert_record('user_devices', $userdevices);
458         // Course request.
459         $courserequest = (object) [
460             'fullname' => 'Test Course',
461             'shortname' => 'TC',
462             'summary' => 'Summary of course',
463             'summaryformat' => 1,
464             'category' => 1,
465             'reason' => 'Because it would be nice.',
466             'requester' => $user->id,
467             'password' => ''
468         ];
469         $DB->insert_record('course_request', $courserequest);
471         // User session table data.
472         $usersessions = (object) [
473             'state' => 0,
474             'sid' => $this->generate_random_string(), // Needs a unique id.
475             'userid' => $user->id,
476             'sessdata' => 'Nothing',
477             'timecreated' => time(),
478             'timemodified' => time(),
479             'firstip' => '0.0.0.0',
480             'lastip' => '0.0.0.0'
481         ];
482         $DB->insert_record('sessions', $usersessions);
483     }
485     /**
486      * Create a random string.
487      *
488      * @param  integer $length length of the string to generate.
489      * @return string A random string.
490      */
491     protected function generate_random_string($length = 6) {
492         $response = '';
493         $source = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
495         if ($length > 0) {
497             $response = '';
498             $source = str_split($source, 1);
500             for ($i = 1; $i <= $length; $i++) {
501                 $num = mt_rand(1, count($source));
502                 $response .= $source[$num - 1];
503             }
504         }
506         return $response;
507     }