See Release Notes
Long Term Support Release
Differences Between: [Versions 401 and 402] [Versions 401 and 403]
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 * Privacy provider tests. 19 * 20 * @package mod_quiz 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 namespace mod_quiz\privacy; 25 26 use core_privacy\local\metadata\collection; 27 use core_privacy\local\request\deletion_criteria; 28 use core_privacy\local\request\writer; 29 use mod_quiz\privacy\provider; 30 use mod_quiz\privacy\helper; 31 32 defined('MOODLE_INTERNAL') || die(); 33 34 global $CFG; 35 require_once($CFG->dirroot . '/question/tests/privacy_helper.php'); 36 37 /** 38 * Privacy provider tests class. 39 * 40 * @package mod_quiz 41 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> 42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 */ 44 class provider_test extends \core_privacy\tests\provider_testcase { 45 46 use \core_question_privacy_helper; 47 48 /** 49 * Test that a user who has no data gets no contexts 50 */ 51 public function test_get_contexts_for_userid_no_data() { 52 global $USER; 53 $this->resetAfterTest(); 54 $this->setAdminUser(); 55 56 $contextlist = provider::get_contexts_for_userid($USER->id); 57 $this->assertEmpty($contextlist); 58 } 59 60 /** 61 * Test for provider::get_contexts_for_userid() when there is no quiz attempt at all. 62 */ 63 public function test_get_contexts_for_userid_no_attempt_with_override() { 64 global $DB; 65 $this->resetAfterTest(true); 66 67 $course = $this->getDataGenerator()->create_course(); 68 $user = $this->getDataGenerator()->create_user(); 69 70 // Make a quiz with an override. 71 $this->setUser(); 72 $quiz = $this->create_test_quiz($course); 73 $DB->insert_record('quiz_overrides', [ 74 'quiz' => $quiz->id, 75 'userid' => $user->id, 76 'timeclose' => 1300, 77 'timelimit' => null, 78 ]); 79 80 $cm = get_coursemodule_from_instance('quiz', $quiz->id); 81 $context = \context_module::instance($cm->id); 82 83 // Fetch the contexts - only one context should be returned. 84 $this->setUser(); 85 $contextlist = provider::get_contexts_for_userid($user->id); 86 $this->assertCount(1, $contextlist); 87 $this->assertEquals($context, $contextlist->current()); 88 } 89 90 /** 91 * The export function should handle an empty contextlist properly. 92 */ 93 public function test_export_user_data_no_data() { 94 global $USER; 95 $this->resetAfterTest(); 96 $this->setAdminUser(); 97 98 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( 99 \core_user::get_user($USER->id), 100 'mod_quiz', 101 [] 102 ); 103 104 provider::export_user_data($approvedcontextlist); 105 $this->assertDebuggingNotCalled(); 106 107 // No data should have been exported. 108 $writer = \core_privacy\local\request\writer::with_context(\context_system::instance()); 109 $this->assertFalse($writer->has_any_data_in_any_context()); 110 } 111 112 /** 113 * The delete function should handle an empty contextlist properly. 114 */ 115 public function test_delete_data_for_user_no_data() { 116 global $USER; 117 $this->resetAfterTest(); 118 $this->setAdminUser(); 119 120 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( 121 \core_user::get_user($USER->id), 122 'mod_quiz', 123 [] 124 ); 125 126 provider::delete_data_for_user($approvedcontextlist); 127 $this->assertDebuggingNotCalled(); 128 } 129 130 /** 131 * Export + Delete quiz data for a user who has made a single attempt. 132 */ 133 public function test_user_with_data() { 134 global $DB; 135 $this->resetAfterTest(true); 136 137 $course = $this->getDataGenerator()->create_course(); 138 $user = $this->getDataGenerator()->create_user(); 139 $otheruser = $this->getDataGenerator()->create_user(); 140 141 // Make a quiz with an override. 142 $this->setUser(); 143 $quiz = $this->create_test_quiz($course); 144 $DB->insert_record('quiz_overrides', [ 145 'quiz' => $quiz->id, 146 'userid' => $user->id, 147 'timeclose' => 1300, 148 'timelimit' => null, 149 ]); 150 151 // Run as the user and make an attempt on the quiz. 152 list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user); 153 $this->attempt_quiz($quiz, $otheruser); 154 $context = $quizobj->get_context(); 155 156 // Fetch the contexts - only one context should be returned. 157 $this->setUser(); 158 $contextlist = provider::get_contexts_for_userid($user->id); 159 $this->assertCount(1, $contextlist); 160 $this->assertEquals($context, $contextlist->current()); 161 162 // Perform the export and check the data. 163 $this->setUser($user); 164 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( 165 \core_user::get_user($user->id), 166 'mod_quiz', 167 $contextlist->get_contextids() 168 ); 169 provider::export_user_data($approvedcontextlist); 170 171 // Ensure that the quiz data was exported correctly. 172 $writer = writer::with_context($context); 173 $this->assertTrue($writer->has_any_data()); 174 175 $quizdata = $writer->get_data([]); 176 $this->assertEquals($quizobj->get_quiz_name(), $quizdata->name); 177 178 // Every module has an intro. 179 $this->assertTrue(isset($quizdata->intro)); 180 181 // Fetch the attempt data. 182 $attempt = $attemptobj->get_attempt(); 183 $attemptsubcontext = [ 184 get_string('attempts', 'mod_quiz'), 185 $attempt->attempt, 186 ]; 187 $attemptdata = writer::with_context($context)->get_data($attemptsubcontext); 188 189 $attempt = $attemptobj->get_attempt(); 190 $this->assertTrue(isset($attemptdata->state)); 191 $this->assertEquals(\quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state); 192 $this->assertTrue(isset($attemptdata->timestart)); 193 $this->assertTrue(isset($attemptdata->timefinish)); 194 $this->assertTrue(isset($attemptdata->timemodified)); 195 $this->assertFalse(isset($attemptdata->timemodifiedoffline)); 196 $this->assertFalse(isset($attemptdata->timecheckstate)); 197 198 $this->assertTrue(isset($attemptdata->grade)); 199 $this->assertEquals(100.00, $attemptdata->grade->grade); 200 201 // Check that the exported question attempts are correct. 202 $attemptsubcontext = helper::get_quiz_attempt_subcontext($attemptobj->get_attempt(), $user); 203 $this->assert_question_attempt_exported( 204 $context, 205 $attemptsubcontext, 206 \question_engine::load_questions_usage_by_activity($attemptobj->get_uniqueid()), 207 quiz_get_review_options($quiz, $attemptobj->get_attempt(), $context), 208 $user 209 ); 210 211 // Delete the data and check it is removed. 212 $this->setUser(); 213 provider::delete_data_for_user($approvedcontextlist); 214 $this->expectException(\dml_missing_record_exception::class); 215 \quiz_attempt::create($attemptobj->get_quizid()); 216 } 217 218 /** 219 * Export + Delete quiz data for a user who has made a single attempt. 220 */ 221 public function test_user_with_preview() { 222 global $DB; 223 $this->resetAfterTest(true); 224 225 // Make a quiz. 226 $course = $this->getDataGenerator()->create_course(); 227 $user = $this->getDataGenerator()->create_user(); 228 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 229 230 $quiz = $quizgenerator->create_instance([ 231 'course' => $course->id, 232 'questionsperpage' => 0, 233 'grade' => 100.0, 234 'sumgrades' => 2, 235 ]); 236 237 // Create a couple of questions. 238 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 239 $cat = $questiongenerator->create_question_category(); 240 241 $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 242 quiz_add_quiz_question($saq->id, $quiz); 243 $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 244 quiz_add_quiz_question($numq->id, $quiz); 245 246 // Run as the user and make an attempt on the quiz. 247 $this->setUser($user); 248 $starttime = time(); 249 $quizobj = \quiz::create($quiz->id, $user->id); 250 $context = $quizobj->get_context(); 251 252 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 253 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 254 255 // Start the attempt. 256 $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, true, $user->id); 257 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime); 258 quiz_attempt_save_started($quizobj, $quba, $attempt); 259 260 // Answer the questions. 261 $attemptobj = \quiz_attempt::create($attempt->id); 262 263 $tosubmit = [ 264 1 => ['answer' => 'frog'], 265 2 => ['answer' => '3.14'], 266 ]; 267 268 $attemptobj->process_submitted_actions($starttime, false, $tosubmit); 269 270 // Finish the attempt. 271 $attemptobj = \quiz_attempt::create($attempt->id); 272 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 273 $attemptobj->process_finish($starttime, false); 274 275 // Fetch the contexts - no context should be returned. 276 $this->setUser(); 277 $contextlist = provider::get_contexts_for_userid($user->id); 278 $this->assertCount(0, $contextlist); 279 } 280 281 /** 282 * Export + Delete quiz data for a user who has made a single attempt. 283 */ 284 public function test_delete_data_for_all_users_in_context() { 285 global $DB; 286 $this->resetAfterTest(true); 287 288 $course = $this->getDataGenerator()->create_course(); 289 $user = $this->getDataGenerator()->create_user(); 290 $otheruser = $this->getDataGenerator()->create_user(); 291 292 // Make a quiz with an override. 293 $this->setUser(); 294 $quiz = $this->create_test_quiz($course); 295 $DB->insert_record('quiz_overrides', [ 296 'quiz' => $quiz->id, 297 'userid' => $user->id, 298 'timeclose' => 1300, 299 'timelimit' => null, 300 ]); 301 302 // Run as the user and make an attempt on the quiz. 303 list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user); 304 list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $otheruser); 305 306 // Create another quiz and questions, and repeat the data insertion. 307 $this->setUser(); 308 $otherquiz = $this->create_test_quiz($course); 309 $DB->insert_record('quiz_overrides', [ 310 'quiz' => $otherquiz->id, 311 'userid' => $user->id, 312 'timeclose' => 1300, 313 'timelimit' => null, 314 ]); 315 316 // Run as the user and make an attempt on the quiz. 317 list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $user); 318 list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $otheruser); 319 320 // Delete all data for all users in the context under test. 321 $this->setUser(); 322 $context = $quizobj->get_context(); 323 provider::delete_data_for_all_users_in_context($context); 324 325 // The quiz attempt should have been deleted from this quiz. 326 $this->assertCount(0, $DB->get_records('quiz_attempts', ['quiz' => $quizobj->get_quizid()])); 327 $this->assertCount(0, $DB->get_records('quiz_overrides', ['quiz' => $quizobj->get_quizid()])); 328 $this->assertCount(0, $DB->get_records('question_attempts', ['questionusageid' => $quba->get_id()])); 329 330 // But not for the other quiz. 331 $this->assertNotCount(0, $DB->get_records('quiz_attempts', ['quiz' => $otherquizobj->get_quizid()])); 332 $this->assertNotCount(0, $DB->get_records('quiz_overrides', ['quiz' => $otherquizobj->get_quizid()])); 333 $this->assertNotCount(0, $DB->get_records('question_attempts', ['questionusageid' => $otherquba->get_id()])); 334 } 335 336 /** 337 * Export + Delete quiz data for a user who has made a single attempt. 338 */ 339 public function test_wrong_context() { 340 global $DB; 341 $this->resetAfterTest(true); 342 343 $course = $this->getDataGenerator()->create_course(); 344 $user = $this->getDataGenerator()->create_user(); 345 346 // Make a choice. 347 $this->setUser(); 348 $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice'); 349 $choice = $plugingenerator->create_instance(['course' => $course->id]); 350 $cm = get_coursemodule_from_instance('choice', $choice->id); 351 $context = \context_module::instance($cm->id); 352 353 // Fetch the contexts - no context should be returned. 354 $this->setUser(); 355 $contextlist = provider::get_contexts_for_userid($user->id); 356 $this->assertCount(0, $contextlist); 357 358 // Perform the export and check the data. 359 $this->setUser($user); 360 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( 361 \core_user::get_user($user->id), 362 'mod_quiz', 363 [$context->id] 364 ); 365 provider::export_user_data($approvedcontextlist); 366 367 // Ensure that nothing was exported. 368 $writer = writer::with_context($context); 369 $this->assertFalse($writer->has_any_data_in_any_context()); 370 371 $this->setUser(); 372 373 $dbwrites = $DB->perf_get_writes(); 374 375 // Perform a deletion with the approved contextlist containing an incorrect context. 376 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( 377 \core_user::get_user($user->id), 378 'mod_quiz', 379 [$context->id] 380 ); 381 provider::delete_data_for_user($approvedcontextlist); 382 $this->assertEquals($dbwrites, $DB->perf_get_writes()); 383 $this->assertDebuggingNotCalled(); 384 385 // Perform a deletion of all data in the context. 386 provider::delete_data_for_all_users_in_context($context); 387 $this->assertEquals($dbwrites, $DB->perf_get_writes()); 388 $this->assertDebuggingNotCalled(); 389 } 390 391 /** 392 * Create a test quiz for the specified course. 393 * 394 * @param \stdClass $course 395 * @return array 396 */ 397 protected function create_test_quiz($course) { 398 global $DB; 399 400 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 401 402 $quiz = $quizgenerator->create_instance([ 403 'course' => $course->id, 404 'questionsperpage' => 0, 405 'grade' => 100.0, 406 'sumgrades' => 2, 407 ]); 408 409 // Create a couple of questions. 410 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 411 $cat = $questiongenerator->create_question_category(); 412 413 $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 414 quiz_add_quiz_question($saq->id, $quiz); 415 $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 416 quiz_add_quiz_question($numq->id, $quiz); 417 418 return $quiz; 419 } 420 421 /** 422 * Answer questions for a quiz + user. 423 * 424 * @param \stdClass $quiz 425 * @param \stdClass $user 426 * @return array 427 */ 428 protected function attempt_quiz($quiz, $user) { 429 $this->setUser($user); 430 431 $starttime = time(); 432 $quizobj = \quiz::create($quiz->id, $user->id); 433 $context = $quizobj->get_context(); 434 435 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 436 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 437 438 // Start the attempt. 439 $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id); 440 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime); 441 quiz_attempt_save_started($quizobj, $quba, $attempt); 442 443 // Answer the questions. 444 $attemptobj = \quiz_attempt::create($attempt->id); 445 446 $tosubmit = [ 447 1 => ['answer' => 'frog'], 448 2 => ['answer' => '3.14'], 449 ]; 450 451 $attemptobj->process_submitted_actions($starttime, false, $tosubmit); 452 453 // Finish the attempt. 454 $attemptobj = \quiz_attempt::create($attempt->id); 455 $attemptobj->process_finish($starttime, false); 456 457 $this->setUser(); 458 459 return [$quizobj, $quba, $attemptobj]; 460 } 461 462 /** 463 * Test for provider::get_users_in_context(). 464 */ 465 public function test_get_users_in_context() { 466 global $DB; 467 $this->resetAfterTest(true); 468 469 $course = $this->getDataGenerator()->create_course(); 470 $user = $this->getDataGenerator()->create_user(); 471 $anotheruser = $this->getDataGenerator()->create_user(); 472 $extrauser = $this->getDataGenerator()->create_user(); 473 474 // Make a quiz. 475 $this->setUser(); 476 $quiz = $this->create_test_quiz($course); 477 478 // Create an override for user1. 479 $DB->insert_record('quiz_overrides', [ 480 'quiz' => $quiz->id, 481 'userid' => $user->id, 482 'timeclose' => 1300, 483 'timelimit' => null, 484 ]); 485 486 // Make an attempt on the quiz as user2. 487 list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $anotheruser); 488 $context = $quizobj->get_context(); 489 490 // Fetch users - user1 and user2 should be returned. 491 $userlist = new \core_privacy\local\request\userlist($context, 'mod_quiz'); 492 provider::get_users_in_context($userlist); 493 $this->assertEqualsCanonicalizing( 494 [$user->id, $anotheruser->id], 495 $userlist->get_userids()); 496 } 497 498 /** 499 * Test for provider::delete_data_for_users(). 500 */ 501 public function test_delete_data_for_users() { 502 global $DB; 503 $this->resetAfterTest(true); 504 505 $user1 = $this->getDataGenerator()->create_user(); 506 $user2 = $this->getDataGenerator()->create_user(); 507 $user3 = $this->getDataGenerator()->create_user(); 508 509 $course1 = $this->getDataGenerator()->create_course(); 510 $course2 = $this->getDataGenerator()->create_course(); 511 512 // Make a quiz in each course. 513 $quiz1 = $this->create_test_quiz($course1); 514 $quiz2 = $this->create_test_quiz($course2); 515 516 // Attempt quiz1 as user1 and user2. 517 list($quiz1obj) = $this->attempt_quiz($quiz1, $user1); 518 $this->attempt_quiz($quiz1, $user2); 519 520 // Create an override in quiz1 for user3. 521 $DB->insert_record('quiz_overrides', [ 522 'quiz' => $quiz1->id, 523 'userid' => $user3->id, 524 'timeclose' => 1300, 525 'timelimit' => null, 526 ]); 527 528 // Attempt quiz2 as user1. 529 $this->attempt_quiz($quiz2, $user1); 530 531 // Delete the data for user1 and user3 in course1 and check it is removed. 532 $quiz1context = $quiz1obj->get_context(); 533 $approveduserlist = new \core_privacy\local\request\approved_userlist($quiz1context, 'mod_quiz', 534 [$user1->id, $user3->id]); 535 provider::delete_data_for_users($approveduserlist); 536 537 // Only the attempt of user2 should be remained in quiz1. 538 $this->assertEquals( 539 [$user2->id], 540 $DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz1->id]) 541 ); 542 543 // The attempt that user1 made in quiz2 should be remained. 544 $this->assertEquals( 545 [$user1->id], 546 $DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz2->id]) 547 ); 548 549 // The quiz override in quiz1 that we had for user3 should be deleted. 550 $this->assertEquals( 551 [], 552 $DB->get_fieldset_select('quiz_overrides', 'userid', 'quiz = ?', [$quiz1->id]) 553 ); 554 } 555 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body