Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }