MDL-66804 user: consistent user access times in privacy export.
[moodle.git] / user / tests / privacy_test.php
CommitLineData
c49f3092
AG
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 */
24
25defined('MOODLE_INTERNAL') || die();
26global $CFG;
27
28use \core_privacy\tests\provider_testcase;
d4d28485
MG
29use \core_user\privacy\provider;
30use \core_privacy\local\request\approved_userlist;
8208292f 31use \core_privacy\local\request\transform;
c49f3092
AG
32
33require_once($CFG->dirroot . "/user/lib.php");
34
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 */
41class core_user_privacy_testcase extends provider_testcase {
42
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();
52
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 }
57
58 /**
59 * Test that data is exported as expected for a user.
60 */
61 public function test_export_user_data() {
62 $this->resetAfterTest();
8208292f
PH
63 $user = $this->getDataGenerator()->create_user([
64 'firstaccess' => 1535760000,
65 'lastaccess' => 1541030400,
66 'currentlogin' => 1541030400,
67 ]);
c49f3092
AG
68 $course = $this->getDataGenerator()->create_course();
69 $context = \context_user::instance($user->id);
70
71 $this->create_data_for_user($user, $course);
72
73 $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'core_user', [$context->id]);
74
75 $writer = \core_privacy\local\request\writer::with_context($context);
76 \core_user\privacy\provider::export_user_data($approvedlist);
77
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);
84
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));
90
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));
96
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));
104
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));
114
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));
124
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));
8208292f
PH
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']);
c49f3092
AG
138 }
139
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();
158
159 $this->create_data_for_user($user, $course);
160 $this->create_data_for_user($user2, $course);
161
162 \core_user\privacy\provider::delete_data_for_all_users_in_context(context_user::instance($user->id));
163
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);
185
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 }
210
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();
229
230 $this->create_data_for_user($user, $course);
231 $this->create_data_for_user($user2, $course);
232
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);
236
237 \core_user\privacy\provider::delete_data_for_user($approvedlist);
238
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);
260
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 }
285
d4d28485
MG
286 /**
287 * Test that only users with a user context are fetched.
288 */
289 public function test_get_users_in_context() {
290 $this->resetAfterTest();
291
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);
297
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);
304
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 }
311
312 /**
313 * Test that data for users in approved userlist is deleted.
314 */
315 public function test_delete_data_for_users() {
316 global $DB;
317
318 $this->resetAfterTest();
319
320 $component = 'core_user';
321
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);
335
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);
349
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);
356
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);
361
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);
385
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 }
408
c49f3092
AG
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);
425
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);
433
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);
442
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);
457
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);
470
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 }
484
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';
494
495 if ($length > 0) {
496
497 $response = '';
498 $source = str_split($source, 1);
499
500 for ($i = 1; $i <= $length; $i++) {
501 $num = mt_rand(1, count($source));
502 $response .= $source[$num - 1];
503 }
504 }
505
506 return $response;
507 }
508}