Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

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