Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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  }