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